From 668992754e0cbeaa6822b5d90118ac1bcff37e0c Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Tue, 20 Aug 2024 12:00:05 -0500 Subject: [PATCH 1/3] Consolidate caching logic --- data_prototype/artist.py | 28 +++++++++++++++++++++++++ data_prototype/conversion_edge.py | 9 ++++++++ data_prototype/patches.py | 34 ++++++++++++++++--------------- 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/data_prototype/artist.py b/data_prototype/artist.py index 9426c52..2776888 100644 --- a/data_prototype/artist.py +++ b/data_prototype/artist.py @@ -1,4 +1,5 @@ from bisect import insort +from collections import OrderedDict from typing import Sequence from contextlib import contextmanager @@ -33,6 +34,8 @@ def __init__( **{"x": np.asarray([0, 1]), "y": np.asarray([0, 1])} ) + self._caches = {} + def draw(self, renderer, graph: Graph) -> None: return @@ -119,6 +122,31 @@ def pick(self, mouseevent, graph: Graph | None = None): # which do not have an axes property but children might a.pick(mouseevent, graph) + def _get_dynamic_graph(self, query): + return Graph([]) + + def _query_and_eval(self, container, requires, graph, cacheset=None): + g = graph + self._graph + query, q_cache_key = container.query(g) + g = g + self._get_dynamic_graph(query) + g_cache_key = g.cache_key() + cache_key = (g_cache_key, q_cache_key) + + cache = None + if cacheset is not None: + cache = self._caches.setdefault(cacheset, OrderedDict()) + if cache_key in cache: + return cache[cache_key] + + conv = g.evaluator(container.describe(), requires) + ret = conv.evaluate(query) + + if cache is not None: + cache[cache_key] = ret + # TODO prune + + return ret + class CompatibilityArtist: """A compatibility shim to ducktype as a classic Matplotlib Artist. diff --git a/data_prototype/conversion_edge.py b/data_prototype/conversion_edge.py index 2a09cc1..20adc9f 100644 --- a/data_prototype/conversion_edge.py +++ b/data_prototype/conversion_edge.py @@ -418,3 +418,12 @@ def __add__(self, other: Graph) -> Graph: aother = {k: v for k, v in other._aliases} aliases = tuple((aself | aother).items()) return Graph(self._edges + other._edges, aliases) + + def cache_key(self): + """A cache key representing the graph. + + Current implementation is a new UUID, that is to say uncachable. + """ + import uuid + + return str(uuid.uuid4()) diff --git a/data_prototype/patches.py b/data_prototype/patches.py index daf50d8..4d6eb46 100644 --- a/data_prototype/patches.py +++ b/data_prototype/patches.py @@ -7,10 +7,9 @@ from .wrappers import ProxyWrapper, _stale_wrapper -from .containers import DataContainer - from .artist import Artist, _renderer_group from .description import Desc +from .containers import DataContainer from .conversion_edge import Graph, CoordinateEdge, DefaultEdge @@ -19,7 +18,7 @@ def __init__(self, container, edges=None, **kwargs): super().__init__(container, edges, **kwargs) scalar = Desc((), "display") # ... this needs thinking... - edges = [ + def_edges = [ CoordinateEdge.from_coords("xycoords", {"x": "auto", "y": "auto"}, "data"), CoordinateEdge.from_coords("codes", {"codes": "auto"}, "display"), CoordinateEdge.from_coords("facecolor", {"color": Desc(())}, "display"), @@ -34,12 +33,11 @@ def __init__(self, container, edges=None, **kwargs): DefaultEdge.from_default_value("alpha_def", "alpha", scalar, 1), DefaultEdge.from_default_value("hatch_def", "hatch", scalar, None), ] - self._graph = self._graph + Graph(edges) + self._graph = self._graph + Graph(def_edges) def draw(self, renderer, graph: Graph) -> None: if not self.get_visible(): return - g = graph + self._graph desc = Desc(("N",), "display") scalar = Desc((), "display") # ... this needs thinking... @@ -55,18 +53,14 @@ def draw(self, renderer, graph: Graph) -> None: "alpha": scalar, } - # copy from line - conv = g.evaluator(self._container.describe(), require) - query, _ = self._container.query(g) - evald = conv.evaluate(query) - - clip_conv = g.evaluator( - self._clip_box.describe(), - {"x": Desc(("N",), "display"), "y": Desc(("N",), "display")}, + evald = self._query_and_eval( + self._container, require, graph, cacheset="default" ) - clip_query, _ = self._clip_box.query(g) - clipx, clipy = clip_conv.evaluate(clip_query).values() - # copy from line + + clip_req = {"x": Desc(("N",), "display"), "y": Desc(("N",), "display")} + clipx, clipy = self._query_and_eval( + self._clip_box, clip_req, graph, cacheset="clip" + ).values() path = mpath.Path._fast_from_codes_and_verts( verts=np.vstack([evald["x"], evald["y"]]).T, codes=evald["codes"] @@ -111,6 +105,14 @@ def draw(self, renderer, graph: Graph) -> None: gc.restore() +class RectangleContainer(DataContainer): ... + + +class Rectangle(Patch): + def __init__(self, container, edges=None, **kwargs): + super().__init__(container, edges, **kwargs) + + class PatchWrapper(ProxyWrapper): _wrapped_class = _Patch _privtized_methods = ( From c0321da12fcaf3427a3ae37cd274617c48dc74fd Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Wed, 28 Aug 2024 14:08:38 -0500 Subject: [PATCH 2/3] adjust to include graph, avoid infinite cache leak --- data_prototype/artist.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/data_prototype/artist.py b/data_prototype/artist.py index 2776888..8d5b56f 100644 --- a/data_prototype/artist.py +++ b/data_prototype/artist.py @@ -122,13 +122,13 @@ def pick(self, mouseevent, graph: Graph | None = None): # which do not have an axes property but children might a.pick(mouseevent, graph) - def _get_dynamic_graph(self, query): + def _get_dynamic_graph(self, query, description, graph): return Graph([]) def _query_and_eval(self, container, requires, graph, cacheset=None): g = graph + self._graph query, q_cache_key = container.query(g) - g = g + self._get_dynamic_graph(query) + g = g + self._get_dynamic_graph(query, container.describe(), graph) g_cache_key = g.cache_key() cache_key = (g_cache_key, q_cache_key) @@ -141,9 +141,9 @@ def _query_and_eval(self, container, requires, graph, cacheset=None): conv = g.evaluator(container.describe(), requires) ret = conv.evaluate(query) - if cache is not None: - cache[cache_key] = ret - # TODO prune + # TODO: actually add to cache and prune + # if cache is not None: + # cache[cache_key] = ret return ret From 5581ada2b98f6a76cc7740ae02d7d4ad92ff3153 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 7 Feb 2025 13:36:32 -0600 Subject: [PATCH 3/3] Lay groundwork for caching of patches Currently does not do a lot of actual caching, but the hooks are present to be filled out when focusing on efficiency --- data_prototype/artist.py | 4 +- data_prototype/patches.py | 196 ++++++++++++++++++++++++++++++++++++-- examples/simple_patch.py | 70 ++++++++------ 3 files changed, 231 insertions(+), 39 deletions(-) diff --git a/data_prototype/artist.py b/data_prototype/artist.py index 8d5b56f..9adbb23 100644 --- a/data_prototype/artist.py +++ b/data_prototype/artist.py @@ -122,13 +122,13 @@ def pick(self, mouseevent, graph: Graph | None = None): # which do not have an axes property but children might a.pick(mouseevent, graph) - def _get_dynamic_graph(self, query, description, graph): + def _get_dynamic_graph(self, query, description, graph, cacheset): return Graph([]) def _query_and_eval(self, container, requires, graph, cacheset=None): g = graph + self._graph query, q_cache_key = container.query(g) - g = g + self._get_dynamic_graph(query, container.describe(), graph) + g = g + self._get_dynamic_graph(query, container.describe(), graph, cacheset) g_cache_key = g.cache_key() cache_key = (g_cache_key, q_cache_key) diff --git a/data_prototype/patches.py b/data_prototype/patches.py index 4d6eb46..d83262f 100644 --- a/data_prototype/patches.py +++ b/data_prototype/patches.py @@ -8,9 +8,9 @@ from .wrappers import ProxyWrapper, _stale_wrapper from .artist import Artist, _renderer_group -from .description import Desc +from .description import Desc, desc_like from .containers import DataContainer -from .conversion_edge import Graph, CoordinateEdge, DefaultEdge +from .conversion_edge import Graph, CoordinateEdge, DefaultEdge, TransformEdge class Patch(Artist): @@ -21,8 +21,14 @@ def __init__(self, container, edges=None, **kwargs): def_edges = [ CoordinateEdge.from_coords("xycoords", {"x": "auto", "y": "auto"}, "data"), CoordinateEdge.from_coords("codes", {"codes": "auto"}, "display"), - CoordinateEdge.from_coords("facecolor", {"color": Desc(())}, "display"), - CoordinateEdge.from_coords("edgecolor", {"color": Desc(())}, "display"), + CoordinateEdge.from_coords("facecolor", {"facecolor": Desc(())}, "display"), + CoordinateEdge.from_coords("edgecolor", {"edgecolor": Desc(())}, "display"), + CoordinateEdge.from_coords( + "facecolor_rgba", {"facecolor": Desc(("M",))}, "display" + ), + CoordinateEdge.from_coords( + "edgecolor_rgba", {"edgecolor": Desc(("M",))}, "display" + ), CoordinateEdge.from_coords("linewidth", {"linewidth": Desc(())}, "display"), CoordinateEdge.from_coords("hatch", {"hatch": Desc(())}, "display"), CoordinateEdge.from_coords("alpha", {"alpha": Desc(())}, "display"), @@ -45,8 +51,8 @@ def draw(self, renderer, graph: Graph) -> None: "x": desc, "y": desc, "codes": desc, - "facecolor": scalar, - "edgecolor": scalar, + "facecolor": Desc((), "display"), + "edgecolor": Desc(("M",), "display"), "linewidth": scalar, "linestyle": scalar, "hatch": scalar, @@ -69,7 +75,7 @@ def draw(self, renderer, graph: Graph) -> None: with _renderer_group(renderer, "patch", None): gc = renderer.new_gc() - gc.set_foreground(evald["facecolor"], isRGBA=False) + gc.set_foreground(evald["edgecolor"], isRGBA=False) gc.set_clip_rectangle( mtransforms.Bbox.from_extents(clipx[0], clipy[0], clipx[1], clipy[1]) ) @@ -112,6 +118,182 @@ class Rectangle(Patch): def __init__(self, container, edges=None, **kwargs): super().__init__(container, edges, **kwargs) + rect = mpath.Path.unit_rectangle() + + desc = Desc((4,), "abstract_path") + scalar = Desc((), "data") + scalar_auto = Desc(()) + def_edges = [ + CoordinateEdge.from_coords( + "llxycoords", + {"lower_left_x": scalar_auto, "lower_left_y": scalar_auto}, + "data", + ), + CoordinateEdge.from_coords( + "urxycoords", + {"upper_right_x": scalar_auto, "upper_right_y": scalar_auto}, + "data", + ), + CoordinateEdge.from_coords( + "rpxycoords", + {"rotation_point_x": scalar_auto, "rotation_point_y": scalar_auto}, + "data", + ), + CoordinateEdge.from_coords("anglecoords", {"angle": scalar_auto}, "data"), + DefaultEdge.from_default_value( + "x_def", "x", desc, rect.vertices.T[0], weight=0.1 + ), + DefaultEdge.from_default_value( + "y_def", "y", desc, rect.vertices.T[1], weight=0.1 + ), + DefaultEdge.from_default_value( + "codes_def", + "codes", + desc_like(desc, coordinates="display"), + rect.codes, + weight=0.1, + ), + DefaultEdge.from_default_value("angle_def", "angle", scalar, 0), + DefaultEdge.from_default_value( + "rotation_point_x_def", "rotation_point_x", scalar, 0 + ), + DefaultEdge.from_default_value( + "rotation_point_y_def", "rotation_point_y", scalar, 0 + ), + ] + + self._graph = self._graph + Graph(def_edges) + + def _get_dynamic_graph(self, query, description, graph, cacheset): + if cacheset == "clip": + return Graph([]) + + desc = Desc((), "data") + + requires = { + "upper_right_x": desc, + "upper_right_y": desc, + "lower_left_x": desc, + "lower_left_y": desc, + "angle": desc, + "rotation_point_x": desc, + "rotation_point_y": desc, + } + + g = graph + self._graph + + conv = g.evaluator(description, requires) + evald = conv.evaluate(query) + + bbox = mtransforms.Bbox.from_extents( + evald["lower_left_x"], + evald["lower_left_y"], + evald["upper_right_x"], + evald["upper_right_y"], + ) + rotation_point = (evald["rotation_point_x"], evald["rotation_point_y"]) + + scale = mtransforms.BboxTransformTo(bbox) + rotate = ( + mtransforms.Affine2D() + .translate(-rotation_point[0], -rotation_point[1]) + .rotate_deg(evald["angle"]) + .translate(*rotation_point) + ) + + descn: Desc = Desc(("N",), coordinates="data") + xy: dict[str, Desc] = {"x": descn, "y": descn} + edges = [ + TransformEdge( + "scale_and_rotate", + desc_like(xy, coordinates="abstract_path"), + xy, + transform=scale + rotate, + ) + ] + + return Graph(edges) + + +class RegularPolygon(Patch): + def __init__(self, container, edges=None, **kwargs): + super().__init__(container, edges, **kwargs) + + scalar = Desc((), "data") + scalar_auto = Desc(()) + def_edges = [ + CoordinateEdge.from_coords( + "centercoords", + {"center_x": scalar_auto, "center_y": scalar_auto}, + "data", + ), + CoordinateEdge.from_coords( + "orientationcoords", {"orientation": scalar_auto}, "data" + ), + CoordinateEdge.from_coords("radiuscoords", {"radius": scalar_auto}, "data"), + CoordinateEdge.from_coords( + "num_vertices_coords", {"num_vertices": scalar_auto}, "data" + ), + DefaultEdge.from_default_value("orientation_def", "orientation", scalar, 0), + DefaultEdge.from_default_value("radius_def", "radius", scalar, 5), + ] + + self._graph = self._graph + Graph(def_edges) + + def _get_dynamic_graph(self, query, description, graph, cacheset): + if cacheset == "clip": + return Graph([]) + + desc = Desc((), "data") + desc_abs = Desc(("N",), "abstract_path") + + requires = { + "center_x": desc, + "center_y": desc, + "radius": desc, + "orientation": desc, + "num_vertices": desc, + } + + g = graph + self._graph + + conv = g.evaluator(description, requires) + evald = conv.evaluate(query) + + circ = mpath.Path.unit_regular_polygon(evald["num_vertices"]) + + scale = mtransforms.Affine2D().scale(evald["radius"]) + rotate = mtransforms.Affine2D().rotate(evald["orientation"]) + translate = mtransforms.Affine2D().translate( + evald["center_x"], evald["center_y"] + ) + + descn: Desc = Desc(("N",), coordinates="data") + xy: dict[str, Desc] = {"x": descn, "y": descn} + edges = [ + TransformEdge( + "scale_and_rotate", + desc_like(xy, coordinates="abstract_path"), + xy, + transform=scale + rotate + translate, + ), + DefaultEdge.from_default_value( + "x_def", "x", desc_abs, circ.vertices.T[0], weight=0.1 + ), + DefaultEdge.from_default_value( + "y_def", "y", desc_abs, circ.vertices.T[1], weight=0.1 + ), + DefaultEdge.from_default_value( + "codes_def", + "codes", + desc_like(desc_abs, coordinates="display"), + circ.codes, + weight=0.1, + ), + ] + + return Graph(edges) + class PatchWrapper(ProxyWrapper): _wrapped_class = _Patch diff --git a/examples/simple_patch.py b/examples/simple_patch.py index 91bce7c..ebfe959 100644 --- a/examples/simple_patch.py +++ b/examples/simple_patch.py @@ -12,53 +12,63 @@ import numpy as np import matplotlib.pyplot as plt +import matplotlib.patches as mpatches from data_prototype.containers import ArrayContainer +from data_prototype.artist import CompatibilityAxes -from data_prototype.patches import RectangleWrapper +from data_prototype.patches import Rectangle cont1 = ArrayContainer( - x=np.array([-3]), - y=np.array([0]), - width=np.array([2]), - height=np.array([3]), - angle=np.array([0]), - rotation_point=np.array(["center"]), + lower_left_x=np.array(-3), + lower_left_y=np.array(0), + upper_right_x=np.array(-1), + upper_right_y=np.array(3), edgecolor=np.array([0, 0, 0]), - facecolor=np.array([0.0, 0.7, 0, 0.5]), - linewidth=np.array([3]), - linestyle=np.array(["-"]), + hatch_color=np.array([0, 0, 0]), + facecolor="green", + linewidth=3, + linestyle="-", antialiased=np.array([True]), - hatch=np.array(["*"]), + hatch="*", fill=np.array([True]), capstyle=np.array(["round"]), joinstyle=np.array(["miter"]), + alpha=np.array(0.5), ) cont2 = ArrayContainer( - x=np.array([0]), - y=np.array([1]), - width=np.array([2]), - height=np.array([3]), - angle=np.array([30]), - rotation_point=np.array(["center"]), - edgecolor=np.array([0, 0, 0]), - facecolor=np.array([0.7, 0, 0]), - linewidth=np.array([6]), - linestyle=np.array(["-"]), + lower_left_x=0, + lower_left_y=np.array(1), + upper_right_x=np.array(2), + upper_right_y=np.array(5), + angle=30, + rotation_point_x=np.array(1), + rotation_point_y=np.array(3.5), + edgecolor=np.array([0.5, 0.2, 0]), + hatch_color=np.array([0, 0, 0]), + facecolor="red", + linewidth=6, + linestyle="-", antialiased=np.array([True]), - hatch=np.array([""]), + hatch="", fill=np.array([True]), - capstyle=np.array(["butt"]), - joinstyle=np.array(["round"]), + capstyle=np.array(["round"]), + joinstyle=np.array(["miter"]), ) -fig, ax = plt.subplots() -ax.set_xlim(-5, 5) -ax.set_ylim(0, 5) -rect1 = RectangleWrapper(cont1, {}) -rect2 = RectangleWrapper(cont2, {}) +fig, nax = plt.subplots() +ax = CompatibilityAxes(nax) +nax.add_artist(ax) +nax.set_xlim(-5, 5) +nax.set_ylim(0, 5) + +rect = mpatches.Rectangle((4, 1), 2, 3, linewidth=6, edgecolor="black", angle=30) +nax.add_artist(rect) + +rect1 = Rectangle(cont1, {}) +rect2 = Rectangle(cont2, {}) ax.add_artist(rect1) ax.add_artist(rect2) -ax.set_aspect(1) +nax.set_aspect(1) plt.show()