Contouring#

Developer: Andrew Nolan (@andrewdnolan)

Overview#

For complete visualization capabilities in mosaic, we need to be able to create contour and filled contour plots. Unfortunately, we are unable to use the existing matplotlib.pyplot.contour and matplotlib.pyplot.contourf functions, due to two matplotlib requirements:

  1. That the X and Y coordinates arrays represent a regularly spaces, structured grid.

  2. That the Z data being contoured be coincidental with the X and Y coordinates on the mesh.

The above requirements are enforced, because “under the hood” matplotlib uses the contourpy packages, which is a C++ implementation of the marching squares algorithm. The unstrucuted nature of MPAS meshes prevents (1) from ever being satisfied. Because MPAS meshes represent control volumes, data are not defined at discrete “nodes” but over a control volumes, therefore preventing (2) from being easily satisfied. The analogous marching triangles algorithm could work for out unstructured data, but contourpy has not implemented it. From a cursory look, there do not seem to be any implementations of marching triangles, with a python interface, as mature and well mainted as the marching squares algorithm within contourpy.

Instead, we’ve opted to follow Andrew Robert’s Ridgepack MATLAB package and use the mesh connectivity information to create contours of unstrucuted MPAS meshes. The implementation below follows Ridgepack, but we have opted to avoid a direct port of the MATLAB code. Instead we crib from the approach, but try to implement as much as possible using external python libraries to increase robustness and performance.

Rough Sketch of Implementation#

Let’s start by considering a small (\(20 \times 20\) cell) planar non-periodic MPAS mesh. The initial field we will consider will be a 2D gaussian with an amplitude (\(A\)) of \(2\) and standard deviation (\(\sigma\)) of \(0.15\):

ds = planar_hex_mesh(20)
gauss = gaussian(ds.xCell, ds.yCell)
descriptor = mosaic.Descriptor(ds)
Error in callback <function _draw_all_if_interactive at 0x7f4811a4ba00> (for post_execute), with arguments args (),kwargs {}:
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/pyplot.py:278, in _draw_all_if_interactive()
    276 def _draw_all_if_interactive() -> None:
    277     if matplotlib.is_interactive():
--> 278         draw_all()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_pylab_helpers.py:131, in Gcf.draw_all(cls, force)
    129 for manager in cls.get_all_fig_managers():
    130     if force or manager.canvas.figure.stale:
--> 131         manager.canvas.draw_idle()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:1893, in FigureCanvasBase.draw_idle(self, *args, **kwargs)
   1891 if not self._is_idle_drawing:
   1892     with self._idle_draw_cntx():
-> 1893         self.draw(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:382, in FigureCanvasAgg.draw(self)
    379 # Acquire a lock on the shared font cache.
    380 with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
    381       else nullcontext()):
--> 382     self.figure.draw(self.renderer)
    383     # A GUI class may be need to update a window using this draw, so
    384     # don't forget to call the superclass.
    385     super().draw()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:405, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    403 cbp_rspan, cbp_cspan = get_cb_parent_spans(cbax)
    404 loc = cbax._colorbar_info['location']
--> 405 cbpos, cbbbox = get_pos_and_bbox(cbax, renderer)
    406 if loc == 'right':
    407     if cbp_cspan.stop == ss.colspan.stop:
    408         # only increase if the colorbar is on the right edge

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4564, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4562 for axis in self._axis_map.values():
   4563     if self.axison and axis.get_visible():
-> 4564         ba = martist._get_tightbbox_for_layout_only(axis, renderer)
   4565         if ba:
   4566             bb.append(ba)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1353, in Axis.get_tightbbox(self, renderer, for_layout_only)
   1350     renderer = self.get_figure(root=True)._get_renderer()
   1351 ticks_to_draw = self._update_ticks()
-> 1353 self._update_label_position(renderer)
   1355 # go back to just this axis's tick labels
   1356 tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2675, in YAxis._update_label_position(self, renderer)
   2671     return
   2673 # get bounding boxes for this axis and any siblings
   2674 # that have been set by `fig.align_ylabels()`
-> 2675 bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
   2676 x, y = self.label.get_position()
   2678 if self.label_position == 'left':
   2679     # Union with extents of the left spine if present, of the axes otherwise.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2241, in Axis._get_tick_boxes_siblings(self, renderer)
   2239 axis = ax._axis_map[name]
   2240 ticks_to_draw = axis._update_ticks()
-> 2241 tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer)
   2242 bboxes.extend(tlb)
   2243 bboxes2.extend(tlb2)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1334, in Axis._get_ticklabel_bboxes(self, ticks, renderer)
   1330 if renderer is None:
   1331     renderer = self.get_figure(root=True)._get_renderer()
   1332 return ([tick.label1.get_window_extent(renderer)
   1333          for tick in ticks if tick.label1.get_visible()],
-> 1334         [tick.label2.get_window_extent(renderer)
   1335          for tick in ticks if tick.label2.get_visible()])

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
    964     raise RuntimeError(
    965         "Cannot get window extent of text w/o renderer. You likely "
    966         "want to call 'figure.draw_without_rendering()' first.")
    968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969     bbox, info, descent = self._get_layout(self._renderer)
    970     x, y = self.get_unitless_position()
    971     x, y = self.get_transform().transform((x, y))

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
    370 ys = []
    372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
    374     renderer, "lp", self._fontproperties,
    375     ismath="TeX" if self.get_usetex() else False,
    376     dpi=self.get_figure(root=True).dpi)
    377 min_dy = (lp_h - lp_d) * self._linespacing
    379 for i, line in enumerate(lines):

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:367, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
--> 367     page, = dvi
    368 # A total height (including the descent) needs to be returned.
    369 return page.width, page.height + page.descent, page.descent

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:261, in Dvi.__iter__(self)
    245 def __iter__(self):
    246     """
    247     Iterate through the pages of the file.
    248 
   (...)    259         integers.
    260     """
--> 261     while self._read():
    262         yield self._output()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:343, in Dvi._read(self)
    341 self._dtable[byte](self, byte)
    342 if self._missing_font:
--> 343     raise self._missing_font.to_exception()
    344 name = self._dtable[byte].__name__
    345 if name == "_push":

FileNotFoundError: Matplotlib's TeX implementation searched for a file named 'cmss10.tfm' in your texmf tree, but could not find it
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/formatters.py:402, in BaseFormatter.__call__(self, obj)
    400     pass
    401 else:
--> 402     return printer(obj)
    403 # Finally look for special method names
    404 method = get_real_method(obj, self.print_method)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:187, in retina_figure(fig, base64, **kwargs)
    178 def retina_figure(fig, base64=False, **kwargs):
    179     """format a figure as a pixel-doubled (retina) PNG
    180 
    181     If `base64` is True, return base64-encoded str instead of raw bytes
   (...)    185         base64 argument
    186     """
--> 187     pngdata = print_figure(fig, fmt="retina", base64=False, **kwargs)
    188     # Make sure that retina_figure acts just like print_figure and returns
    189     # None when the figure is empty.
    190     if pngdata is None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:170, in print_figure(fig, fmt, bbox_inches, base64, **kwargs)
    167     from matplotlib.backend_bases import FigureCanvasBase
    168     FigureCanvasBase(fig)
--> 170 fig.canvas.print_figure(bytes_io, **kw)
    171 data = bytes_io.getvalue()
    172 if fmt == 'svg':

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:2157, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2154     # we do this instead of `self.figure.draw_without_rendering`
   2155     # so that we can inject the orientation
   2156     with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2157         self.figure.draw(renderer)
   2158 if bbox_inches:
   2159     if bbox_inches == "tight":

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:405, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    403 cbp_rspan, cbp_cspan = get_cb_parent_spans(cbax)
    404 loc = cbax._colorbar_info['location']
--> 405 cbpos, cbbbox = get_pos_and_bbox(cbax, renderer)
    406 if loc == 'right':
    407     if cbp_cspan.stop == ss.colspan.stop:
    408         # only increase if the colorbar is on the right edge

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4564, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4562 for axis in self._axis_map.values():
   4563     if self.axison and axis.get_visible():
-> 4564         ba = martist._get_tightbbox_for_layout_only(axis, renderer)
   4565         if ba:
   4566             bb.append(ba)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1353, in Axis.get_tightbbox(self, renderer, for_layout_only)
   1350     renderer = self.get_figure(root=True)._get_renderer()
   1351 ticks_to_draw = self._update_ticks()
-> 1353 self._update_label_position(renderer)
   1355 # go back to just this axis's tick labels
   1356 tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2675, in YAxis._update_label_position(self, renderer)
   2671     return
   2673 # get bounding boxes for this axis and any siblings
   2674 # that have been set by `fig.align_ylabels()`
-> 2675 bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
   2676 x, y = self.label.get_position()
   2678 if self.label_position == 'left':
   2679     # Union with extents of the left spine if present, of the axes otherwise.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2241, in Axis._get_tick_boxes_siblings(self, renderer)
   2239 axis = ax._axis_map[name]
   2240 ticks_to_draw = axis._update_ticks()
-> 2241 tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer)
   2242 bboxes.extend(tlb)
   2243 bboxes2.extend(tlb2)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1334, in Axis._get_ticklabel_bboxes(self, ticks, renderer)
   1330 if renderer is None:
   1331     renderer = self.get_figure(root=True)._get_renderer()
   1332 return ([tick.label1.get_window_extent(renderer)
   1333          for tick in ticks if tick.label1.get_visible()],
-> 1334         [tick.label2.get_window_extent(renderer)
   1335          for tick in ticks if tick.label2.get_visible()])

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
    964     raise RuntimeError(
    965         "Cannot get window extent of text w/o renderer. You likely "
    966         "want to call 'figure.draw_without_rendering()' first.")
    968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969     bbox, info, descent = self._get_layout(self._renderer)
    970     x, y = self.get_unitless_position()
    971     x, y = self.get_transform().transform((x, y))

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
    370 ys = []
    372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
    374     renderer, "lp", self._fontproperties,
    375     ismath="TeX" if self.get_usetex() else False,
    376     dpi=self.get_figure(root=True).dpi)
    377 min_dy = (lp_h - lp_d) * self._linespacing
    379 for i, line in enumerate(lines):

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:367, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
--> 367     page, = dvi
    368 # A total height (including the descent) needs to be returned.
    369 return page.width, page.height + page.descent, page.descent

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:261, in Dvi.__iter__(self)
    245 def __iter__(self):
    246     """
    247     Iterate through the pages of the file.
    248 
   (...)    259         integers.
    260     """
--> 261     while self._read():
    262         yield self._output()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:343, in Dvi._read(self)
    341 self._dtable[byte](self, byte)
    342 if self._missing_font:
--> 343     raise self._missing_font.to_exception()
    344 name = self._dtable[byte].__name__
    345 if name == "_push":

FileNotFoundError: Matplotlib's TeX implementation searched for a file named 'cmss10.tfm' in your texmf tree, but could not find it
<Figure size 350x350 with 2 Axes>

Figure 1. Gaussian kernel with \(A=2\) and \(\sigma=0.15\) evaluated on a \(20\times20\) cell planar non-periodic MPAS mesh.

Now, let’s contour the 1.0 level. Like “Marching Squares” algorithm, we’ll start by creating a boolean mask of cells above and below the contour level.

mask = gauss < 1.0
Error in callback <function _draw_all_if_interactive at 0x7f4811a4ba00> (for post_execute), with arguments args (),kwargs {}:
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:250, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
    249 try:
--> 250     report = subprocess.check_output(
    251         command, cwd=cwd if cwd is not None else cls._texcache,
    252         stderr=subprocess.STDOUT)
    253 except FileNotFoundError as exc:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/subprocess.py:472, in check_output(timeout, *popenargs, **kwargs)
    470     kwargs['input'] = empty
--> 472 return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
    473            **kwargs).stdout

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/subprocess.py:554, in run(input, capture_output, timeout, check, *popenargs, **kwargs)
    552     kwargs['stderr'] = PIPE
--> 554 with Popen(*popenargs, **kwargs) as process:
    555     try:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/subprocess.py:1038, in Popen.__init__(self, args, bufsize, executable, stdin, stdout, stderr, preexec_fn, close_fds, shell, cwd, env, universal_newlines, startupinfo, creationflags, restore_signals, start_new_session, pass_fds, user, group, extra_groups, encoding, errors, text, umask, pipesize, process_group)
   1035             self.stderr = io.TextIOWrapper(self.stderr,
   1036                     encoding=encoding, errors=errors)
-> 1038     self._execute_child(args, executable, preexec_fn, close_fds,
   1039                         pass_fds, cwd, env,
   1040                         startupinfo, creationflags, shell,
   1041                         p2cread, p2cwrite,
   1042                         c2pread, c2pwrite,
   1043                         errread, errwrite,
   1044                         restore_signals,
   1045                         gid, gids, uid, umask,
   1046                         start_new_session, process_group)
   1047 except:
   1048     # Cleanup if the child failed starting.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/subprocess.py:1989, in Popen._execute_child(self, args, executable, preexec_fn, close_fds, pass_fds, cwd, env, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite, restore_signals, gid, gids, uid, umask, start_new_session, process_group)
   1988 if err_filename is not None:
-> 1989     raise child_exception_type(errno_num, err_msg, err_filename)
   1990 else:

FileNotFoundError: [Errno 2] No such file or directory: 'latex'

The above exception was the direct cause of the following exception:

RuntimeError                              Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/pyplot.py:278, in _draw_all_if_interactive()
    276 def _draw_all_if_interactive() -> None:
    277     if matplotlib.is_interactive():
--> 278         draw_all()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_pylab_helpers.py:131, in Gcf.draw_all(cls, force)
    129 for manager in cls.get_all_fig_managers():
    130     if force or manager.canvas.figure.stale:
--> 131         manager.canvas.draw_idle()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:1893, in FigureCanvasBase.draw_idle(self, *args, **kwargs)
   1891 if not self._is_idle_drawing:
   1892     with self._idle_draw_cntx():
-> 1893         self.draw(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:382, in FigureCanvasAgg.draw(self)
    379 # Acquire a lock on the shared font cache.
    380 with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
    381       else nullcontext()):
--> 382     self.figure.draw(self.renderer)
    383     # A GUI class may be need to update a window using this draw, so
    384     # don't forget to call the superclass.
    385     super().draw()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:388, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    384     return
    386 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
    387                                  hspace=hspace, wspace=wspace)
--> 388 pos, bbox = get_pos_and_bbox(ax, renderer)
    389 # the margin is the distance between the bounding box of the Axes
    390 # and its position (plus the padding from above)
    391 margin['left'] += pos.x0 - bbox.x0

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4587, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4584     bbox_artists = self.get_default_bbox_extra_artists()
   4586 for a in bbox_artists:
-> 4587     bbox = a.get_tightbbox(renderer)
   4588     if (bbox is not None
   4589             and 0 < bbox.width < np.inf
   4590             and 0 < bbox.height < np.inf):
   4591         bb.append(bbox)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/legend.py:1060, in Legend.get_tightbbox(self, renderer)
   1058 def get_tightbbox(self, renderer=None):
   1059     # docstring inherited
-> 1060     return self._legend_box.get_window_extent(renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:367, in OffsetBox.get_window_extent(self, renderer)
    365 if renderer is None:
    366     renderer = self.get_figure(root=True)._get_renderer()
--> 367 bbox = self.get_bbox(renderer)
    368 try:  # Some subclasses redefine get_offset to take no args.
    369     px, py = self.get_offset(bbox, renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:460, in VPacker._get_bbox_and_child_offsets(self, renderer)
    457         if isinstance(c, PackerBase) and c.mode == "expand":
    458             c.set_width(self.width)
--> 460 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    461 (x0, x1), xoffsets = _get_aligned_offsets(
    462     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    463 height, yoffsets = _get_packed_offsets(
    464     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:493, in HPacker._get_bbox_and_child_offsets(self, renderer)
    490 pad = self.pad * dpicor
    491 sep = self.sep * dpicor
--> 493 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    494 if not bboxes:
    495     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:460, in VPacker._get_bbox_and_child_offsets(self, renderer)
    457         if isinstance(c, PackerBase) and c.mode == "expand":
    458             c.set_width(self.width)
--> 460 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    461 (x0, x1), xoffsets = _get_aligned_offsets(
    462     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    463 height, yoffsets = _get_packed_offsets(
    464     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:493, in HPacker._get_bbox_and_child_offsets(self, renderer)
    490 pad = self.pad * dpicor
    491 sep = self.sep * dpicor
--> 493 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    494 if not bboxes:
    495     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:797, in TextArea.get_bbox(self, renderer)
    796 def get_bbox(self, renderer):
--> 797     _, h_, d_ = mtext._get_text_metrics_with_cache(
    798         renderer, "lp", self._text._fontproperties,
    799         ismath="TeX" if self._text.get_usetex() else False,
    800         dpi=self.get_figure(root=True).dpi)
    802     bbox, info, yd = self._text._get_layout(renderer)
    803     w, h = bbox.size

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:364, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    362 if tex.strip() == '':
    363     return 0, 0, 0
--> 364 dvifile = cls.make_dvi(tex, fontsize)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:293, in TexManager.make_dvi(cls, tex, fontsize)
    290 with TemporaryDirectory(dir=dvifile.parent) as tmpdir:
    291     Path(tmpdir, "file.tex").write_text(
    292         cls._get_tex_source(tex, fontsize), encoding='utf-8')
--> 293     cls._run_checked_subprocess(
    294         ["latex", "-interaction=nonstopmode", "--halt-on-error",
    295          "file.tex"], tex, cwd=tmpdir)
    296     Path(tmpdir, "file.dvi").replace(dvifile)
    297     # Also move the tex source to the main cache directory, but
    298     # only for backcompat.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:254, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
    250     report = subprocess.check_output(
    251         command, cwd=cwd if cwd is not None else cls._texcache,
    252         stderr=subprocess.STDOUT)
    253 except FileNotFoundError as exc:
--> 254     raise RuntimeError(
    255         f'Failed to process string with tex because {command[0]} '
    256         'could not be found') from exc
    257 except subprocess.CalledProcessError as exc:
    258     raise RuntimeError(
    259         '{prog} was not able to process the following string:\n'
    260         '{tex!r}\n\n'
   (...)    267             exc=exc.output.decode('utf-8', 'backslashreplace'))
    268         ) from None

RuntimeError: Failed to process string with tex because latex could not be found
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:250, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
    249 try:
--> 250     report = subprocess.check_output(
    251         command, cwd=cwd if cwd is not None else cls._texcache,
    252         stderr=subprocess.STDOUT)
    253 except FileNotFoundError as exc:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/subprocess.py:472, in check_output(timeout, *popenargs, **kwargs)
    470     kwargs['input'] = empty
--> 472 return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
    473            **kwargs).stdout

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/subprocess.py:554, in run(input, capture_output, timeout, check, *popenargs, **kwargs)
    552     kwargs['stderr'] = PIPE
--> 554 with Popen(*popenargs, **kwargs) as process:
    555     try:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/subprocess.py:1038, in Popen.__init__(self, args, bufsize, executable, stdin, stdout, stderr, preexec_fn, close_fds, shell, cwd, env, universal_newlines, startupinfo, creationflags, restore_signals, start_new_session, pass_fds, user, group, extra_groups, encoding, errors, text, umask, pipesize, process_group)
   1035             self.stderr = io.TextIOWrapper(self.stderr,
   1036                     encoding=encoding, errors=errors)
-> 1038     self._execute_child(args, executable, preexec_fn, close_fds,
   1039                         pass_fds, cwd, env,
   1040                         startupinfo, creationflags, shell,
   1041                         p2cread, p2cwrite,
   1042                         c2pread, c2pwrite,
   1043                         errread, errwrite,
   1044                         restore_signals,
   1045                         gid, gids, uid, umask,
   1046                         start_new_session, process_group)
   1047 except:
   1048     # Cleanup if the child failed starting.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/subprocess.py:1989, in Popen._execute_child(self, args, executable, preexec_fn, close_fds, pass_fds, cwd, env, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite, restore_signals, gid, gids, uid, umask, start_new_session, process_group)
   1988 if err_filename is not None:
-> 1989     raise child_exception_type(errno_num, err_msg, err_filename)
   1990 else:

FileNotFoundError: [Errno 2] No such file or directory: 'latex'

The above exception was the direct cause of the following exception:

RuntimeError                              Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/formatters.py:402, in BaseFormatter.__call__(self, obj)
    400     pass
    401 else:
--> 402     return printer(obj)
    403 # Finally look for special method names
    404 method = get_real_method(obj, self.print_method)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:187, in retina_figure(fig, base64, **kwargs)
    178 def retina_figure(fig, base64=False, **kwargs):
    179     """format a figure as a pixel-doubled (retina) PNG
    180 
    181     If `base64` is True, return base64-encoded str instead of raw bytes
   (...)    185         base64 argument
    186     """
--> 187     pngdata = print_figure(fig, fmt="retina", base64=False, **kwargs)
    188     # Make sure that retina_figure acts just like print_figure and returns
    189     # None when the figure is empty.
    190     if pngdata is None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:170, in print_figure(fig, fmt, bbox_inches, base64, **kwargs)
    167     from matplotlib.backend_bases import FigureCanvasBase
    168     FigureCanvasBase(fig)
--> 170 fig.canvas.print_figure(bytes_io, **kw)
    171 data = bytes_io.getvalue()
    172 if fmt == 'svg':

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:2157, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2154     # we do this instead of `self.figure.draw_without_rendering`
   2155     # so that we can inject the orientation
   2156     with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2157         self.figure.draw(renderer)
   2158 if bbox_inches:
   2159     if bbox_inches == "tight":

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:388, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    384     return
    386 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
    387                                  hspace=hspace, wspace=wspace)
--> 388 pos, bbox = get_pos_and_bbox(ax, renderer)
    389 # the margin is the distance between the bounding box of the Axes
    390 # and its position (plus the padding from above)
    391 margin['left'] += pos.x0 - bbox.x0

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4587, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4584     bbox_artists = self.get_default_bbox_extra_artists()
   4586 for a in bbox_artists:
-> 4587     bbox = a.get_tightbbox(renderer)
   4588     if (bbox is not None
   4589             and 0 < bbox.width < np.inf
   4590             and 0 < bbox.height < np.inf):
   4591         bb.append(bbox)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/legend.py:1060, in Legend.get_tightbbox(self, renderer)
   1058 def get_tightbbox(self, renderer=None):
   1059     # docstring inherited
-> 1060     return self._legend_box.get_window_extent(renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:367, in OffsetBox.get_window_extent(self, renderer)
    365 if renderer is None:
    366     renderer = self.get_figure(root=True)._get_renderer()
--> 367 bbox = self.get_bbox(renderer)
    368 try:  # Some subclasses redefine get_offset to take no args.
    369     px, py = self.get_offset(bbox, renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:460, in VPacker._get_bbox_and_child_offsets(self, renderer)
    457         if isinstance(c, PackerBase) and c.mode == "expand":
    458             c.set_width(self.width)
--> 460 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    461 (x0, x1), xoffsets = _get_aligned_offsets(
    462     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    463 height, yoffsets = _get_packed_offsets(
    464     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:493, in HPacker._get_bbox_and_child_offsets(self, renderer)
    490 pad = self.pad * dpicor
    491 sep = self.sep * dpicor
--> 493 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    494 if not bboxes:
    495     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:460, in VPacker._get_bbox_and_child_offsets(self, renderer)
    457         if isinstance(c, PackerBase) and c.mode == "expand":
    458             c.set_width(self.width)
--> 460 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    461 (x0, x1), xoffsets = _get_aligned_offsets(
    462     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    463 height, yoffsets = _get_packed_offsets(
    464     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:493, in HPacker._get_bbox_and_child_offsets(self, renderer)
    490 pad = self.pad * dpicor
    491 sep = self.sep * dpicor
--> 493 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    494 if not bboxes:
    495     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:797, in TextArea.get_bbox(self, renderer)
    796 def get_bbox(self, renderer):
--> 797     _, h_, d_ = mtext._get_text_metrics_with_cache(
    798         renderer, "lp", self._text._fontproperties,
    799         ismath="TeX" if self._text.get_usetex() else False,
    800         dpi=self.get_figure(root=True).dpi)
    802     bbox, info, yd = self._text._get_layout(renderer)
    803     w, h = bbox.size

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:364, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    362 if tex.strip() == '':
    363     return 0, 0, 0
--> 364 dvifile = cls.make_dvi(tex, fontsize)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:293, in TexManager.make_dvi(cls, tex, fontsize)
    290 with TemporaryDirectory(dir=dvifile.parent) as tmpdir:
    291     Path(tmpdir, "file.tex").write_text(
    292         cls._get_tex_source(tex, fontsize), encoding='utf-8')
--> 293     cls._run_checked_subprocess(
    294         ["latex", "-interaction=nonstopmode", "--halt-on-error",
    295          "file.tex"], tex, cwd=tmpdir)
    296     Path(tmpdir, "file.dvi").replace(dvifile)
    297     # Also move the tex source to the main cache directory, but
    298     # only for backcompat.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:254, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
    250     report = subprocess.check_output(
    251         command, cwd=cwd if cwd is not None else cls._texcache,
    252         stderr=subprocess.STDOUT)
    253 except FileNotFoundError as exc:
--> 254     raise RuntimeError(
    255         f'Failed to process string with tex because {command[0]} '
    256         'could not be found') from exc
    257 except subprocess.CalledProcessError as exc:
    258     raise RuntimeError(
    259         '{prog} was not able to process the following string:\n'
    260         '{tex!r}\n\n'
   (...)    267             exc=exc.output.decode('utf-8', 'backslashreplace'))
    268         ) from None

RuntimeError: Failed to process string with tex because latex could not be found
<Figure size 350x350 with 1 Axes>

Figure 2. Boolean mask of cells with a value greater than \(1\) for the field plotted above (Figure 1). Gray cells are above (True) and white cells are below (False) the contour level.

From examining the boolean mask, plotted on the native mesh, it becomes clear that one way to display contour boundary is to use the edges of the mesh where the mask is true on one side and false on the other. To identify the edges where this is true, we’ll use the “exculusive or” (i.e. \(\mathrm{XOR}\)) operation along the second dimension of the \(\mathrm{cellsOnEdge}\) array, which is of size \(\mathrm{nEdges} \times 2\).

Error in callback <function _draw_all_if_interactive at 0x7f4811a4ba00> (for post_execute), with arguments args (),kwargs {}:
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:250, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
    249 try:
--> 250     report = subprocess.check_output(
    251         command, cwd=cwd if cwd is not None else cls._texcache,
    252         stderr=subprocess.STDOUT)
    253 except FileNotFoundError as exc:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/subprocess.py:472, in check_output(timeout, *popenargs, **kwargs)
    470     kwargs['input'] = empty
--> 472 return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
    473            **kwargs).stdout

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/subprocess.py:554, in run(input, capture_output, timeout, check, *popenargs, **kwargs)
    552     kwargs['stderr'] = PIPE
--> 554 with Popen(*popenargs, **kwargs) as process:
    555     try:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/subprocess.py:1038, in Popen.__init__(self, args, bufsize, executable, stdin, stdout, stderr, preexec_fn, close_fds, shell, cwd, env, universal_newlines, startupinfo, creationflags, restore_signals, start_new_session, pass_fds, user, group, extra_groups, encoding, errors, text, umask, pipesize, process_group)
   1035             self.stderr = io.TextIOWrapper(self.stderr,
   1036                     encoding=encoding, errors=errors)
-> 1038     self._execute_child(args, executable, preexec_fn, close_fds,
   1039                         pass_fds, cwd, env,
   1040                         startupinfo, creationflags, shell,
   1041                         p2cread, p2cwrite,
   1042                         c2pread, c2pwrite,
   1043                         errread, errwrite,
   1044                         restore_signals,
   1045                         gid, gids, uid, umask,
   1046                         start_new_session, process_group)
   1047 except:
   1048     # Cleanup if the child failed starting.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/subprocess.py:1989, in Popen._execute_child(self, args, executable, preexec_fn, close_fds, pass_fds, cwd, env, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite, restore_signals, gid, gids, uid, umask, start_new_session, process_group)
   1988 if err_filename is not None:
-> 1989     raise child_exception_type(errno_num, err_msg, err_filename)
   1990 else:

FileNotFoundError: [Errno 2] No such file or directory: 'latex'

The above exception was the direct cause of the following exception:

RuntimeError                              Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/pyplot.py:278, in _draw_all_if_interactive()
    276 def _draw_all_if_interactive() -> None:
    277     if matplotlib.is_interactive():
--> 278         draw_all()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_pylab_helpers.py:131, in Gcf.draw_all(cls, force)
    129 for manager in cls.get_all_fig_managers():
    130     if force or manager.canvas.figure.stale:
--> 131         manager.canvas.draw_idle()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:1893, in FigureCanvasBase.draw_idle(self, *args, **kwargs)
   1891 if not self._is_idle_drawing:
   1892     with self._idle_draw_cntx():
-> 1893         self.draw(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:382, in FigureCanvasAgg.draw(self)
    379 # Acquire a lock on the shared font cache.
    380 with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
    381       else nullcontext()):
--> 382     self.figure.draw(self.renderer)
    383     # A GUI class may be need to update a window using this draw, so
    384     # don't forget to call the superclass.
    385     super().draw()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:388, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    384     return
    386 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
    387                                  hspace=hspace, wspace=wspace)
--> 388 pos, bbox = get_pos_and_bbox(ax, renderer)
    389 # the margin is the distance between the bounding box of the Axes
    390 # and its position (plus the padding from above)
    391 margin['left'] += pos.x0 - bbox.x0

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4587, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4584     bbox_artists = self.get_default_bbox_extra_artists()
   4586 for a in bbox_artists:
-> 4587     bbox = a.get_tightbbox(renderer)
   4588     if (bbox is not None
   4589             and 0 < bbox.width < np.inf
   4590             and 0 < bbox.height < np.inf):
   4591         bb.append(bbox)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/legend.py:1060, in Legend.get_tightbbox(self, renderer)
   1058 def get_tightbbox(self, renderer=None):
   1059     # docstring inherited
-> 1060     return self._legend_box.get_window_extent(renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:367, in OffsetBox.get_window_extent(self, renderer)
    365 if renderer is None:
    366     renderer = self.get_figure(root=True)._get_renderer()
--> 367 bbox = self.get_bbox(renderer)
    368 try:  # Some subclasses redefine get_offset to take no args.
    369     px, py = self.get_offset(bbox, renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:460, in VPacker._get_bbox_and_child_offsets(self, renderer)
    457         if isinstance(c, PackerBase) and c.mode == "expand":
    458             c.set_width(self.width)
--> 460 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    461 (x0, x1), xoffsets = _get_aligned_offsets(
    462     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    463 height, yoffsets = _get_packed_offsets(
    464     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:493, in HPacker._get_bbox_and_child_offsets(self, renderer)
    490 pad = self.pad * dpicor
    491 sep = self.sep * dpicor
--> 493 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    494 if not bboxes:
    495     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:460, in VPacker._get_bbox_and_child_offsets(self, renderer)
    457         if isinstance(c, PackerBase) and c.mode == "expand":
    458             c.set_width(self.width)
--> 460 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    461 (x0, x1), xoffsets = _get_aligned_offsets(
    462     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    463 height, yoffsets = _get_packed_offsets(
    464     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:493, in HPacker._get_bbox_and_child_offsets(self, renderer)
    490 pad = self.pad * dpicor
    491 sep = self.sep * dpicor
--> 493 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    494 if not bboxes:
    495     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:797, in TextArea.get_bbox(self, renderer)
    796 def get_bbox(self, renderer):
--> 797     _, h_, d_ = mtext._get_text_metrics_with_cache(
    798         renderer, "lp", self._text._fontproperties,
    799         ismath="TeX" if self._text.get_usetex() else False,
    800         dpi=self.get_figure(root=True).dpi)
    802     bbox, info, yd = self._text._get_layout(renderer)
    803     w, h = bbox.size

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:364, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    362 if tex.strip() == '':
    363     return 0, 0, 0
--> 364 dvifile = cls.make_dvi(tex, fontsize)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:293, in TexManager.make_dvi(cls, tex, fontsize)
    290 with TemporaryDirectory(dir=dvifile.parent) as tmpdir:
    291     Path(tmpdir, "file.tex").write_text(
    292         cls._get_tex_source(tex, fontsize), encoding='utf-8')
--> 293     cls._run_checked_subprocess(
    294         ["latex", "-interaction=nonstopmode", "--halt-on-error",
    295          "file.tex"], tex, cwd=tmpdir)
    296     Path(tmpdir, "file.dvi").replace(dvifile)
    297     # Also move the tex source to the main cache directory, but
    298     # only for backcompat.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:254, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
    250     report = subprocess.check_output(
    251         command, cwd=cwd if cwd is not None else cls._texcache,
    252         stderr=subprocess.STDOUT)
    253 except FileNotFoundError as exc:
--> 254     raise RuntimeError(
    255         f'Failed to process string with tex because {command[0]} '
    256         'could not be found') from exc
    257 except subprocess.CalledProcessError as exc:
    258     raise RuntimeError(
    259         '{prog} was not able to process the following string:\n'
    260         '{tex!r}\n\n'
   (...)    267             exc=exc.output.decode('utf-8', 'backslashreplace'))
    268         ) from None

RuntimeError: Failed to process string with tex because latex could not be found
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:250, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
    249 try:
--> 250     report = subprocess.check_output(
    251         command, cwd=cwd if cwd is not None else cls._texcache,
    252         stderr=subprocess.STDOUT)
    253 except FileNotFoundError as exc:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/subprocess.py:472, in check_output(timeout, *popenargs, **kwargs)
    470     kwargs['input'] = empty
--> 472 return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
    473            **kwargs).stdout

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/subprocess.py:554, in run(input, capture_output, timeout, check, *popenargs, **kwargs)
    552     kwargs['stderr'] = PIPE
--> 554 with Popen(*popenargs, **kwargs) as process:
    555     try:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/subprocess.py:1038, in Popen.__init__(self, args, bufsize, executable, stdin, stdout, stderr, preexec_fn, close_fds, shell, cwd, env, universal_newlines, startupinfo, creationflags, restore_signals, start_new_session, pass_fds, user, group, extra_groups, encoding, errors, text, umask, pipesize, process_group)
   1035             self.stderr = io.TextIOWrapper(self.stderr,
   1036                     encoding=encoding, errors=errors)
-> 1038     self._execute_child(args, executable, preexec_fn, close_fds,
   1039                         pass_fds, cwd, env,
   1040                         startupinfo, creationflags, shell,
   1041                         p2cread, p2cwrite,
   1042                         c2pread, c2pwrite,
   1043                         errread, errwrite,
   1044                         restore_signals,
   1045                         gid, gids, uid, umask,
   1046                         start_new_session, process_group)
   1047 except:
   1048     # Cleanup if the child failed starting.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/subprocess.py:1989, in Popen._execute_child(self, args, executable, preexec_fn, close_fds, pass_fds, cwd, env, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite, restore_signals, gid, gids, uid, umask, start_new_session, process_group)
   1988 if err_filename is not None:
-> 1989     raise child_exception_type(errno_num, err_msg, err_filename)
   1990 else:

FileNotFoundError: [Errno 2] No such file or directory: 'latex'

The above exception was the direct cause of the following exception:

RuntimeError                              Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/formatters.py:402, in BaseFormatter.__call__(self, obj)
    400     pass
    401 else:
--> 402     return printer(obj)
    403 # Finally look for special method names
    404 method = get_real_method(obj, self.print_method)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:187, in retina_figure(fig, base64, **kwargs)
    178 def retina_figure(fig, base64=False, **kwargs):
    179     """format a figure as a pixel-doubled (retina) PNG
    180 
    181     If `base64` is True, return base64-encoded str instead of raw bytes
   (...)    185         base64 argument
    186     """
--> 187     pngdata = print_figure(fig, fmt="retina", base64=False, **kwargs)
    188     # Make sure that retina_figure acts just like print_figure and returns
    189     # None when the figure is empty.
    190     if pngdata is None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:170, in print_figure(fig, fmt, bbox_inches, base64, **kwargs)
    167     from matplotlib.backend_bases import FigureCanvasBase
    168     FigureCanvasBase(fig)
--> 170 fig.canvas.print_figure(bytes_io, **kw)
    171 data = bytes_io.getvalue()
    172 if fmt == 'svg':

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:2157, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2154     # we do this instead of `self.figure.draw_without_rendering`
   2155     # so that we can inject the orientation
   2156     with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2157         self.figure.draw(renderer)
   2158 if bbox_inches:
   2159     if bbox_inches == "tight":

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:388, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    384     return
    386 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
    387                                  hspace=hspace, wspace=wspace)
--> 388 pos, bbox = get_pos_and_bbox(ax, renderer)
    389 # the margin is the distance between the bounding box of the Axes
    390 # and its position (plus the padding from above)
    391 margin['left'] += pos.x0 - bbox.x0

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4587, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4584     bbox_artists = self.get_default_bbox_extra_artists()
   4586 for a in bbox_artists:
-> 4587     bbox = a.get_tightbbox(renderer)
   4588     if (bbox is not None
   4589             and 0 < bbox.width < np.inf
   4590             and 0 < bbox.height < np.inf):
   4591         bb.append(bbox)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/legend.py:1060, in Legend.get_tightbbox(self, renderer)
   1058 def get_tightbbox(self, renderer=None):
   1059     # docstring inherited
-> 1060     return self._legend_box.get_window_extent(renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:367, in OffsetBox.get_window_extent(self, renderer)
    365 if renderer is None:
    366     renderer = self.get_figure(root=True)._get_renderer()
--> 367 bbox = self.get_bbox(renderer)
    368 try:  # Some subclasses redefine get_offset to take no args.
    369     px, py = self.get_offset(bbox, renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:460, in VPacker._get_bbox_and_child_offsets(self, renderer)
    457         if isinstance(c, PackerBase) and c.mode == "expand":
    458             c.set_width(self.width)
--> 460 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    461 (x0, x1), xoffsets = _get_aligned_offsets(
    462     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    463 height, yoffsets = _get_packed_offsets(
    464     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:493, in HPacker._get_bbox_and_child_offsets(self, renderer)
    490 pad = self.pad * dpicor
    491 sep = self.sep * dpicor
--> 493 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    494 if not bboxes:
    495     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:460, in VPacker._get_bbox_and_child_offsets(self, renderer)
    457         if isinstance(c, PackerBase) and c.mode == "expand":
    458             c.set_width(self.width)
--> 460 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    461 (x0, x1), xoffsets = _get_aligned_offsets(
    462     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    463 height, yoffsets = _get_packed_offsets(
    464     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:493, in HPacker._get_bbox_and_child_offsets(self, renderer)
    490 pad = self.pad * dpicor
    491 sep = self.sep * dpicor
--> 493 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    494 if not bboxes:
    495     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:797, in TextArea.get_bbox(self, renderer)
    796 def get_bbox(self, renderer):
--> 797     _, h_, d_ = mtext._get_text_metrics_with_cache(
    798         renderer, "lp", self._text._fontproperties,
    799         ismath="TeX" if self._text.get_usetex() else False,
    800         dpi=self.get_figure(root=True).dpi)
    802     bbox, info, yd = self._text._get_layout(renderer)
    803     w, h = bbox.size

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:364, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    362 if tex.strip() == '':
    363     return 0, 0, 0
--> 364 dvifile = cls.make_dvi(tex, fontsize)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:293, in TexManager.make_dvi(cls, tex, fontsize)
    290 with TemporaryDirectory(dir=dvifile.parent) as tmpdir:
    291     Path(tmpdir, "file.tex").write_text(
    292         cls._get_tex_source(tex, fontsize), encoding='utf-8')
--> 293     cls._run_checked_subprocess(
    294         ["latex", "-interaction=nonstopmode", "--halt-on-error",
    295          "file.tex"], tex, cwd=tmpdir)
    296     Path(tmpdir, "file.dvi").replace(dvifile)
    297     # Also move the tex source to the main cache directory, but
    298     # only for backcompat.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:254, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
    250     report = subprocess.check_output(
    251         command, cwd=cwd if cwd is not None else cls._texcache,
    252         stderr=subprocess.STDOUT)
    253 except FileNotFoundError as exc:
--> 254     raise RuntimeError(
    255         f'Failed to process string with tex because {command[0]} '
    256         'could not be found') from exc
    257 except subprocess.CalledProcessError as exc:
    258     raise RuntimeError(
    259         '{prog} was not able to process the following string:\n'
    260         '{tex!r}\n\n'
   (...)    267             exc=exc.output.decode('utf-8', 'backslashreplace'))
    268         ) from None

RuntimeError: Failed to process string with tex because latex could not be found
<Figure size 350x350 with 1 Axes>

Figure 3. Boolean mask of cells with a value greater than \(1\), with edges corresponding to the contour boundary denoted in blue.

Unfortunately it’s not quite that simple. While we’ve identified the edges that correspond to the contour boundary, the order of those edges are quite important for plotting.

Error in callback <function _draw_all_if_interactive at 0x7f4811a4ba00> (for post_execute), with arguments args (),kwargs {}:
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/pyplot.py:278, in _draw_all_if_interactive()
    276 def _draw_all_if_interactive() -> None:
    277     if matplotlib.is_interactive():
--> 278         draw_all()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_pylab_helpers.py:131, in Gcf.draw_all(cls, force)
    129 for manager in cls.get_all_fig_managers():
    130     if force or manager.canvas.figure.stale:
--> 131         manager.canvas.draw_idle()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:1893, in FigureCanvasBase.draw_idle(self, *args, **kwargs)
   1891 if not self._is_idle_drawing:
   1892     with self._idle_draw_cntx():
-> 1893         self.draw(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:382, in FigureCanvasAgg.draw(self)
    379 # Acquire a lock on the shared font cache.
    380 with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
    381       else nullcontext()):
--> 382     self.figure.draw(self.renderer)
    383     # A GUI class may be need to update a window using this draw, so
    384     # don't forget to call the superclass.
    385     super().draw()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:388, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    384     return
    386 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
    387                                  hspace=hspace, wspace=wspace)
--> 388 pos, bbox = get_pos_and_bbox(ax, renderer)
    389 # the margin is the distance between the bounding box of the Axes
    390 # and its position (plus the padding from above)
    391 margin['left'] += pos.x0 - bbox.x0

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4587, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4584     bbox_artists = self.get_default_bbox_extra_artists()
   4586 for a in bbox_artists:
-> 4587     bbox = a.get_tightbbox(renderer)
   4588     if (bbox is not None
   4589             and 0 < bbox.width < np.inf
   4590             and 0 < bbox.height < np.inf):
   4591         bb.append(bbox)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/legend.py:1060, in Legend.get_tightbbox(self, renderer)
   1058 def get_tightbbox(self, renderer=None):
   1059     # docstring inherited
-> 1060     return self._legend_box.get_window_extent(renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:367, in OffsetBox.get_window_extent(self, renderer)
    365 if renderer is None:
    366     renderer = self.get_figure(root=True)._get_renderer()
--> 367 bbox = self.get_bbox(renderer)
    368 try:  # Some subclasses redefine get_offset to take no args.
    369     px, py = self.get_offset(bbox, renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:460, in VPacker._get_bbox_and_child_offsets(self, renderer)
    457         if isinstance(c, PackerBase) and c.mode == "expand":
    458             c.set_width(self.width)
--> 460 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    461 (x0, x1), xoffsets = _get_aligned_offsets(
    462     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    463 height, yoffsets = _get_packed_offsets(
    464     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:493, in HPacker._get_bbox_and_child_offsets(self, renderer)
    490 pad = self.pad * dpicor
    491 sep = self.sep * dpicor
--> 493 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    494 if not bboxes:
    495     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:460, in VPacker._get_bbox_and_child_offsets(self, renderer)
    457         if isinstance(c, PackerBase) and c.mode == "expand":
    458             c.set_width(self.width)
--> 460 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    461 (x0, x1), xoffsets = _get_aligned_offsets(
    462     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    463 height, yoffsets = _get_packed_offsets(
    464     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:493, in HPacker._get_bbox_and_child_offsets(self, renderer)
    490 pad = self.pad * dpicor
    491 sep = self.sep * dpicor
--> 493 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    494 if not bboxes:
    495     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:797, in TextArea.get_bbox(self, renderer)
    796 def get_bbox(self, renderer):
--> 797     _, h_, d_ = mtext._get_text_metrics_with_cache(
    798         renderer, "lp", self._text._fontproperties,
    799         ismath="TeX" if self._text.get_usetex() else False,
    800         dpi=self.get_figure(root=True).dpi)
    802     bbox, info, yd = self._text._get_layout(renderer)
    803     w, h = bbox.size

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:367, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
--> 367     page, = dvi
    368 # A total height (including the descent) needs to be returned.
    369 return page.width, page.height + page.descent, page.descent

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:261, in Dvi.__iter__(self)
    245 def __iter__(self):
    246     """
    247     Iterate through the pages of the file.
    248 
   (...)    259         integers.
    260     """
--> 261     while self._read():
    262         yield self._output()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:343, in Dvi._read(self)
    341 self._dtable[byte](self, byte)
    342 if self._missing_font:
--> 343     raise self._missing_font.to_exception()
    344 name = self._dtable[byte].__name__
    345 if name == "_push":

FileNotFoundError: Matplotlib's TeX implementation searched for a file named 'cmss10.tfm' in your texmf tree, but could not find it
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/formatters.py:402, in BaseFormatter.__call__(self, obj)
    400     pass
    401 else:
--> 402     return printer(obj)
    403 # Finally look for special method names
    404 method = get_real_method(obj, self.print_method)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:187, in retina_figure(fig, base64, **kwargs)
    178 def retina_figure(fig, base64=False, **kwargs):
    179     """format a figure as a pixel-doubled (retina) PNG
    180 
    181     If `base64` is True, return base64-encoded str instead of raw bytes
   (...)    185         base64 argument
    186     """
--> 187     pngdata = print_figure(fig, fmt="retina", base64=False, **kwargs)
    188     # Make sure that retina_figure acts just like print_figure and returns
    189     # None when the figure is empty.
    190     if pngdata is None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:170, in print_figure(fig, fmt, bbox_inches, base64, **kwargs)
    167     from matplotlib.backend_bases import FigureCanvasBase
    168     FigureCanvasBase(fig)
--> 170 fig.canvas.print_figure(bytes_io, **kw)
    171 data = bytes_io.getvalue()
    172 if fmt == 'svg':

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:2157, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2154     # we do this instead of `self.figure.draw_without_rendering`
   2155     # so that we can inject the orientation
   2156     with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2157         self.figure.draw(renderer)
   2158 if bbox_inches:
   2159     if bbox_inches == "tight":

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:388, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    384     return
    386 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
    387                                  hspace=hspace, wspace=wspace)
--> 388 pos, bbox = get_pos_and_bbox(ax, renderer)
    389 # the margin is the distance between the bounding box of the Axes
    390 # and its position (plus the padding from above)
    391 margin['left'] += pos.x0 - bbox.x0

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4587, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4584     bbox_artists = self.get_default_bbox_extra_artists()
   4586 for a in bbox_artists:
-> 4587     bbox = a.get_tightbbox(renderer)
   4588     if (bbox is not None
   4589             and 0 < bbox.width < np.inf
   4590             and 0 < bbox.height < np.inf):
   4591         bb.append(bbox)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/legend.py:1060, in Legend.get_tightbbox(self, renderer)
   1058 def get_tightbbox(self, renderer=None):
   1059     # docstring inherited
-> 1060     return self._legend_box.get_window_extent(renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:367, in OffsetBox.get_window_extent(self, renderer)
    365 if renderer is None:
    366     renderer = self.get_figure(root=True)._get_renderer()
--> 367 bbox = self.get_bbox(renderer)
    368 try:  # Some subclasses redefine get_offset to take no args.
    369     px, py = self.get_offset(bbox, renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:460, in VPacker._get_bbox_and_child_offsets(self, renderer)
    457         if isinstance(c, PackerBase) and c.mode == "expand":
    458             c.set_width(self.width)
--> 460 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    461 (x0, x1), xoffsets = _get_aligned_offsets(
    462     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    463 height, yoffsets = _get_packed_offsets(
    464     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:493, in HPacker._get_bbox_and_child_offsets(self, renderer)
    490 pad = self.pad * dpicor
    491 sep = self.sep * dpicor
--> 493 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    494 if not bboxes:
    495     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:460, in VPacker._get_bbox_and_child_offsets(self, renderer)
    457         if isinstance(c, PackerBase) and c.mode == "expand":
    458             c.set_width(self.width)
--> 460 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    461 (x0, x1), xoffsets = _get_aligned_offsets(
    462     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    463 height, yoffsets = _get_packed_offsets(
    464     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:493, in HPacker._get_bbox_and_child_offsets(self, renderer)
    490 pad = self.pad * dpicor
    491 sep = self.sep * dpicor
--> 493 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    494 if not bboxes:
    495     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:797, in TextArea.get_bbox(self, renderer)
    796 def get_bbox(self, renderer):
--> 797     _, h_, d_ = mtext._get_text_metrics_with_cache(
    798         renderer, "lp", self._text._fontproperties,
    799         ismath="TeX" if self._text.get_usetex() else False,
    800         dpi=self.get_figure(root=True).dpi)
    802     bbox, info, yd = self._text._get_layout(renderer)
    803     w, h = bbox.size

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:367, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
--> 367     page, = dvi
    368 # A total height (including the descent) needs to be returned.
    369 return page.width, page.height + page.descent, page.descent

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:261, in Dvi.__iter__(self)
    245 def __iter__(self):
    246     """
    247     Iterate through the pages of the file.
    248 
   (...)    259         integers.
    260     """
--> 261     while self._read():
    262         yield self._output()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:343, in Dvi._read(self)
    341 self._dtable[byte](self, byte)
    342 if self._missing_font:
--> 343     raise self._missing_font.to_exception()
    344 name = self._dtable[byte].__name__
    345 if name == "_push":

FileNotFoundError: Matplotlib's TeX implementation searched for a file named 'cmss10.tfm' in your texmf tree, but could not find it
<Figure size 735x350 with 4 Axes>

Figure 4. (Left) Contour boundary edges color coded by their index in the boundary edge list. (Right) An attempt to plot the boundary edges as polygon. Because of the near random order of edges around the contour, the resulting polygon is self-intersecting.

Unstrucuted Mesh Contours as Graphs#

Using the \(\mathrm{XOR}\) operator and the \(\mathrm{cellsOnEdge}\) connectivity array, we have identified the edges corresponding to the contour boundary. These contour edges are useful, but for plotting purposes what we are really interested in is an ordered sequence of vertices so that we can draw them as a continuous line. We can use the \(\mathrm{verticesOnEdge}\) connectivity array (\(\mathrm{nEdges} \times 2\)) for the subset of edges that correspond to the contour boundary to get the information we need. This subset of \(\mathrm{verticesOnEdge}\) naturally defines a graph, where mesh vertices become graph nodes and contour edges become graph edges.

The key structural property of this graph is that every node has degree at most 2; each contour vertex is shared by at most two contour edges (the one “entering” and the one “leaving”). This constraint means every connected component is one of exactly two topologies:

  • Path graph: an open arc whose two degree-1 endpoints lie on the domain boundary. The contour crosses the mesh boundary.

  • Cycle graph: a closed loop entirely within the domain interior where every node has degree exactly 2.

Because the maximum degree is 2, traversal reduces to a simple linear chain walk: at each step there is at most one unvisited neighbor. For a path, we start at one of the two degree-1 endpoints and walk to the other. For a cycle, we start at any node and walk until no unvisited neighbors remain, then close the loop by appending the start node.

This is implemented in the custom ContourGraph class in mosaic/contour.py. The walk() method performs the chain traversal, and components() uses an iterative depth-first search to enumerate connected components. No general graph library is required at runtime — NetworkX is available only as an optional testing utility via ContourGraph.to_networkx().

Error in callback <function _draw_all_if_interactive at 0x7f4811a4ba00> (for post_execute), with arguments args (),kwargs {}:
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/pyplot.py:278, in _draw_all_if_interactive()
    276 def _draw_all_if_interactive() -> None:
    277     if matplotlib.is_interactive():
--> 278         draw_all()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_pylab_helpers.py:131, in Gcf.draw_all(cls, force)
    129 for manager in cls.get_all_fig_managers():
    130     if force or manager.canvas.figure.stale:
--> 131         manager.canvas.draw_idle()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:1893, in FigureCanvasBase.draw_idle(self, *args, **kwargs)
   1891 if not self._is_idle_drawing:
   1892     with self._idle_draw_cntx():
-> 1893         self.draw(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:382, in FigureCanvasAgg.draw(self)
    379 # Acquire a lock on the shared font cache.
    380 with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
    381       else nullcontext()):
--> 382     self.figure.draw(self.renderer)
    383     # A GUI class may be need to update a window using this draw, so
    384     # don't forget to call the superclass.
    385     super().draw()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:388, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    384     return
    386 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
    387                                  hspace=hspace, wspace=wspace)
--> 388 pos, bbox = get_pos_and_bbox(ax, renderer)
    389 # the margin is the distance between the bounding box of the Axes
    390 # and its position (plus the padding from above)
    391 margin['left'] += pos.x0 - bbox.x0

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4587, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4584     bbox_artists = self.get_default_bbox_extra_artists()
   4586 for a in bbox_artists:
-> 4587     bbox = a.get_tightbbox(renderer)
   4588     if (bbox is not None
   4589             and 0 < bbox.width < np.inf
   4590             and 0 < bbox.height < np.inf):
   4591         bb.append(bbox)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/legend.py:1060, in Legend.get_tightbbox(self, renderer)
   1058 def get_tightbbox(self, renderer=None):
   1059     # docstring inherited
-> 1060     return self._legend_box.get_window_extent(renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:367, in OffsetBox.get_window_extent(self, renderer)
    365 if renderer is None:
    366     renderer = self.get_figure(root=True)._get_renderer()
--> 367 bbox = self.get_bbox(renderer)
    368 try:  # Some subclasses redefine get_offset to take no args.
    369     px, py = self.get_offset(bbox, renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:460, in VPacker._get_bbox_and_child_offsets(self, renderer)
    457         if isinstance(c, PackerBase) and c.mode == "expand":
    458             c.set_width(self.width)
--> 460 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    461 (x0, x1), xoffsets = _get_aligned_offsets(
    462     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    463 height, yoffsets = _get_packed_offsets(
    464     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:493, in HPacker._get_bbox_and_child_offsets(self, renderer)
    490 pad = self.pad * dpicor
    491 sep = self.sep * dpicor
--> 493 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    494 if not bboxes:
    495     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:460, in VPacker._get_bbox_and_child_offsets(self, renderer)
    457         if isinstance(c, PackerBase) and c.mode == "expand":
    458             c.set_width(self.width)
--> 460 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    461 (x0, x1), xoffsets = _get_aligned_offsets(
    462     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    463 height, yoffsets = _get_packed_offsets(
    464     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:493, in HPacker._get_bbox_and_child_offsets(self, renderer)
    490 pad = self.pad * dpicor
    491 sep = self.sep * dpicor
--> 493 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    494 if not bboxes:
    495     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:797, in TextArea.get_bbox(self, renderer)
    796 def get_bbox(self, renderer):
--> 797     _, h_, d_ = mtext._get_text_metrics_with_cache(
    798         renderer, "lp", self._text._fontproperties,
    799         ismath="TeX" if self._text.get_usetex() else False,
    800         dpi=self.get_figure(root=True).dpi)
    802     bbox, info, yd = self._text._get_layout(renderer)
    803     w, h = bbox.size

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:367, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
--> 367     page, = dvi
    368 # A total height (including the descent) needs to be returned.
    369 return page.width, page.height + page.descent, page.descent

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:261, in Dvi.__iter__(self)
    245 def __iter__(self):
    246     """
    247     Iterate through the pages of the file.
    248 
   (...)    259         integers.
    260     """
--> 261     while self._read():
    262         yield self._output()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:343, in Dvi._read(self)
    341 self._dtable[byte](self, byte)
    342 if self._missing_font:
--> 343     raise self._missing_font.to_exception()
    344 name = self._dtable[byte].__name__
    345 if name == "_push":

FileNotFoundError: Matplotlib's TeX implementation searched for a file named 'cmss10.tfm' in your texmf tree, but could not find it
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/formatters.py:402, in BaseFormatter.__call__(self, obj)
    400     pass
    401 else:
--> 402     return printer(obj)
    403 # Finally look for special method names
    404 method = get_real_method(obj, self.print_method)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:187, in retina_figure(fig, base64, **kwargs)
    178 def retina_figure(fig, base64=False, **kwargs):
    179     """format a figure as a pixel-doubled (retina) PNG
    180 
    181     If `base64` is True, return base64-encoded str instead of raw bytes
   (...)    185         base64 argument
    186     """
--> 187     pngdata = print_figure(fig, fmt="retina", base64=False, **kwargs)
    188     # Make sure that retina_figure acts just like print_figure and returns
    189     # None when the figure is empty.
    190     if pngdata is None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:170, in print_figure(fig, fmt, bbox_inches, base64, **kwargs)
    167     from matplotlib.backend_bases import FigureCanvasBase
    168     FigureCanvasBase(fig)
--> 170 fig.canvas.print_figure(bytes_io, **kw)
    171 data = bytes_io.getvalue()
    172 if fmt == 'svg':

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:2157, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2154     # we do this instead of `self.figure.draw_without_rendering`
   2155     # so that we can inject the orientation
   2156     with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2157         self.figure.draw(renderer)
   2158 if bbox_inches:
   2159     if bbox_inches == "tight":

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:388, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    384     return
    386 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
    387                                  hspace=hspace, wspace=wspace)
--> 388 pos, bbox = get_pos_and_bbox(ax, renderer)
    389 # the margin is the distance between the bounding box of the Axes
    390 # and its position (plus the padding from above)
    391 margin['left'] += pos.x0 - bbox.x0

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4587, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4584     bbox_artists = self.get_default_bbox_extra_artists()
   4586 for a in bbox_artists:
-> 4587     bbox = a.get_tightbbox(renderer)
   4588     if (bbox is not None
   4589             and 0 < bbox.width < np.inf
   4590             and 0 < bbox.height < np.inf):
   4591         bb.append(bbox)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/legend.py:1060, in Legend.get_tightbbox(self, renderer)
   1058 def get_tightbbox(self, renderer=None):
   1059     # docstring inherited
-> 1060     return self._legend_box.get_window_extent(renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:367, in OffsetBox.get_window_extent(self, renderer)
    365 if renderer is None:
    366     renderer = self.get_figure(root=True)._get_renderer()
--> 367 bbox = self.get_bbox(renderer)
    368 try:  # Some subclasses redefine get_offset to take no args.
    369     px, py = self.get_offset(bbox, renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:460, in VPacker._get_bbox_and_child_offsets(self, renderer)
    457         if isinstance(c, PackerBase) and c.mode == "expand":
    458             c.set_width(self.width)
--> 460 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    461 (x0, x1), xoffsets = _get_aligned_offsets(
    462     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    463 height, yoffsets = _get_packed_offsets(
    464     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:493, in HPacker._get_bbox_and_child_offsets(self, renderer)
    490 pad = self.pad * dpicor
    491 sep = self.sep * dpicor
--> 493 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    494 if not bboxes:
    495     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:460, in VPacker._get_bbox_and_child_offsets(self, renderer)
    457         if isinstance(c, PackerBase) and c.mode == "expand":
    458             c.set_width(self.width)
--> 460 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    461 (x0, x1), xoffsets = _get_aligned_offsets(
    462     [bbox.intervalx for bbox in bboxes], self.width, self.align)
    463 height, yoffsets = _get_packed_offsets(
    464     [bbox.height for bbox in bboxes], self.height, sep, self.mode)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:360, in OffsetBox.get_bbox(self, renderer)
    358 def get_bbox(self, renderer):
    359     """Return the bbox of the offsetbox, ignoring parent offsets."""
--> 360     bbox, offsets = self._get_bbox_and_child_offsets(renderer)
    361     return bbox

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:493, in HPacker._get_bbox_and_child_offsets(self, renderer)
    490 pad = self.pad * dpicor
    491 sep = self.sep * dpicor
--> 493 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
    494 if not bboxes:
    495     return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/offsetbox.py:797, in TextArea.get_bbox(self, renderer)
    796 def get_bbox(self, renderer):
--> 797     _, h_, d_ = mtext._get_text_metrics_with_cache(
    798         renderer, "lp", self._text._fontproperties,
    799         ismath="TeX" if self._text.get_usetex() else False,
    800         dpi=self.get_figure(root=True).dpi)
    802     bbox, info, yd = self._text._get_layout(renderer)
    803     w, h = bbox.size

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:367, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
--> 367     page, = dvi
    368 # A total height (including the descent) needs to be returned.
    369 return page.width, page.height + page.descent, page.descent

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:261, in Dvi.__iter__(self)
    245 def __iter__(self):
    246     """
    247     Iterate through the pages of the file.
    248 
   (...)    259         integers.
    260     """
--> 261     while self._read():
    262         yield self._output()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:343, in Dvi._read(self)
    341 self._dtable[byte](self, byte)
    342 if self._missing_font:
--> 343     raise self._missing_font.to_exception()
    344 name = self._dtable[byte].__name__
    345 if name == "_push":

FileNotFoundError: Matplotlib's TeX implementation searched for a file named 'cmss10.tfm' in your texmf tree, but could not find it
<Figure size 735x350 with 4 Axes>

Figure 5. (Left) Ordered contour boundary edges color-coded by their index in the boundary edge list. (Right) Ordered contour boundary edges as a polygon. Because the graph traversal produces a sorted vertex sequence, the resulting polygon is valid (i.e. not self-intersecting).

With the ordered vertex sequence from the graph traversal, we can now plot the contour boundary as a polygon. This is the basic functionality we need to plot unstructured contour boundaries in matplotlib. Before we move on, let’s quickly look at another way to delineate the contour boundary.

Interface#

With a basic sketch of the algorithmic approach outlined above, let’s now discuss how we will implement this in practice and how we envision a user interacting with it. The goal is to provide contouring ability in mosaic where the interface to our contouring functions are as close as possible to matplotlib interface. Such that users can capitialize on their previous experience with matplotlib and seamlessly contour directly on the unstructed mesh.

Ideally, we will implement something like:

mosaic.contour(ax, descriptor, field, ...)
mosaic.contourf(ax, descriptor, field, ...)

where ... are the exact same keyword positional and key-word arguments as the matplotlib comensurate functions.

Unfilled Contours#

We’ll begin with unfilled contours (i.e. mosaic.contour), as they are simpler task.

Instead of such a simple gaussian field as we consider before, let’s work with something more complicated:

\[ f(x, y) = A \sin \left(\frac{N \pi x}{L_{\rm x}}\right) \cos \left(\frac{N \pi y}{L_{\rm y}}\right) \]
where:

  • \(A\) is the amplitude

  • \(N\) is the period

  • \(L_{\rm x}\) and \(L_{\rm y}\) as the domain lengths in the \(x\) and \(y\) directions, respectively

We’ll also slightly increase the size of the mesh to be \(50 \times 50\) cells.

ds = planar_hex_mesh(100)

field = sinusoid(ds.xCell, ds.yCell)
descriptor = mosaic.Descriptor(ds)
Error in callback <function _draw_all_if_interactive at 0x7f4811a4ba00> (for post_execute), with arguments args (),kwargs {}:
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/pyplot.py:278, in _draw_all_if_interactive()
    276 def _draw_all_if_interactive() -> None:
    277     if matplotlib.is_interactive():
--> 278         draw_all()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_pylab_helpers.py:131, in Gcf.draw_all(cls, force)
    129 for manager in cls.get_all_fig_managers():
    130     if force or manager.canvas.figure.stale:
--> 131         manager.canvas.draw_idle()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:1893, in FigureCanvasBase.draw_idle(self, *args, **kwargs)
   1891 if not self._is_idle_drawing:
   1892     with self._idle_draw_cntx():
-> 1893         self.draw(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:382, in FigureCanvasAgg.draw(self)
    379 # Acquire a lock on the shared font cache.
    380 with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
    381       else nullcontext()):
--> 382     self.figure.draw(self.renderer)
    383     # A GUI class may be need to update a window using this draw, so
    384     # don't forget to call the superclass.
    385     super().draw()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:405, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    403 cbp_rspan, cbp_cspan = get_cb_parent_spans(cbax)
    404 loc = cbax._colorbar_info['location']
--> 405 cbpos, cbbbox = get_pos_and_bbox(cbax, renderer)
    406 if loc == 'right':
    407     if cbp_cspan.stop == ss.colspan.stop:
    408         # only increase if the colorbar is on the right edge

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4564, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4562 for axis in self._axis_map.values():
   4563     if self.axison and axis.get_visible():
-> 4564         ba = martist._get_tightbbox_for_layout_only(axis, renderer)
   4565         if ba:
   4566             bb.append(ba)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1353, in Axis.get_tightbbox(self, renderer, for_layout_only)
   1350     renderer = self.get_figure(root=True)._get_renderer()
   1351 ticks_to_draw = self._update_ticks()
-> 1353 self._update_label_position(renderer)
   1355 # go back to just this axis's tick labels
   1356 tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2675, in YAxis._update_label_position(self, renderer)
   2671     return
   2673 # get bounding boxes for this axis and any siblings
   2674 # that have been set by `fig.align_ylabels()`
-> 2675 bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
   2676 x, y = self.label.get_position()
   2678 if self.label_position == 'left':
   2679     # Union with extents of the left spine if present, of the axes otherwise.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2241, in Axis._get_tick_boxes_siblings(self, renderer)
   2239 axis = ax._axis_map[name]
   2240 ticks_to_draw = axis._update_ticks()
-> 2241 tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer)
   2242 bboxes.extend(tlb)
   2243 bboxes2.extend(tlb2)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1334, in Axis._get_ticklabel_bboxes(self, ticks, renderer)
   1330 if renderer is None:
   1331     renderer = self.get_figure(root=True)._get_renderer()
   1332 return ([tick.label1.get_window_extent(renderer)
   1333          for tick in ticks if tick.label1.get_visible()],
-> 1334         [tick.label2.get_window_extent(renderer)
   1335          for tick in ticks if tick.label2.get_visible()])

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
    964     raise RuntimeError(
    965         "Cannot get window extent of text w/o renderer. You likely "
    966         "want to call 'figure.draw_without_rendering()' first.")
    968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969     bbox, info, descent = self._get_layout(self._renderer)
    970     x, y = self.get_unitless_position()
    971     x, y = self.get_transform().transform((x, y))

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
    370 ys = []
    372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
    374     renderer, "lp", self._fontproperties,
    375     ismath="TeX" if self.get_usetex() else False,
    376     dpi=self.get_figure(root=True).dpi)
    377 min_dy = (lp_h - lp_d) * self._linespacing
    379 for i, line in enumerate(lines):

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:367, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
--> 367     page, = dvi
    368 # A total height (including the descent) needs to be returned.
    369 return page.width, page.height + page.descent, page.descent

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:261, in Dvi.__iter__(self)
    245 def __iter__(self):
    246     """
    247     Iterate through the pages of the file.
    248 
   (...)    259         integers.
    260     """
--> 261     while self._read():
    262         yield self._output()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:343, in Dvi._read(self)
    341 self._dtable[byte](self, byte)
    342 if self._missing_font:
--> 343     raise self._missing_font.to_exception()
    344 name = self._dtable[byte].__name__
    345 if name == "_push":

FileNotFoundError: Matplotlib's TeX implementation searched for a file named 'cmss10.tfm' in your texmf tree, but could not find it
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/formatters.py:402, in BaseFormatter.__call__(self, obj)
    400     pass
    401 else:
--> 402     return printer(obj)
    403 # Finally look for special method names
    404 method = get_real_method(obj, self.print_method)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:187, in retina_figure(fig, base64, **kwargs)
    178 def retina_figure(fig, base64=False, **kwargs):
    179     """format a figure as a pixel-doubled (retina) PNG
    180 
    181     If `base64` is True, return base64-encoded str instead of raw bytes
   (...)    185         base64 argument
    186     """
--> 187     pngdata = print_figure(fig, fmt="retina", base64=False, **kwargs)
    188     # Make sure that retina_figure acts just like print_figure and returns
    189     # None when the figure is empty.
    190     if pngdata is None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:170, in print_figure(fig, fmt, bbox_inches, base64, **kwargs)
    167     from matplotlib.backend_bases import FigureCanvasBase
    168     FigureCanvasBase(fig)
--> 170 fig.canvas.print_figure(bytes_io, **kw)
    171 data = bytes_io.getvalue()
    172 if fmt == 'svg':

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:2157, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2154     # we do this instead of `self.figure.draw_without_rendering`
   2155     # so that we can inject the orientation
   2156     with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2157         self.figure.draw(renderer)
   2158 if bbox_inches:
   2159     if bbox_inches == "tight":

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:405, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    403 cbp_rspan, cbp_cspan = get_cb_parent_spans(cbax)
    404 loc = cbax._colorbar_info['location']
--> 405 cbpos, cbbbox = get_pos_and_bbox(cbax, renderer)
    406 if loc == 'right':
    407     if cbp_cspan.stop == ss.colspan.stop:
    408         # only increase if the colorbar is on the right edge

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4564, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4562 for axis in self._axis_map.values():
   4563     if self.axison and axis.get_visible():
-> 4564         ba = martist._get_tightbbox_for_layout_only(axis, renderer)
   4565         if ba:
   4566             bb.append(ba)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1353, in Axis.get_tightbbox(self, renderer, for_layout_only)
   1350     renderer = self.get_figure(root=True)._get_renderer()
   1351 ticks_to_draw = self._update_ticks()
-> 1353 self._update_label_position(renderer)
   1355 # go back to just this axis's tick labels
   1356 tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2675, in YAxis._update_label_position(self, renderer)
   2671     return
   2673 # get bounding boxes for this axis and any siblings
   2674 # that have been set by `fig.align_ylabels()`
-> 2675 bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
   2676 x, y = self.label.get_position()
   2678 if self.label_position == 'left':
   2679     # Union with extents of the left spine if present, of the axes otherwise.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2241, in Axis._get_tick_boxes_siblings(self, renderer)
   2239 axis = ax._axis_map[name]
   2240 ticks_to_draw = axis._update_ticks()
-> 2241 tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer)
   2242 bboxes.extend(tlb)
   2243 bboxes2.extend(tlb2)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1334, in Axis._get_ticklabel_bboxes(self, ticks, renderer)
   1330 if renderer is None:
   1331     renderer = self.get_figure(root=True)._get_renderer()
   1332 return ([tick.label1.get_window_extent(renderer)
   1333          for tick in ticks if tick.label1.get_visible()],
-> 1334         [tick.label2.get_window_extent(renderer)
   1335          for tick in ticks if tick.label2.get_visible()])

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
    964     raise RuntimeError(
    965         "Cannot get window extent of text w/o renderer. You likely "
    966         "want to call 'figure.draw_without_rendering()' first.")
    968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969     bbox, info, descent = self._get_layout(self._renderer)
    970     x, y = self.get_unitless_position()
    971     x, y = self.get_transform().transform((x, y))

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
    370 ys = []
    372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
    374     renderer, "lp", self._fontproperties,
    375     ismath="TeX" if self.get_usetex() else False,
    376     dpi=self.get_figure(root=True).dpi)
    377 min_dy = (lp_h - lp_d) * self._linespacing
    379 for i, line in enumerate(lines):

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:367, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
--> 367     page, = dvi
    368 # A total height (including the descent) needs to be returned.
    369 return page.width, page.height + page.descent, page.descent

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:261, in Dvi.__iter__(self)
    245 def __iter__(self):
    246     """
    247     Iterate through the pages of the file.
    248 
   (...)    259         integers.
    260     """
--> 261     while self._read():
    262         yield self._output()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:343, in Dvi._read(self)
    341 self._dtable[byte](self, byte)
    342 if self._missing_font:
--> 343     raise self._missing_font.to_exception()
    344 name = self._dtable[byte].__name__
    345 if name == "_push":

FileNotFoundError: Matplotlib's TeX implementation searched for a file named 'cmss10.tfm' in your texmf tree, but could not find it
<Figure size 350x350 with 2 Axes>

Figure 6. Checkerboard field, evaluated on a \(100 \times 100\) cell mesh.

A first step, let’s create the boolean

mask = field < 0.0

Figure 7. (Left) Boolean mask of \(f(x, y) < 0.0\) evaluated on the MPAS mesh. (Right) The same checkerboard field, evaluated and contoured on a regular quadrilateral mesh. Blue lines are the contour lines and grey is the filled contour area.

Boundaries

We can see from the matplotlib.pyplot.contour example above (RHS of Figure 7), that contour lines should be discontinuous when they intersect with a boundary. For example, while the plotted mask follows the plot boundary, the contour lines abruptly end. They do not from closed loops by using the boundary. We’ll mimic this behavior in mosaic.

Hide code cell source

fig, ax = double_pannel()

kwargs = {
    "levels": [-1.0, 0.0, 1.0],
    "colors": ["tab:blue", "tab:orange", "tab:red"],
    "linestyles": "-",
}

mosaic.polypcolor(ax[0], descriptor, field, alpha=1 / 3, ec="k", lw=1 / 5)
mosaic.contour(
    ax[0],
    descriptor,
    field,
    alpha=1.0,
    **kwargs,
)

ax[1].pcolormesh(*rect_grid, rect_data, alpha=1 / 3, lw=1 / 5)
ax[1].contour(*rect_grid, rect_data, **kwargs)

ax[0].set_title(r"\texttt{mosiac.contour}")
ax[1].set_title(r"\texttt{plt.contour}");
Error in callback <function _draw_all_if_interactive at 0x7f4811a4ba00> (for post_execute), with arguments args (),kwargs {}:
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/pyplot.py:278, in _draw_all_if_interactive()
    276 def _draw_all_if_interactive() -> None:
    277     if matplotlib.is_interactive():
--> 278         draw_all()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_pylab_helpers.py:131, in Gcf.draw_all(cls, force)
    129 for manager in cls.get_all_fig_managers():
    130     if force or manager.canvas.figure.stale:
--> 131         manager.canvas.draw_idle()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:1893, in FigureCanvasBase.draw_idle(self, *args, **kwargs)
   1891 if not self._is_idle_drawing:
   1892     with self._idle_draw_cntx():
-> 1893         self.draw(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:382, in FigureCanvasAgg.draw(self)
    379 # Acquire a lock on the shared font cache.
    380 with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
    381       else nullcontext()):
--> 382     self.figure.draw(self.renderer)
    383     # A GUI class may be need to update a window using this draw, so
    384     # don't forget to call the superclass.
    385     super().draw()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:388, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    384     return
    386 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
    387                                  hspace=hspace, wspace=wspace)
--> 388 pos, bbox = get_pos_and_bbox(ax, renderer)
    389 # the margin is the distance between the bounding box of the Axes
    390 # and its position (plus the padding from above)
    391 margin['left'] += pos.x0 - bbox.x0

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4567, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4565         if ba:
   4566             bb.append(ba)
-> 4567 self._update_title_position(renderer)
   4568 axbbox = self.get_window_extent(renderer)
   4569 bb.append(axbbox)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:3134, in _AxesBase._update_title_position(self, renderer)
   3132 if title.get_text():
   3133     for ax in axs:
-> 3134         ax.yaxis.get_tightbbox(renderer)  # update offsetText
   3135         if ax.yaxis.offsetText.get_text():
   3136             bb = ax.yaxis.offsetText.get_tightbbox(renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1353, in Axis.get_tightbbox(self, renderer, for_layout_only)
   1350     renderer = self.get_figure(root=True)._get_renderer()
   1351 ticks_to_draw = self._update_ticks()
-> 1353 self._update_label_position(renderer)
   1355 # go back to just this axis's tick labels
   1356 tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2675, in YAxis._update_label_position(self, renderer)
   2671     return
   2673 # get bounding boxes for this axis and any siblings
   2674 # that have been set by `fig.align_ylabels()`
-> 2675 bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
   2676 x, y = self.label.get_position()
   2678 if self.label_position == 'left':
   2679     # Union with extents of the left spine if present, of the axes otherwise.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2241, in Axis._get_tick_boxes_siblings(self, renderer)
   2239 axis = ax._axis_map[name]
   2240 ticks_to_draw = axis._update_ticks()
-> 2241 tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer)
   2242 bboxes.extend(tlb)
   2243 bboxes2.extend(tlb2)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1332, in Axis._get_ticklabel_bboxes(self, ticks, renderer)
   1330 if renderer is None:
   1331     renderer = self.get_figure(root=True)._get_renderer()
-> 1332 return ([tick.label1.get_window_extent(renderer)
   1333          for tick in ticks if tick.label1.get_visible()],
   1334         [tick.label2.get_window_extent(renderer)
   1335          for tick in ticks if tick.label2.get_visible()])

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
    964     raise RuntimeError(
    965         "Cannot get window extent of text w/o renderer. You likely "
    966         "want to call 'figure.draw_without_rendering()' first.")
    968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969     bbox, info, descent = self._get_layout(self._renderer)
    970     x, y = self.get_unitless_position()
    971     x, y = self.get_transform().transform((x, y))

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
    370 ys = []
    372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
    374     renderer, "lp", self._fontproperties,
    375     ismath="TeX" if self.get_usetex() else False,
    376     dpi=self.get_figure(root=True).dpi)
    377 min_dy = (lp_h - lp_d) * self._linespacing
    379 for i, line in enumerate(lines):

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:367, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
--> 367     page, = dvi
    368 # A total height (including the descent) needs to be returned.
    369 return page.width, page.height + page.descent, page.descent

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:261, in Dvi.__iter__(self)
    245 def __iter__(self):
    246     """
    247     Iterate through the pages of the file.
    248 
   (...)    259         integers.
    260     """
--> 261     while self._read():
    262         yield self._output()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:343, in Dvi._read(self)
    341 self._dtable[byte](self, byte)
    342 if self._missing_font:
--> 343     raise self._missing_font.to_exception()
    344 name = self._dtable[byte].__name__
    345 if name == "_push":

FileNotFoundError: Matplotlib's TeX implementation searched for a file named 'cmss10.tfm' in your texmf tree, but could not find it
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/formatters.py:402, in BaseFormatter.__call__(self, obj)
    400     pass
    401 else:
--> 402     return printer(obj)
    403 # Finally look for special method names
    404 method = get_real_method(obj, self.print_method)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:187, in retina_figure(fig, base64, **kwargs)
    178 def retina_figure(fig, base64=False, **kwargs):
    179     """format a figure as a pixel-doubled (retina) PNG
    180 
    181     If `base64` is True, return base64-encoded str instead of raw bytes
   (...)    185         base64 argument
    186     """
--> 187     pngdata = print_figure(fig, fmt="retina", base64=False, **kwargs)
    188     # Make sure that retina_figure acts just like print_figure and returns
    189     # None when the figure is empty.
    190     if pngdata is None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:170, in print_figure(fig, fmt, bbox_inches, base64, **kwargs)
    167     from matplotlib.backend_bases import FigureCanvasBase
    168     FigureCanvasBase(fig)
--> 170 fig.canvas.print_figure(bytes_io, **kw)
    171 data = bytes_io.getvalue()
    172 if fmt == 'svg':

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:2157, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2154     # we do this instead of `self.figure.draw_without_rendering`
   2155     # so that we can inject the orientation
   2156     with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2157         self.figure.draw(renderer)
   2158 if bbox_inches:
   2159     if bbox_inches == "tight":

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:388, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    384     return
    386 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
    387                                  hspace=hspace, wspace=wspace)
--> 388 pos, bbox = get_pos_and_bbox(ax, renderer)
    389 # the margin is the distance between the bounding box of the Axes
    390 # and its position (plus the padding from above)
    391 margin['left'] += pos.x0 - bbox.x0

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4567, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4565         if ba:
   4566             bb.append(ba)
-> 4567 self._update_title_position(renderer)
   4568 axbbox = self.get_window_extent(renderer)
   4569 bb.append(axbbox)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:3134, in _AxesBase._update_title_position(self, renderer)
   3132 if title.get_text():
   3133     for ax in axs:
-> 3134         ax.yaxis.get_tightbbox(renderer)  # update offsetText
   3135         if ax.yaxis.offsetText.get_text():
   3136             bb = ax.yaxis.offsetText.get_tightbbox(renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1353, in Axis.get_tightbbox(self, renderer, for_layout_only)
   1350     renderer = self.get_figure(root=True)._get_renderer()
   1351 ticks_to_draw = self._update_ticks()
-> 1353 self._update_label_position(renderer)
   1355 # go back to just this axis's tick labels
   1356 tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2675, in YAxis._update_label_position(self, renderer)
   2671     return
   2673 # get bounding boxes for this axis and any siblings
   2674 # that have been set by `fig.align_ylabels()`
-> 2675 bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
   2676 x, y = self.label.get_position()
   2678 if self.label_position == 'left':
   2679     # Union with extents of the left spine if present, of the axes otherwise.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2241, in Axis._get_tick_boxes_siblings(self, renderer)
   2239 axis = ax._axis_map[name]
   2240 ticks_to_draw = axis._update_ticks()
-> 2241 tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer)
   2242 bboxes.extend(tlb)
   2243 bboxes2.extend(tlb2)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1332, in Axis._get_ticklabel_bboxes(self, ticks, renderer)
   1330 if renderer is None:
   1331     renderer = self.get_figure(root=True)._get_renderer()
-> 1332 return ([tick.label1.get_window_extent(renderer)
   1333          for tick in ticks if tick.label1.get_visible()],
   1334         [tick.label2.get_window_extent(renderer)
   1335          for tick in ticks if tick.label2.get_visible()])

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
    964     raise RuntimeError(
    965         "Cannot get window extent of text w/o renderer. You likely "
    966         "want to call 'figure.draw_without_rendering()' first.")
    968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969     bbox, info, descent = self._get_layout(self._renderer)
    970     x, y = self.get_unitless_position()
    971     x, y = self.get_transform().transform((x, y))

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
    370 ys = []
    372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
    374     renderer, "lp", self._fontproperties,
    375     ismath="TeX" if self.get_usetex() else False,
    376     dpi=self.get_figure(root=True).dpi)
    377 min_dy = (lp_h - lp_d) * self._linespacing
    379 for i, line in enumerate(lines):

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:367, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
--> 367     page, = dvi
    368 # A total height (including the descent) needs to be returned.
    369 return page.width, page.height + page.descent, page.descent

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:261, in Dvi.__iter__(self)
    245 def __iter__(self):
    246     """
    247     Iterate through the pages of the file.
    248 
   (...)    259         integers.
    260     """
--> 261     while self._read():
    262         yield self._output()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:343, in Dvi._read(self)
    341 self._dtable[byte](self, byte)
    342 if self._missing_font:
--> 343     raise self._missing_font.to_exception()
    344 name = self._dtable[byte].__name__
    345 if name == "_push":

FileNotFoundError: Matplotlib's TeX implementation searched for a file named 'cmss10.tfm' in your texmf tree, but could not find it
<Figure size 735x350 with 3 Axes>

Figure 8. (Left) mosaic.contour result for contours at \(-1, 0, 1\) (blue, orange, red) of the checkerboard field, evaluated on the MPAS mesh. (Right) plt.contour of the same checkerboard field on a regular quadrilateral grid for comparison.

As we can see above, we get good agreement between our mosaic.contour implementation and the plt.contour result. The major difference is the jagged appearance of the MPAS contours due to the coarse resolution of the mesh, but this is desired feature of implementation. plt.contour does linear interpolation, which produces the smooth boundaries, but that is not something we want (or at least on all the time). Future work will investigate adding optional support for smooth contours.

Filled Contours#

Filled contours, specifically the possibility of interior boundaries, presents a challenge to our treatment so far mesh contours as graphs. To illustrate this challenge we will visualize two, multi-component, isomorphic graphs, which according to graph theory are equivalent.

---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/formatters.py:402, in BaseFormatter.__call__(self, obj)
    400     pass
    401 else:
--> 402     return printer(obj)
    403 # Finally look for special method names
    404 method = get_real_method(obj, self.print_method)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:187, in retina_figure(fig, base64, **kwargs)
    178 def retina_figure(fig, base64=False, **kwargs):
    179     """format a figure as a pixel-doubled (retina) PNG
    180 
    181     If `base64` is True, return base64-encoded str instead of raw bytes
   (...)    185         base64 argument
    186     """
--> 187     pngdata = print_figure(fig, fmt="retina", base64=False, **kwargs)
    188     # Make sure that retina_figure acts just like print_figure and returns
    189     # None when the figure is empty.
    190     if pngdata is None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:170, in print_figure(fig, fmt, bbox_inches, base64, **kwargs)
    167     from matplotlib.backend_bases import FigureCanvasBase
    168     FigureCanvasBase(fig)
--> 170 fig.canvas.print_figure(bytes_io, **kw)
    171 data = bytes_io.getvalue()
    172 if fmt == 'svg':

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:2157, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2154     # we do this instead of `self.figure.draw_without_rendering`
   2155     # so that we can inject the orientation
   2156     with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2157         self.figure.draw(renderer)
   2158 if bbox_inches:
   2159     if bbox_inches == "tight":

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:388, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    384     return
    386 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
    387                                  hspace=hspace, wspace=wspace)
--> 388 pos, bbox = get_pos_and_bbox(ax, renderer)
    389 # the margin is the distance between the bounding box of the Axes
    390 # and its position (plus the padding from above)
    391 margin['left'] += pos.x0 - bbox.x0

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4567, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4565         if ba:
   4566             bb.append(ba)
-> 4567 self._update_title_position(renderer)
   4568 axbbox = self.get_window_extent(renderer)
   4569 bb.append(axbbox)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:3134, in _AxesBase._update_title_position(self, renderer)
   3132 if title.get_text():
   3133     for ax in axs:
-> 3134         ax.yaxis.get_tightbbox(renderer)  # update offsetText
   3135         if ax.yaxis.offsetText.get_text():
   3136             bb = ax.yaxis.offsetText.get_tightbbox(renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1353, in Axis.get_tightbbox(self, renderer, for_layout_only)
   1350     renderer = self.get_figure(root=True)._get_renderer()
   1351 ticks_to_draw = self._update_ticks()
-> 1353 self._update_label_position(renderer)
   1355 # go back to just this axis's tick labels
   1356 tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2675, in YAxis._update_label_position(self, renderer)
   2671     return
   2673 # get bounding boxes for this axis and any siblings
   2674 # that have been set by `fig.align_ylabels()`
-> 2675 bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
   2676 x, y = self.label.get_position()
   2678 if self.label_position == 'left':
   2679     # Union with extents of the left spine if present, of the axes otherwise.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2241, in Axis._get_tick_boxes_siblings(self, renderer)
   2239 axis = ax._axis_map[name]
   2240 ticks_to_draw = axis._update_ticks()
-> 2241 tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer)
   2242 bboxes.extend(tlb)
   2243 bboxes2.extend(tlb2)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1332, in Axis._get_ticklabel_bboxes(self, ticks, renderer)
   1330 if renderer is None:
   1331     renderer = self.get_figure(root=True)._get_renderer()
-> 1332 return ([tick.label1.get_window_extent(renderer)
   1333          for tick in ticks if tick.label1.get_visible()],
   1334         [tick.label2.get_window_extent(renderer)
   1335          for tick in ticks if tick.label2.get_visible()])

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
    964     raise RuntimeError(
    965         "Cannot get window extent of text w/o renderer. You likely "
    966         "want to call 'figure.draw_without_rendering()' first.")
    968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969     bbox, info, descent = self._get_layout(self._renderer)
    970     x, y = self.get_unitless_position()
    971     x, y = self.get_transform().transform((x, y))

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
    370 ys = []
    372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
    374     renderer, "lp", self._fontproperties,
    375     ismath="TeX" if self.get_usetex() else False,
    376     dpi=self.get_figure(root=True).dpi)
    377 min_dy = (lp_h - lp_d) * self._linespacing
    379 for i, line in enumerate(lines):

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:367, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
--> 367     page, = dvi
    368 # A total height (including the descent) needs to be returned.
    369 return page.width, page.height + page.descent, page.descent

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:261, in Dvi.__iter__(self)
    245 def __iter__(self):
    246     """
    247     Iterate through the pages of the file.
    248 
   (...)    259         integers.
    260     """
--> 261     while self._read():
    262         yield self._output()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:343, in Dvi._read(self)
    341 self._dtable[byte](self, byte)
    342 if self._missing_font:
--> 343     raise self._missing_font.to_exception()
    344 name = self._dtable[byte].__name__
    345 if name == "_push":

FileNotFoundError: Matplotlib's TeX implementation searched for a file named 'cmss10.tfm' in your texmf tree, but could not find it
<Figure size 735x350 with 3 Axes>

Figure 9. Two multi-componnet isomorphic graphs, plotted using their coordinate information.

As far a graph theory is concerned, the two graphs above are equivalent. Because we use the coordinate information to plot these graphs, we can see an important distinction: the graph on the left is nested where the graph on the right is separated.

For filled contours, Figure 9. is a crude, but illustrative, example of a contour with an interior boundary. So far we only used the mesh connectivity information to create the unfilled contour boundaries, where coordinate information has only been used to plotting. But, for filled contours after creating the contour boundaries from the connectivity information, we’ll need to use coordinate info to recursively search of interior boundaries within the resulting contours.

We can efficiently recursively search for interior boundaries using shapely.STRtree, which uses the bounding boxes of each geometry to query for containment.

Relative Ordering#

One final matplotlib consideration, is the relative ordering (ie., clockwise vs counter-clockwise) of interior boundaries relative to their exterior boundaries.

In order for an interior boundary to be unfilled, as intended, it’s relative order must be opposite the relative order of the exterior boundary. For more information about this, refer to the matplotlib documentation: here.

Following the matplotlib documentation

Error in callback <function _draw_all_if_interactive at 0x7f4811a4ba00> (for post_execute), with arguments args (),kwargs {}:
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/pyplot.py:278, in _draw_all_if_interactive()
    276 def _draw_all_if_interactive() -> None:
    277     if matplotlib.is_interactive():
--> 278         draw_all()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_pylab_helpers.py:131, in Gcf.draw_all(cls, force)
    129 for manager in cls.get_all_fig_managers():
    130     if force or manager.canvas.figure.stale:
--> 131         manager.canvas.draw_idle()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:1893, in FigureCanvasBase.draw_idle(self, *args, **kwargs)
   1891 if not self._is_idle_drawing:
   1892     with self._idle_draw_cntx():
-> 1893         self.draw(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:382, in FigureCanvasAgg.draw(self)
    379 # Acquire a lock on the shared font cache.
    380 with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
    381       else nullcontext()):
--> 382     self.figure.draw(self.renderer)
    383     # A GUI class may be need to update a window using this draw, so
    384     # don't forget to call the superclass.
    385     super().draw()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:388, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    384     return
    386 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
    387                                  hspace=hspace, wspace=wspace)
--> 388 pos, bbox = get_pos_and_bbox(ax, renderer)
    389 # the margin is the distance between the bounding box of the Axes
    390 # and its position (plus the padding from above)
    391 margin['left'] += pos.x0 - bbox.x0

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4587, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4584     bbox_artists = self.get_default_bbox_extra_artists()
   4586 for a in bbox_artists:
-> 4587     bbox = a.get_tightbbox(renderer)
   4588     if (bbox is not None
   4589             and 0 < bbox.width < np.inf
   4590             and 0 < bbox.height < np.inf):
   4591         bb.append(bbox)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:2032, in Annotation.get_tightbbox(self, renderer)
   2030 if not self._check_xy(renderer):
   2031     return Bbox.null()
-> 2032 return super().get_tightbbox(renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:364, in Artist.get_tightbbox(self, renderer)
    348 def get_tightbbox(self, renderer=None):
    349     """
    350     Like `.Artist.get_window_extent`, but includes any clipping.
    351 
   (...)    362         Returns None if clipping results in no intersection.
    363     """
--> 364     bbox = self.get_window_extent(renderer)
    365     if self.get_clip_on():
    366         clip_box = self.get_clip_box()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:2020, in Annotation.get_window_extent(self, renderer)
   2016     raise RuntimeError('Cannot get window extent without renderer')
   2018 self.update_positions(self._renderer)
-> 2020 text_bbox = Text.get_window_extent(self)
   2021 bboxes = [text_bbox]
   2023 if self.arrow_patch is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
    964     raise RuntimeError(
    965         "Cannot get window extent of text w/o renderer. You likely "
    966         "want to call 'figure.draw_without_rendering()' first.")
    968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969     bbox, info, descent = self._get_layout(self._renderer)
    970     x, y = self.get_unitless_position()
    971     x, y = self.get_transform().transform((x, y))

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
    370 ys = []
    372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
    374     renderer, "lp", self._fontproperties,
    375     ismath="TeX" if self.get_usetex() else False,
    376     dpi=self.get_figure(root=True).dpi)
    377 min_dy = (lp_h - lp_d) * self._linespacing
    379 for i, line in enumerate(lines):

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:367, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
--> 367     page, = dvi
    368 # A total height (including the descent) needs to be returned.
    369 return page.width, page.height + page.descent, page.descent

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:261, in Dvi.__iter__(self)
    245 def __iter__(self):
    246     """
    247     Iterate through the pages of the file.
    248 
   (...)    259         integers.
    260     """
--> 261     while self._read():
    262         yield self._output()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:343, in Dvi._read(self)
    341 self._dtable[byte](self, byte)
    342 if self._missing_font:
--> 343     raise self._missing_font.to_exception()
    344 name = self._dtable[byte].__name__
    345 if name == "_push":

FileNotFoundError: Matplotlib's TeX implementation searched for a file named 'cmss10.tfm' in your texmf tree, but could not find it
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/formatters.py:402, in BaseFormatter.__call__(self, obj)
    400     pass
    401 else:
--> 402     return printer(obj)
    403 # Finally look for special method names
    404 method = get_real_method(obj, self.print_method)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:187, in retina_figure(fig, base64, **kwargs)
    178 def retina_figure(fig, base64=False, **kwargs):
    179     """format a figure as a pixel-doubled (retina) PNG
    180 
    181     If `base64` is True, return base64-encoded str instead of raw bytes
   (...)    185         base64 argument
    186     """
--> 187     pngdata = print_figure(fig, fmt="retina", base64=False, **kwargs)
    188     # Make sure that retina_figure acts just like print_figure and returns
    189     # None when the figure is empty.
    190     if pngdata is None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:170, in print_figure(fig, fmt, bbox_inches, base64, **kwargs)
    167     from matplotlib.backend_bases import FigureCanvasBase
    168     FigureCanvasBase(fig)
--> 170 fig.canvas.print_figure(bytes_io, **kw)
    171 data = bytes_io.getvalue()
    172 if fmt == 'svg':

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:2157, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2154     # we do this instead of `self.figure.draw_without_rendering`
   2155     # so that we can inject the orientation
   2156     with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2157         self.figure.draw(renderer)
   2158 if bbox_inches:
   2159     if bbox_inches == "tight":

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:388, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    384     return
    386 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
    387                                  hspace=hspace, wspace=wspace)
--> 388 pos, bbox = get_pos_and_bbox(ax, renderer)
    389 # the margin is the distance between the bounding box of the Axes
    390 # and its position (plus the padding from above)
    391 margin['left'] += pos.x0 - bbox.x0

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4587, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4584     bbox_artists = self.get_default_bbox_extra_artists()
   4586 for a in bbox_artists:
-> 4587     bbox = a.get_tightbbox(renderer)
   4588     if (bbox is not None
   4589             and 0 < bbox.width < np.inf
   4590             and 0 < bbox.height < np.inf):
   4591         bb.append(bbox)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:2032, in Annotation.get_tightbbox(self, renderer)
   2030 if not self._check_xy(renderer):
   2031     return Bbox.null()
-> 2032 return super().get_tightbbox(renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:364, in Artist.get_tightbbox(self, renderer)
    348 def get_tightbbox(self, renderer=None):
    349     """
    350     Like `.Artist.get_window_extent`, but includes any clipping.
    351 
   (...)    362         Returns None if clipping results in no intersection.
    363     """
--> 364     bbox = self.get_window_extent(renderer)
    365     if self.get_clip_on():
    366         clip_box = self.get_clip_box()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:2020, in Annotation.get_window_extent(self, renderer)
   2016     raise RuntimeError('Cannot get window extent without renderer')
   2018 self.update_positions(self._renderer)
-> 2020 text_bbox = Text.get_window_extent(self)
   2021 bboxes = [text_bbox]
   2023 if self.arrow_patch is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
    964     raise RuntimeError(
    965         "Cannot get window extent of text w/o renderer. You likely "
    966         "want to call 'figure.draw_without_rendering()' first.")
    968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969     bbox, info, descent = self._get_layout(self._renderer)
    970     x, y = self.get_unitless_position()
    971     x, y = self.get_transform().transform((x, y))

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
    370 ys = []
    372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
    374     renderer, "lp", self._fontproperties,
    375     ismath="TeX" if self.get_usetex() else False,
    376     dpi=self.get_figure(root=True).dpi)
    377 min_dy = (lp_h - lp_d) * self._linespacing
    379 for i, line in enumerate(lines):

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:367, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
--> 367     page, = dvi
    368 # A total height (including the descent) needs to be returned.
    369 return page.width, page.height + page.descent, page.descent

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:261, in Dvi.__iter__(self)
    245 def __iter__(self):
    246     """
    247     Iterate through the pages of the file.
    248 
   (...)    259         integers.
    260     """
--> 261     while self._read():
    262         yield self._output()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:343, in Dvi._read(self)
    341 self._dtable[byte](self, byte)
    342 if self._missing_font:
--> 343     raise self._missing_font.to_exception()
    344 name = self._dtable[byte].__name__
    345 if name == "_push":

FileNotFoundError: Matplotlib's TeX implementation searched for a file named 'cmss10.tfm' in your texmf tree, but could not find it
<Figure size 550x550 with 1 Axes>

Figure 10. Illustration of the importance of the interior vs. exterior relative ordering for rendering interior boundaries.

So, with above considerations in mind let’s return to the same sine field, evaluated on a \(100 \times 100\) cell mesh.

Error in callback <function _draw_all_if_interactive at 0x7f4811a4ba00> (for post_execute), with arguments args (),kwargs {}:
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/pyplot.py:278, in _draw_all_if_interactive()
    276 def _draw_all_if_interactive() -> None:
    277     if matplotlib.is_interactive():
--> 278         draw_all()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_pylab_helpers.py:131, in Gcf.draw_all(cls, force)
    129 for manager in cls.get_all_fig_managers():
    130     if force or manager.canvas.figure.stale:
--> 131         manager.canvas.draw_idle()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:1893, in FigureCanvasBase.draw_idle(self, *args, **kwargs)
   1891 if not self._is_idle_drawing:
   1892     with self._idle_draw_cntx():
-> 1893         self.draw(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:382, in FigureCanvasAgg.draw(self)
    379 # Acquire a lock on the shared font cache.
    380 with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
    381       else nullcontext()):
--> 382     self.figure.draw(self.renderer)
    383     # A GUI class may be need to update a window using this draw, so
    384     # don't forget to call the superclass.
    385     super().draw()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:405, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    403 cbp_rspan, cbp_cspan = get_cb_parent_spans(cbax)
    404 loc = cbax._colorbar_info['location']
--> 405 cbpos, cbbbox = get_pos_and_bbox(cbax, renderer)
    406 if loc == 'right':
    407     if cbp_cspan.stop == ss.colspan.stop:
    408         # only increase if the colorbar is on the right edge

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4564, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4562 for axis in self._axis_map.values():
   4563     if self.axison and axis.get_visible():
-> 4564         ba = martist._get_tightbbox_for_layout_only(axis, renderer)
   4565         if ba:
   4566             bb.append(ba)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1353, in Axis.get_tightbbox(self, renderer, for_layout_only)
   1350     renderer = self.get_figure(root=True)._get_renderer()
   1351 ticks_to_draw = self._update_ticks()
-> 1353 self._update_label_position(renderer)
   1355 # go back to just this axis's tick labels
   1356 tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2675, in YAxis._update_label_position(self, renderer)
   2671     return
   2673 # get bounding boxes for this axis and any siblings
   2674 # that have been set by `fig.align_ylabels()`
-> 2675 bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
   2676 x, y = self.label.get_position()
   2678 if self.label_position == 'left':
   2679     # Union with extents of the left spine if present, of the axes otherwise.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2241, in Axis._get_tick_boxes_siblings(self, renderer)
   2239 axis = ax._axis_map[name]
   2240 ticks_to_draw = axis._update_ticks()
-> 2241 tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer)
   2242 bboxes.extend(tlb)
   2243 bboxes2.extend(tlb2)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1334, in Axis._get_ticklabel_bboxes(self, ticks, renderer)
   1330 if renderer is None:
   1331     renderer = self.get_figure(root=True)._get_renderer()
   1332 return ([tick.label1.get_window_extent(renderer)
   1333          for tick in ticks if tick.label1.get_visible()],
-> 1334         [tick.label2.get_window_extent(renderer)
   1335          for tick in ticks if tick.label2.get_visible()])

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
    964     raise RuntimeError(
    965         "Cannot get window extent of text w/o renderer. You likely "
    966         "want to call 'figure.draw_without_rendering()' first.")
    968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969     bbox, info, descent = self._get_layout(self._renderer)
    970     x, y = self.get_unitless_position()
    971     x, y = self.get_transform().transform((x, y))

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
    370 ys = []
    372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
    374     renderer, "lp", self._fontproperties,
    375     ismath="TeX" if self.get_usetex() else False,
    376     dpi=self.get_figure(root=True).dpi)
    377 min_dy = (lp_h - lp_d) * self._linespacing
    379 for i, line in enumerate(lines):

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:367, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
--> 367     page, = dvi
    368 # A total height (including the descent) needs to be returned.
    369 return page.width, page.height + page.descent, page.descent

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:261, in Dvi.__iter__(self)
    245 def __iter__(self):
    246     """
    247     Iterate through the pages of the file.
    248 
   (...)    259         integers.
    260     """
--> 261     while self._read():
    262         yield self._output()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:343, in Dvi._read(self)
    341 self._dtable[byte](self, byte)
    342 if self._missing_font:
--> 343     raise self._missing_font.to_exception()
    344 name = self._dtable[byte].__name__
    345 if name == "_push":

FileNotFoundError: Matplotlib's TeX implementation searched for a file named 'cmss10.tfm' in your texmf tree, but could not find it
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/formatters.py:402, in BaseFormatter.__call__(self, obj)
    400     pass
    401 else:
--> 402     return printer(obj)
    403 # Finally look for special method names
    404 method = get_real_method(obj, self.print_method)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:187, in retina_figure(fig, base64, **kwargs)
    178 def retina_figure(fig, base64=False, **kwargs):
    179     """format a figure as a pixel-doubled (retina) PNG
    180 
    181     If `base64` is True, return base64-encoded str instead of raw bytes
   (...)    185         base64 argument
    186     """
--> 187     pngdata = print_figure(fig, fmt="retina", base64=False, **kwargs)
    188     # Make sure that retina_figure acts just like print_figure and returns
    189     # None when the figure is empty.
    190     if pngdata is None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:170, in print_figure(fig, fmt, bbox_inches, base64, **kwargs)
    167     from matplotlib.backend_bases import FigureCanvasBase
    168     FigureCanvasBase(fig)
--> 170 fig.canvas.print_figure(bytes_io, **kw)
    171 data = bytes_io.getvalue()
    172 if fmt == 'svg':

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:2157, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2154     # we do this instead of `self.figure.draw_without_rendering`
   2155     # so that we can inject the orientation
   2156     with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2157         self.figure.draw(renderer)
   2158 if bbox_inches:
   2159     if bbox_inches == "tight":

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:405, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    403 cbp_rspan, cbp_cspan = get_cb_parent_spans(cbax)
    404 loc = cbax._colorbar_info['location']
--> 405 cbpos, cbbbox = get_pos_and_bbox(cbax, renderer)
    406 if loc == 'right':
    407     if cbp_cspan.stop == ss.colspan.stop:
    408         # only increase if the colorbar is on the right edge

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4564, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4562 for axis in self._axis_map.values():
   4563     if self.axison and axis.get_visible():
-> 4564         ba = martist._get_tightbbox_for_layout_only(axis, renderer)
   4565         if ba:
   4566             bb.append(ba)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1353, in Axis.get_tightbbox(self, renderer, for_layout_only)
   1350     renderer = self.get_figure(root=True)._get_renderer()
   1351 ticks_to_draw = self._update_ticks()
-> 1353 self._update_label_position(renderer)
   1355 # go back to just this axis's tick labels
   1356 tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2675, in YAxis._update_label_position(self, renderer)
   2671     return
   2673 # get bounding boxes for this axis and any siblings
   2674 # that have been set by `fig.align_ylabels()`
-> 2675 bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
   2676 x, y = self.label.get_position()
   2678 if self.label_position == 'left':
   2679     # Union with extents of the left spine if present, of the axes otherwise.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2241, in Axis._get_tick_boxes_siblings(self, renderer)
   2239 axis = ax._axis_map[name]
   2240 ticks_to_draw = axis._update_ticks()
-> 2241 tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer)
   2242 bboxes.extend(tlb)
   2243 bboxes2.extend(tlb2)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1334, in Axis._get_ticklabel_bboxes(self, ticks, renderer)
   1330 if renderer is None:
   1331     renderer = self.get_figure(root=True)._get_renderer()
   1332 return ([tick.label1.get_window_extent(renderer)
   1333          for tick in ticks if tick.label1.get_visible()],
-> 1334         [tick.label2.get_window_extent(renderer)
   1335          for tick in ticks if tick.label2.get_visible()])

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
    964     raise RuntimeError(
    965         "Cannot get window extent of text w/o renderer. You likely "
    966         "want to call 'figure.draw_without_rendering()' first.")
    968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969     bbox, info, descent = self._get_layout(self._renderer)
    970     x, y = self.get_unitless_position()
    971     x, y = self.get_transform().transform((x, y))

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
    370 ys = []
    372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
    374     renderer, "lp", self._fontproperties,
    375     ismath="TeX" if self.get_usetex() else False,
    376     dpi=self.get_figure(root=True).dpi)
    377 min_dy = (lp_h - lp_d) * self._linespacing
    379 for i, line in enumerate(lines):

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:367, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
--> 367     page, = dvi
    368 # A total height (including the descent) needs to be returned.
    369 return page.width, page.height + page.descent, page.descent

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:261, in Dvi.__iter__(self)
    245 def __iter__(self):
    246     """
    247     Iterate through the pages of the file.
    248 
   (...)    259         integers.
    260     """
--> 261     while self._read():
    262         yield self._output()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:343, in Dvi._read(self)
    341 self._dtable[byte](self, byte)
    342 if self._missing_font:
--> 343     raise self._missing_font.to_exception()
    344 name = self._dtable[byte].__name__
    345 if name == "_push":

FileNotFoundError: Matplotlib's TeX implementation searched for a file named 'cmss10.tfm' in your texmf tree, but could not find it
<Figure size 350x350 with 2 Axes>

Figure 11. Checkerboard field, evaluated on a \(100 \times 100\) cell mesh.

Like filled contouring, our first step will be to create the boolean mask. Because this is a filled contour we will need both an upper and lower bound (cf., unfilled).

cell_mask = (field > -1.0) & (field < 0.0)

Figure 12. (Left) Boolean mask of \(-1.0 < f(x, y) < 0.0\) evaluated on the MPAS mesh. (Right) The same checkerboard field, evaluated and contoured on a regular quadrilateral mesh. The grey is the filled contour area.

As a first step, we will use the exact same steps as unfilled contours to extract and sort the contour boundaries using the connectivity information. One important difference between the filled (grey) and unfilled (blue lines) is that the filled contours follow the mesh boundary.

Figure 13. The contour boundaries identified just using connectivity information. Each unique contour component has it’s own color. Notice the pink and brown boundaries are really interior boundaries of the blue contour, but just using connectivity information alone is not enough to identify this.

We will now used the coordinate information to recursively search for containment of our contour components. If we find an interior boundary during this recursive search, we will also ensure it’s relative order is opposite it’s parent to ensure proper plotting in matplotlib once we will the contours.

Figure 14. The contour boundaries identified after recursively search for containment using the coordinate information. Notice the interior boundaries of the blue contour are now blue as we’d expect.

With all this in hand, we now have a fully functional filled contour algorthinm. Now let’s see it in practice and compare to our matplotlib regularly spaced reference.

Hide code cell source

fig, ax = double_pannel()

kwargs = {
    "levels": [-1.0, 0.0, 1.0],
    "colors": ["tab:blue", "tab:orange"],
    "linestyles": "-",
}

mosaic.polypcolor(ax[0], descriptor, field, alpha=1 / 3, ec="k", lw=1 / 5)
mosaic.contourf(
    ax[0],
    descriptor,
    field,
    **kwargs,
)

ax[1].pcolormesh(*rect_grid, rect_data, alpha=1 / 3, lw=1 / 5)
ax[1].contourf(*rect_grid, rect_data, **kwargs)

ax[0].set_title(r"\texttt{mosiac.contour}")
ax[1].set_title(r"\texttt{plt.contour}");
Error in callback <function _draw_all_if_interactive at 0x7f4811a4ba00> (for post_execute), with arguments args (),kwargs {}:
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/pyplot.py:278, in _draw_all_if_interactive()
    276 def _draw_all_if_interactive() -> None:
    277     if matplotlib.is_interactive():
--> 278         draw_all()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_pylab_helpers.py:131, in Gcf.draw_all(cls, force)
    129 for manager in cls.get_all_fig_managers():
    130     if force or manager.canvas.figure.stale:
--> 131         manager.canvas.draw_idle()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:1893, in FigureCanvasBase.draw_idle(self, *args, **kwargs)
   1891 if not self._is_idle_drawing:
   1892     with self._idle_draw_cntx():
-> 1893         self.draw(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:382, in FigureCanvasAgg.draw(self)
    379 # Acquire a lock on the shared font cache.
    380 with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
    381       else nullcontext()):
--> 382     self.figure.draw(self.renderer)
    383     # A GUI class may be need to update a window using this draw, so
    384     # don't forget to call the superclass.
    385     super().draw()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:388, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    384     return
    386 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
    387                                  hspace=hspace, wspace=wspace)
--> 388 pos, bbox = get_pos_and_bbox(ax, renderer)
    389 # the margin is the distance between the bounding box of the Axes
    390 # and its position (plus the padding from above)
    391 margin['left'] += pos.x0 - bbox.x0

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4567, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4565         if ba:
   4566             bb.append(ba)
-> 4567 self._update_title_position(renderer)
   4568 axbbox = self.get_window_extent(renderer)
   4569 bb.append(axbbox)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:3134, in _AxesBase._update_title_position(self, renderer)
   3132 if title.get_text():
   3133     for ax in axs:
-> 3134         ax.yaxis.get_tightbbox(renderer)  # update offsetText
   3135         if ax.yaxis.offsetText.get_text():
   3136             bb = ax.yaxis.offsetText.get_tightbbox(renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1353, in Axis.get_tightbbox(self, renderer, for_layout_only)
   1350     renderer = self.get_figure(root=True)._get_renderer()
   1351 ticks_to_draw = self._update_ticks()
-> 1353 self._update_label_position(renderer)
   1355 # go back to just this axis's tick labels
   1356 tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2675, in YAxis._update_label_position(self, renderer)
   2671     return
   2673 # get bounding boxes for this axis and any siblings
   2674 # that have been set by `fig.align_ylabels()`
-> 2675 bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
   2676 x, y = self.label.get_position()
   2678 if self.label_position == 'left':
   2679     # Union with extents of the left spine if present, of the axes otherwise.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2241, in Axis._get_tick_boxes_siblings(self, renderer)
   2239 axis = ax._axis_map[name]
   2240 ticks_to_draw = axis._update_ticks()
-> 2241 tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer)
   2242 bboxes.extend(tlb)
   2243 bboxes2.extend(tlb2)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1332, in Axis._get_ticklabel_bboxes(self, ticks, renderer)
   1330 if renderer is None:
   1331     renderer = self.get_figure(root=True)._get_renderer()
-> 1332 return ([tick.label1.get_window_extent(renderer)
   1333          for tick in ticks if tick.label1.get_visible()],
   1334         [tick.label2.get_window_extent(renderer)
   1335          for tick in ticks if tick.label2.get_visible()])

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
    964     raise RuntimeError(
    965         "Cannot get window extent of text w/o renderer. You likely "
    966         "want to call 'figure.draw_without_rendering()' first.")
    968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969     bbox, info, descent = self._get_layout(self._renderer)
    970     x, y = self.get_unitless_position()
    971     x, y = self.get_transform().transform((x, y))

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
    370 ys = []
    372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
    374     renderer, "lp", self._fontproperties,
    375     ismath="TeX" if self.get_usetex() else False,
    376     dpi=self.get_figure(root=True).dpi)
    377 min_dy = (lp_h - lp_d) * self._linespacing
    379 for i, line in enumerate(lines):

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:367, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
--> 367     page, = dvi
    368 # A total height (including the descent) needs to be returned.
    369 return page.width, page.height + page.descent, page.descent

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:261, in Dvi.__iter__(self)
    245 def __iter__(self):
    246     """
    247     Iterate through the pages of the file.
    248 
   (...)    259         integers.
    260     """
--> 261     while self._read():
    262         yield self._output()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:343, in Dvi._read(self)
    341 self._dtable[byte](self, byte)
    342 if self._missing_font:
--> 343     raise self._missing_font.to_exception()
    344 name = self._dtable[byte].__name__
    345 if name == "_push":

FileNotFoundError: Matplotlib's TeX implementation searched for a file named 'cmss10.tfm' in your texmf tree, but could not find it
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/formatters.py:402, in BaseFormatter.__call__(self, obj)
    400     pass
    401 else:
--> 402     return printer(obj)
    403 # Finally look for special method names
    404 method = get_real_method(obj, self.print_method)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:187, in retina_figure(fig, base64, **kwargs)
    178 def retina_figure(fig, base64=False, **kwargs):
    179     """format a figure as a pixel-doubled (retina) PNG
    180 
    181     If `base64` is True, return base64-encoded str instead of raw bytes
   (...)    185         base64 argument
    186     """
--> 187     pngdata = print_figure(fig, fmt="retina", base64=False, **kwargs)
    188     # Make sure that retina_figure acts just like print_figure and returns
    189     # None when the figure is empty.
    190     if pngdata is None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/IPython/core/pylabtools.py:170, in print_figure(fig, fmt, bbox_inches, base64, **kwargs)
    167     from matplotlib.backend_bases import FigureCanvasBase
    168     FigureCanvasBase(fig)
--> 170 fig.canvas.print_figure(bytes_io, **kw)
    171 data = bytes_io.getvalue()
    172 if fmt == 'svg':

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:2157, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2154     # we do this instead of `self.figure.draw_without_rendering`
   2155     # so that we can inject the orientation
   2156     with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2157         self.figure.draw(renderer)
   2158 if bbox_inches:
   2159     if bbox_inches == "tight":

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     92 @wraps(draw)
     93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94     result = draw(artist, renderer, *args, **kwargs)
     95     if renderer._rasterizing:
     96         renderer.stop_rasterizing()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     68     if artist.get_agg_filter() is not None:
     69         renderer.start_filter()
---> 71     return draw(artist, renderer)
     72 finally:
     73     if artist.get_agg_filter() is not None:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/figure.py:3251, in Figure.draw(self, renderer)
   3249 if self.axes and self.get_layout_engine() is not None:
   3250     try:
-> 3251         self.get_layout_engine().execute(self)
   3252     except ValueError:
   3253         pass

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/layout_engine.py:278, in ConstrainedLayoutEngine.execute(self, fig)
    275 w_pad = self._params['w_pad'] / width
    276 h_pad = self._params['h_pad'] / height
--> 278 return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
    279                              wspace=self._params['wspace'],
    280                              hspace=self._params['hspace'],
    281                              rect=self._params['rect'],
    282                              compress=self._compress)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:116, in do_constrained_layout(fig, h_pad, w_pad, hspace, wspace, rect, compress)
    106     return
    108 for _ in range(2):
    109     # do the algorithm twice.  This has to be done because decorations
    110     # change size after the first re-position (i.e. x/yticklabels get
   (...)    114     # make margins for all the Axes and subfigures in the
    115     # figure.  Add margins for colorbars...
--> 116     make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
    117                         w_pad=w_pad, hspace=hspace, wspace=wspace)
    118     make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
    119                           w_pad=w_pad)
    121     # if a layout is such that a columns (or rows) margin has no
    122     # constraints, we need to make all such instances in the grid
    123     # match in margin size.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:388, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    384     return
    386 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
    387                                  hspace=hspace, wspace=wspace)
--> 388 pos, bbox = get_pos_and_bbox(ax, renderer)
    389 # the margin is the distance between the bounding box of the Axes
    390 # and its position (plus the padding from above)
    391 margin['left'] += pos.x0 - bbox.x0

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:645, in get_pos_and_bbox(ax, renderer)
    643 # pos is in panel co-ords, but we need in figure for the layout
    644 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 645 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    646 if tightbbox is None:
    647     bbox = pos

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1396 """
   1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1399 when encountering third-party subclasses that do not support it.
   1400 """
   1401 try:
-> 1402     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1403 except TypeError:
   1404     return obj.get_tightbbox(*args, **kwargs)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:4567, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4565         if ba:
   4566             bb.append(ba)
-> 4567 self._update_title_position(renderer)
   4568 axbbox = self.get_window_extent(renderer)
   4569 bb.append(axbbox)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axes/_base.py:3134, in _AxesBase._update_title_position(self, renderer)
   3132 if title.get_text():
   3133     for ax in axs:
-> 3134         ax.yaxis.get_tightbbox(renderer)  # update offsetText
   3135         if ax.yaxis.offsetText.get_text():
   3136             bb = ax.yaxis.offsetText.get_tightbbox(renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1353, in Axis.get_tightbbox(self, renderer, for_layout_only)
   1350     renderer = self.get_figure(root=True)._get_renderer()
   1351 ticks_to_draw = self._update_ticks()
-> 1353 self._update_label_position(renderer)
   1355 # go back to just this axis's tick labels
   1356 tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2675, in YAxis._update_label_position(self, renderer)
   2671     return
   2673 # get bounding boxes for this axis and any siblings
   2674 # that have been set by `fig.align_ylabels()`
-> 2675 bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
   2676 x, y = self.label.get_position()
   2678 if self.label_position == 'left':
   2679     # Union with extents of the left spine if present, of the axes otherwise.

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:2241, in Axis._get_tick_boxes_siblings(self, renderer)
   2239 axis = ax._axis_map[name]
   2240 ticks_to_draw = axis._update_ticks()
-> 2241 tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer)
   2242 bboxes.extend(tlb)
   2243 bboxes2.extend(tlb2)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/axis.py:1332, in Axis._get_ticklabel_bboxes(self, ticks, renderer)
   1330 if renderer is None:
   1331     renderer = self.get_figure(root=True)._get_renderer()
-> 1332 return ([tick.label1.get_window_extent(renderer)
   1333          for tick in ticks if tick.label1.get_visible()],
   1334         [tick.label2.get_window_extent(renderer)
   1335          for tick in ticks if tick.label2.get_visible()])

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
    964     raise RuntimeError(
    965         "Cannot get window extent of text w/o renderer. You likely "
    966         "want to call 'figure.draw_without_rendering()' first.")
    968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969     bbox, info, descent = self._get_layout(self._renderer)
    970     x, y = self.get_unitless_position()
    971     x, y = self.get_transform().transform((x, y))

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
    370 ys = []
    372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
    374     renderer, "lp", self._fontproperties,
    375     ismath="TeX" if self.get_usetex() else False,
    376     dpi=self.get_figure(root=True).dpi)
    377 min_dy = (lp_h - lp_d) * self._linespacing
    379 for i, line in enumerate(lines):

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
     67 # Cached based on a copy of fontprop so that later in-place mutations of
     68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
     70     weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
     73 @functools.lru_cache(4096)
     74 def _get_text_metrics_with_cache_impl(
     75         renderer_ref, text, fontprop, ismath, dpi):
     76     # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77     return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    209 _api.check_in_list(["TeX", True, False], ismath=ismath)
    210 if ismath == "TeX":
--> 211     return super().get_text_width_height_descent(s, prop, ismath)
    213 if ismath:
    214     ox, oy, width, height, descent, font_image = \
    215         self.mathtext_parser.parse(s, self.dpi, prop)

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    562 fontsize = prop.get_size_in_points()
    564 if ismath == 'TeX':
    565     # todo: handle properties
--> 566     return self.get_texmanager().get_text_width_height_descent(
    567         s, fontsize, renderer=self)
    569 dpi = self.points_to_pixels(72)
    570 if ismath:

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/texmanager.py:367, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    365 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    366 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
--> 367     page, = dvi
    368 # A total height (including the descent) needs to be returned.
    369 return page.width, page.height + page.descent, page.descent

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:261, in Dvi.__iter__(self)
    245 def __iter__(self):
    246     """
    247     Iterate through the pages of the file.
    248 
   (...)    259         integers.
    260     """
--> 261     while self._read():
    262         yield self._output()

File ~/miniforge3/envs/mosaic-dev/lib/python3.14/site-packages/matplotlib/dviread.py:343, in Dvi._read(self)
    341 self._dtable[byte](self, byte)
    342 if self._missing_font:
--> 343     raise self._missing_font.to_exception()
    344 name = self._dtable[byte].__name__
    345 if name == "_push":

FileNotFoundError: Matplotlib's TeX implementation searched for a file named 'cmss10.tfm' in your texmf tree, but could not find it
<Figure size 735x350 with 3 Axes>

Figure 15. mosaic.contourf result for filled contour levels of \(-1, 0, 1\) of the checkerboard field, evaluated on the MPAS mesh. (Right) plt.contourf of the same checkerboard field on a regular quadrilateral grid for comparison.

As we can see above, we also get good agreement between our mosaic.contourf implementation and the plt.contourf result. Again, the major difference is the jagged appearance of the MPAS contours due to the coarse resolution of the mesh, but this is desired feature of implementation.

Future Work#

Smooth (interpolated) contours:#

  • https://www.boristhebrave.com/2018/04/15/marching-cubes-tutorial/

  • https://www.cs.wustl.edu/~taoju/cse554/lectures/lect04_Contouring_I.pdf

  • https://www.cse.wustl.edu/~taoju/research/dualContour.pdf

  • https://en.wikipedia.org/wiki/Marching_squares

Marching Squares implementation:#

  • skimage has a cython based marching squares implementation that we could probably easily port to marching triangles.

  • https://github.com/scikit-image/scikit-image/blob/main/src/skimage/measure/_find_contours_cy.pyx