Analysing Samples

Download this as a Jupyter notebook

This guide covers how to use pyXla to conduct landscape analysis.

The first step is to load a sample:

from pyxla.sampling import HilbertCurveSampler
from pyxla import load_data
sampler = HilbertCurveSampler(
    sample_size=100, dim=2, return_neighbourhood=True
)

sample = {
    "X": sampler,
    "F": lambda x: (x**2).sum(),
    "V": lambda x: ((x**2).sum() - 3),
    "D": lambda x1, x2: ((x1 - x2)**2).sum(), #must take a pair of arguments and real number
    # N file is provided by the sampler since return_neighbourhood=True
}

load_data(sample)

The next step, is to import the explainable landscape analysis (xLA) feature of choice. For instance we import the distr_f (distribution of objective values) below:

from pyxla import distr_f

Lastly, invoke the imported feature.

feat, plot = distr_f(sample)
Error in callback <function _draw_all_if_interactive at 0x786ba5423110> (for post_execute), with arguments args (),kwargs {}:
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File /usr/local/lib/python3.14/site-packages/matplotlib/texmanager.py:258, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
    257 try:
--> 258     report = subprocess.check_output(
    259         command, cwd=cwd if cwd is not None else cls._cache_dir,
    260         stderr=subprocess.STDOUT)
    261 except subprocess.CalledProcessError as exc:

File /usr/local/lib/python3.14/subprocess.py:473, in check_output(timeout, *popenargs, **kwargs)
    471     kwargs['input'] = empty
--> 473 return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
    474            **kwargs).stdout

File /usr/local/lib/python3.14/subprocess.py:555, in run(input, capture_output, timeout, check, *popenargs, **kwargs)
    553     kwargs['stderr'] = PIPE
--> 555 with Popen(*popenargs, **kwargs) as process:
    556     try:

File /usr/local/lib/python3.14/subprocess.py:1039, 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)
   1036             self.stderr = io.TextIOWrapper(self.stderr,
   1037                     encoding=encoding, errors=errors)
-> 1039     self._execute_child(args, executable, preexec_fn, close_fds,
   1040                         pass_fds, cwd, env,
   1041                         startupinfo, creationflags, shell,
   1042                         p2cread, p2cwrite,
   1043                         c2pread, c2pwrite,
   1044                         errread, errwrite,
   1045                         restore_signals,
   1046                         gid, gids, uid, umask,
   1047                         start_new_session, process_group)
   1048 except:
   1049     # Cleanup if the child failed starting.

File /usr/local/lib/python3.14/subprocess.py:1990, 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)
   1989 if err_filename is not None:
-> 1990     raise child_exception_type(errno_num, err_msg, err_filename)
   1991 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 /usr/local/lib/python3.14/site-packages/matplotlib/pyplot.py:300, in _draw_all_if_interactive()
    298 def _draw_all_if_interactive() -> None:
    299     if matplotlib.is_interactive():
--> 300         draw_all()

File /usr/local/lib/python3.14/site-packages/matplotlib/_pylab_helpers.py:133, in Gcf.draw_all(cls, force)
    131 for manager in cls.get_all_fig_managers():
    132     if force or manager.canvas.figure.stale:
--> 133         manager.canvas.draw_idle()

File /usr/local/lib/python3.14/site-packages/matplotlib/backend_bases.py:1989, in FigureCanvasBase.draw_idle(self, *args, **kwargs)
   1987 if not self._is_idle_drawing:
   1988     with self._idle_draw_cntx():
-> 1989         self.draw(*args, **kwargs)

File /usr/local/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:438, in FigureCanvasAgg.draw(self)
    435 # Acquire a lock on the shared font cache.
    436 with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
    437       else nullcontext()):
--> 438     self.figure.draw(self.renderer)
    439     # A GUI class may be need to update a window using this draw, so
    440     # don't forget to call the superclass.
    441     super().draw()

File /usr/local/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 /usr/local/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 /usr/local/lib/python3.14/site-packages/matplotlib/figure.py:3276, in Figure.draw(self, renderer)
   3274 if self.axes and self.get_layout_engine() is not None:
   3275     try:
-> 3276         self.get_layout_engine().execute(self)
   3277     except ValueError:
   3278         pass

File /usr/local/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 /usr/local/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 /usr/local/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:389, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    385     return
    387 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
    388                                  hspace=hspace, wspace=wspace)
--> 389 pos, bbox = get_pos_and_bbox(ax, renderer)
    390 # the margin is the distance between the bounding box of the Axes
    391 # and its position (plus the padding from above)
    392 margin['left'] += pos.x0 - bbox.x0

File /usr/local/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:637, in get_pos_and_bbox(ax, renderer)
    635 # pos is in panel co-ords, but we need in figure for the layout
    636 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 637 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    638 if tightbbox is None:
    639     bbox = pos

File /usr/local/lib/python3.14/site-packages/matplotlib/artist.py:1485, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1479 """
   1480 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1481 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1482 when encountering third-party subclasses that do not support it.
   1483 """
   1484 try:
-> 1485     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1486 except TypeError:
   1487     return obj.get_tightbbox(*args, **kwargs)

File /usr/local/lib/python3.14/site-packages/matplotlib/axes/_base.py:4758, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4756 if self.axison and axis.get_visible():
   4757     if for_layout_only:
-> 4758         ba = martist._get_tightbbox_for_layout_only(axis, renderer)
   4759     else:
   4760         ba = axis.get_tightbbox(renderer)

File /usr/local/lib/python3.14/site-packages/matplotlib/artist.py:1485, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1479 """
   1480 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1481 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1482 when encountering third-party subclasses that do not support it.
   1483 """
   1484 try:
-> 1485     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1486 except TypeError:
   1487     return obj.get_tightbbox(*args, **kwargs)

File /usr/local/lib/python3.14/site-packages/matplotlib/axis.py:1427, in Axis.get_tightbbox(self, renderer, for_layout_only)
   1424     renderer = self.get_figure(root=True)._get_renderer()
   1425 ticks_to_draw = self._update_ticks()
-> 1427 self._update_label_position(renderer)
   1429 # go back to just this axis's tick labels
   1430 tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer)

File /usr/local/lib/python3.14/site-packages/matplotlib/axis.py:2615, in XAxis._update_label_position(self, renderer)
   2611     return
   2613 # get bounding boxes for this axis and any siblings
   2614 # that have been set by `fig.align_xlabels()`
-> 2615 bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
   2616 x, y = self.label.get_position()
   2618 if self.label_position == 'bottom':
   2619     # Union with extents of the bottom spine if present, of the axes otherwise.

File /usr/local/lib/python3.14/site-packages/matplotlib/axis.py:2403, in Axis._get_tick_boxes_siblings(self, renderer)
   2401 axis = ax._axis_map[name]
   2402 ticks_to_draw = axis._update_ticks()
-> 2403 tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer)
   2404 bboxes.extend(tlb)
   2405 bboxes2.extend(tlb2)

File /usr/local/lib/python3.14/site-packages/matplotlib/axis.py:1404, in Axis._get_ticklabel_bboxes(self, ticks, renderer)
   1402 def _get_ticklabel_bboxes(self, ticks, renderer):
   1403     """Return lists of bboxes for ticks' label1's and label2's."""
-> 1404     return ([tick.label1.get_window_extent(renderer)
   1405              for tick in ticks
   1406              if tick.label1.get_visible() and tick.label1.get_in_layout()],
   1407             [tick.label2.get_window_extent(renderer)
   1408              for tick in ticks
   1409              if tick.label2.get_visible() and tick.label2.get_in_layout()])

File /usr/local/lib/python3.14/site-packages/matplotlib/text.py:1076, in Text.get_window_extent(self, renderer, dpi)
   1071     raise RuntimeError(
   1072         "Cannot get window extent of text w/o renderer. You likely "
   1073         "want to call 'figure.draw_without_rendering()' first.")
   1075 with cbook._setattr_cm(fig, dpi=dpi):
-> 1076     bbox, _, _ = self._get_layout(self._renderer)
   1077     x, y = self.get_unitless_position()
   1078     x, y = self.get_transform().transform((x, y))

File /usr/local/lib/python3.14/site-packages/matplotlib/text.py:465, in Text._get_layout(self, renderer)
    462             break
    463 if None in (min_ascent, min_descent):
    464     # Fallback to font measurement.
--> 465     _, h, min_descent = _get_text_metrics_with_cache(
    466         renderer, "lp", self._fontproperties,
    467         ismath="TeX" if self.get_usetex() else False,
    468         dpi=dpi)
    469     min_ascent = h - min_descent
    470     line_gap = 0

File /usr/local/lib/python3.14/site-packages/matplotlib/text.py:59, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     52 get_text_metrics = _get_text_metrics_function(renderer)
     53 # call the function to compute the metrics and return
     54 #
     55 # We pass a copy of the fontprop because FontProperties is both mutable and
     56 # has a `__hash__` that depends on that mutable state.  This is not ideal
     57 # as it means the hash of an object is not stable over time which leads to
     58 # very confusing behavior when used as keys in dictionaries or hashes.
---> 59 return get_text_metrics(text, fontprop.copy(), ismath, dpi)

File /usr/local/lib/python3.14/site-packages/matplotlib/text.py:125, in _get_text_metrics_function.<locals>._text_metrics(text, fontprop, ismath, dpi)
    120     raise RuntimeError(
    121         "Trying to get text metrics for a renderer that no longer exists.  "
    122         "This should never happen and is evidence of a bug elsewhere."
    123         )
    124 # do the actual method call we need and return the result
--> 125 return local_renderer.get_text_width_height_descent(text, fontprop, ismath)

File /usr/local/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:256, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    254 _api.check_in_list(["TeX", True, False], ismath=ismath)
    255 if ismath == "TeX":
--> 256     return super().get_text_width_height_descent(s, prop, ismath)
    258 if ismath:
    259     parse = self.mathtext_parser.parse(s, self.dpi, prop)

File /usr/local/lib/python3.14/site-packages/matplotlib/backend_bases.py:589, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    585 fontsize = prop.get_size_in_points()
    587 if ismath == 'TeX':
    588     # todo: handle properties
--> 589     return self.get_texmanager().get_text_width_height_descent(
    590         s, fontsize, renderer=self)
    592 dpi = self.points_to_pixels(72)
    593 if ismath:

File /usr/local/lib/python3.14/site-packages/matplotlib/texmanager.py:369, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    367 if tex.strip() == '':
    368     return 0, 0, 0
--> 369 dvipath = cls.make_dvi(tex, fontsize)
    370 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    371 with dviread.Dvi(dvipath, 72 * dpi_fraction) as dvi:

File /usr/local/lib/python3.14/site-packages/matplotlib/texmanager.py:301, in TexManager.make_dvi(cls, tex, fontsize)
    298 with TemporaryDirectory(dir=dvipath.parent) as tmpdir:
    299     Path(tmpdir, "file.tex").write_text(
    300         cls._get_tex_source(tex, fontsize), encoding='utf-8')
--> 301     cls._run_checked_subprocess(
    302         ["latex", "-interaction=nonstopmode", "-halt-on-error",
    303          "-no-shell-escape", "file.tex"], tex, cwd=tmpdir)
    304     Path(tmpdir, "file.dvi").replace(dvipath)
    305     # Also move the tex source to the main cache directory, but
    306     # only for backcompat.

File /usr/local/lib/python3.14/site-packages/matplotlib/texmanager.py:274, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
    262     raise RuntimeError(
    263         '{prog} was not able to process the following string:\n'
    264         '{tex!r}\n\n'
   (...)    271             exc=exc.output.decode('utf-8', 'backslashreplace'))
    272         ) from None
    273 except (FileNotFoundError, OSError) as exc:
--> 274     raise RuntimeError(
    275         f'Failed to process string with tex because {command[0]} '
    276         'could not be found') from exc
    277 _log.debug(report)
    278 return report

RuntimeError: Failed to process string with tex because latex could not be found
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File /usr/local/lib/python3.14/site-packages/matplotlib/texmanager.py:258, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
    257 try:
--> 258     report = subprocess.check_output(
    259         command, cwd=cwd if cwd is not None else cls._cache_dir,
    260         stderr=subprocess.STDOUT)
    261 except subprocess.CalledProcessError as exc:

File /usr/local/lib/python3.14/subprocess.py:473, in check_output(timeout, *popenargs, **kwargs)
    471     kwargs['input'] = empty
--> 473 return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
    474            **kwargs).stdout

File /usr/local/lib/python3.14/subprocess.py:555, in run(input, capture_output, timeout, check, *popenargs, **kwargs)
    553     kwargs['stderr'] = PIPE
--> 555 with Popen(*popenargs, **kwargs) as process:
    556     try:

File /usr/local/lib/python3.14/subprocess.py:1039, 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)
   1036             self.stderr = io.TextIOWrapper(self.stderr,
   1037                     encoding=encoding, errors=errors)
-> 1039     self._execute_child(args, executable, preexec_fn, close_fds,
   1040                         pass_fds, cwd, env,
   1041                         startupinfo, creationflags, shell,
   1042                         p2cread, p2cwrite,
   1043                         c2pread, c2pwrite,
   1044                         errread, errwrite,
   1045                         restore_signals,
   1046                         gid, gids, uid, umask,
   1047                         start_new_session, process_group)
   1048 except:
   1049     # Cleanup if the child failed starting.

File /usr/local/lib/python3.14/subprocess.py:1990, 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)
   1989 if err_filename is not None:
-> 1990     raise child_exception_type(errno_num, err_msg, err_filename)
   1991 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 /usr/local/lib/python3.14/site-packages/matplotlib/backend_bases.py:2257, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2254     # we do this instead of `self.figure.draw_without_rendering`
   2255     # so that we can inject the orientation
   2256     with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2257         self.figure.draw(renderer)
   2258 else:
   2259     renderer = None

File /usr/local/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 /usr/local/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 /usr/local/lib/python3.14/site-packages/matplotlib/figure.py:3276, in Figure.draw(self, renderer)
   3274 if self.axes and self.get_layout_engine() is not None:
   3275     try:
-> 3276         self.get_layout_engine().execute(self)
   3277     except ValueError:
   3278         pass

File /usr/local/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 /usr/local/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 /usr/local/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:389, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    385     return
    387 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
    388                                  hspace=hspace, wspace=wspace)
--> 389 pos, bbox = get_pos_and_bbox(ax, renderer)
    390 # the margin is the distance between the bounding box of the Axes
    391 # and its position (plus the padding from above)
    392 margin['left'] += pos.x0 - bbox.x0

File /usr/local/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:637, in get_pos_and_bbox(ax, renderer)
    635 # pos is in panel co-ords, but we need in figure for the layout
    636 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 637 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    638 if tightbbox is None:
    639     bbox = pos

File /usr/local/lib/python3.14/site-packages/matplotlib/artist.py:1485, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1479 """
   1480 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1481 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1482 when encountering third-party subclasses that do not support it.
   1483 """
   1484 try:
-> 1485     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1486 except TypeError:
   1487     return obj.get_tightbbox(*args, **kwargs)

File /usr/local/lib/python3.14/site-packages/matplotlib/axes/_base.py:4758, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4756 if self.axison and axis.get_visible():
   4757     if for_layout_only:
-> 4758         ba = martist._get_tightbbox_for_layout_only(axis, renderer)
   4759     else:
   4760         ba = axis.get_tightbbox(renderer)

File /usr/local/lib/python3.14/site-packages/matplotlib/artist.py:1485, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1479 """
   1480 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1481 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1482 when encountering third-party subclasses that do not support it.
   1483 """
   1484 try:
-> 1485     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1486 except TypeError:
   1487     return obj.get_tightbbox(*args, **kwargs)

File /usr/local/lib/python3.14/site-packages/matplotlib/axis.py:1427, in Axis.get_tightbbox(self, renderer, for_layout_only)
   1424     renderer = self.get_figure(root=True)._get_renderer()
   1425 ticks_to_draw = self._update_ticks()
-> 1427 self._update_label_position(renderer)
   1429 # go back to just this axis's tick labels
   1430 tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer)

File /usr/local/lib/python3.14/site-packages/matplotlib/axis.py:2615, in XAxis._update_label_position(self, renderer)
   2611     return
   2613 # get bounding boxes for this axis and any siblings
   2614 # that have been set by `fig.align_xlabels()`
-> 2615 bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
   2616 x, y = self.label.get_position()
   2618 if self.label_position == 'bottom':
   2619     # Union with extents of the bottom spine if present, of the axes otherwise.

File /usr/local/lib/python3.14/site-packages/matplotlib/axis.py:2403, in Axis._get_tick_boxes_siblings(self, renderer)
   2401 axis = ax._axis_map[name]
   2402 ticks_to_draw = axis._update_ticks()
-> 2403 tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer)
   2404 bboxes.extend(tlb)
   2405 bboxes2.extend(tlb2)

File /usr/local/lib/python3.14/site-packages/matplotlib/axis.py:1404, in Axis._get_ticklabel_bboxes(self, ticks, renderer)
   1402 def _get_ticklabel_bboxes(self, ticks, renderer):
   1403     """Return lists of bboxes for ticks' label1's and label2's."""
-> 1404     return ([tick.label1.get_window_extent(renderer)
   1405              for tick in ticks
   1406              if tick.label1.get_visible() and tick.label1.get_in_layout()],
   1407             [tick.label2.get_window_extent(renderer)
   1408              for tick in ticks
   1409              if tick.label2.get_visible() and tick.label2.get_in_layout()])

File /usr/local/lib/python3.14/site-packages/matplotlib/text.py:1076, in Text.get_window_extent(self, renderer, dpi)
   1071     raise RuntimeError(
   1072         "Cannot get window extent of text w/o renderer. You likely "
   1073         "want to call 'figure.draw_without_rendering()' first.")
   1075 with cbook._setattr_cm(fig, dpi=dpi):
-> 1076     bbox, _, _ = self._get_layout(self._renderer)
   1077     x, y = self.get_unitless_position()
   1078     x, y = self.get_transform().transform((x, y))

File /usr/local/lib/python3.14/site-packages/matplotlib/text.py:465, in Text._get_layout(self, renderer)
    462             break
    463 if None in (min_ascent, min_descent):
    464     # Fallback to font measurement.
--> 465     _, h, min_descent = _get_text_metrics_with_cache(
    466         renderer, "lp", self._fontproperties,
    467         ismath="TeX" if self.get_usetex() else False,
    468         dpi=dpi)
    469     min_ascent = h - min_descent
    470     line_gap = 0

File /usr/local/lib/python3.14/site-packages/matplotlib/text.py:59, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     52 get_text_metrics = _get_text_metrics_function(renderer)
     53 # call the function to compute the metrics and return
     54 #
     55 # We pass a copy of the fontprop because FontProperties is both mutable and
     56 # has a `__hash__` that depends on that mutable state.  This is not ideal
     57 # as it means the hash of an object is not stable over time which leads to
     58 # very confusing behavior when used as keys in dictionaries or hashes.
---> 59 return get_text_metrics(text, fontprop.copy(), ismath, dpi)

File /usr/local/lib/python3.14/site-packages/matplotlib/text.py:125, in _get_text_metrics_function.<locals>._text_metrics(text, fontprop, ismath, dpi)
    120     raise RuntimeError(
    121         "Trying to get text metrics for a renderer that no longer exists.  "
    122         "This should never happen and is evidence of a bug elsewhere."
    123         )
    124 # do the actual method call we need and return the result
--> 125 return local_renderer.get_text_width_height_descent(text, fontprop, ismath)

File /usr/local/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:256, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    254 _api.check_in_list(["TeX", True, False], ismath=ismath)
    255 if ismath == "TeX":
--> 256     return super().get_text_width_height_descent(s, prop, ismath)
    258 if ismath:
    259     parse = self.mathtext_parser.parse(s, self.dpi, prop)

File /usr/local/lib/python3.14/site-packages/matplotlib/backend_bases.py:589, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    585 fontsize = prop.get_size_in_points()
    587 if ismath == 'TeX':
    588     # todo: handle properties
--> 589     return self.get_texmanager().get_text_width_height_descent(
    590         s, fontsize, renderer=self)
    592 dpi = self.points_to_pixels(72)
    593 if ismath:

File /usr/local/lib/python3.14/site-packages/matplotlib/texmanager.py:369, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    367 if tex.strip() == '':
    368     return 0, 0, 0
--> 369 dvipath = cls.make_dvi(tex, fontsize)
    370 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    371 with dviread.Dvi(dvipath, 72 * dpi_fraction) as dvi:

File /usr/local/lib/python3.14/site-packages/matplotlib/texmanager.py:301, in TexManager.make_dvi(cls, tex, fontsize)
    298 with TemporaryDirectory(dir=dvipath.parent) as tmpdir:
    299     Path(tmpdir, "file.tex").write_text(
    300         cls._get_tex_source(tex, fontsize), encoding='utf-8')
--> 301     cls._run_checked_subprocess(
    302         ["latex", "-interaction=nonstopmode", "-halt-on-error",
    303          "-no-shell-escape", "file.tex"], tex, cwd=tmpdir)
    304     Path(tmpdir, "file.dvi").replace(dvipath)
    305     # Also move the tex source to the main cache directory, but
    306     # only for backcompat.

File /usr/local/lib/python3.14/site-packages/matplotlib/texmanager.py:274, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
    262     raise RuntimeError(
    263         '{prog} was not able to process the following string:\n'
    264         '{tex!r}\n\n'
   (...)    271             exc=exc.output.decode('utf-8', 'backslashreplace'))
    272         ) from None
    273 except (FileNotFoundError, OSError) as exc:
--> 274     raise RuntimeError(
    275         f'Failed to process string with tex because {command[0]} '
    276         'could not be found') from exc
    277 _log.debug(report)
    278 return report

RuntimeError: Failed to process string with tex because latex could not be found
<Figure size 300x600 with 2 Axes>

Each xLA feature in pyXla produces as output both visual and numerical outputs. For most features these outputs are returned as a pair.

The numerical output is the first item in the pair:

feat
{'f0_min': 70.48654206472348,
 'f0_max': 17250.503688665594,
 'f0_mean': 6984.475586503501,
 'f0_med': 6386.739212703842,
 'f0_q1': 3617.790500534753,
 'f0_q3': 9706.684508771683,
 'f0_sd': 4392.63164992962,
 'f0_skew': 0.42039306093321,
 'f0_kurt': -0.6140907651652197,
 'f0_rank_min': 1,
 'f0_rank_max': 100,
 'f0_rank_mean': 50.5,
 'f0_rank_med': 50.5,
 'f0_rank_q1': 25.25,
 'f0_rank_q3': 75.75,
 'f0_rank_sd': 29.011491975882016,
 'f0_rank_skew': 0.0,
 'f0_rank_kurt': -1.2002400240024003}

The visual output is the second item in the pair:

plot
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File /usr/local/lib/python3.14/site-packages/matplotlib/texmanager.py:258, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
    257 try:
--> 258     report = subprocess.check_output(
    259         command, cwd=cwd if cwd is not None else cls._cache_dir,
    260         stderr=subprocess.STDOUT)
    261 except subprocess.CalledProcessError as exc:

File /usr/local/lib/python3.14/subprocess.py:473, in check_output(timeout, *popenargs, **kwargs)
    471     kwargs['input'] = empty
--> 473 return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
    474            **kwargs).stdout

File /usr/local/lib/python3.14/subprocess.py:555, in run(input, capture_output, timeout, check, *popenargs, **kwargs)
    553     kwargs['stderr'] = PIPE
--> 555 with Popen(*popenargs, **kwargs) as process:
    556     try:

File /usr/local/lib/python3.14/subprocess.py:1039, 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)
   1036             self.stderr = io.TextIOWrapper(self.stderr,
   1037                     encoding=encoding, errors=errors)
-> 1039     self._execute_child(args, executable, preexec_fn, close_fds,
   1040                         pass_fds, cwd, env,
   1041                         startupinfo, creationflags, shell,
   1042                         p2cread, p2cwrite,
   1043                         c2pread, c2pwrite,
   1044                         errread, errwrite,
   1045                         restore_signals,
   1046                         gid, gids, uid, umask,
   1047                         start_new_session, process_group)
   1048 except:
   1049     # Cleanup if the child failed starting.

File /usr/local/lib/python3.14/subprocess.py:1990, 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)
   1989 if err_filename is not None:
-> 1990     raise child_exception_type(errno_num, err_msg, err_filename)
   1991 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 /usr/local/lib/python3.14/site-packages/matplotlib/backend_bases.py:2257, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2254     # we do this instead of `self.figure.draw_without_rendering`
   2255     # so that we can inject the orientation
   2256     with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2257         self.figure.draw(renderer)
   2258 else:
   2259     renderer = None

File /usr/local/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 /usr/local/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 /usr/local/lib/python3.14/site-packages/matplotlib/figure.py:3276, in Figure.draw(self, renderer)
   3274 if self.axes and self.get_layout_engine() is not None:
   3275     try:
-> 3276         self.get_layout_engine().execute(self)
   3277     except ValueError:
   3278         pass

File /usr/local/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 /usr/local/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 /usr/local/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:389, in make_layout_margins(layoutgrids, fig, renderer, w_pad, h_pad, hspace, wspace)
    385     return
    387 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
    388                                  hspace=hspace, wspace=wspace)
--> 389 pos, bbox = get_pos_and_bbox(ax, renderer)
    390 # the margin is the distance between the bounding box of the Axes
    391 # and its position (plus the padding from above)
    392 margin['left'] += pos.x0 - bbox.x0

File /usr/local/lib/python3.14/site-packages/matplotlib/_constrained_layout.py:637, in get_pos_and_bbox(ax, renderer)
    635 # pos is in panel co-ords, but we need in figure for the layout
    636 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
--> 637 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
    638 if tightbbox is None:
    639     bbox = pos

File /usr/local/lib/python3.14/site-packages/matplotlib/artist.py:1485, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1479 """
   1480 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1481 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1482 when encountering third-party subclasses that do not support it.
   1483 """
   1484 try:
-> 1485     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1486 except TypeError:
   1487     return obj.get_tightbbox(*args, **kwargs)

File /usr/local/lib/python3.14/site-packages/matplotlib/axes/_base.py:4758, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4756 if self.axison and axis.get_visible():
   4757     if for_layout_only:
-> 4758         ba = martist._get_tightbbox_for_layout_only(axis, renderer)
   4759     else:
   4760         ba = axis.get_tightbbox(renderer)

File /usr/local/lib/python3.14/site-packages/matplotlib/artist.py:1485, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
   1479 """
   1480 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
   1481 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
   1482 when encountering third-party subclasses that do not support it.
   1483 """
   1484 try:
-> 1485     return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
   1486 except TypeError:
   1487     return obj.get_tightbbox(*args, **kwargs)

File /usr/local/lib/python3.14/site-packages/matplotlib/axis.py:1427, in Axis.get_tightbbox(self, renderer, for_layout_only)
   1424     renderer = self.get_figure(root=True)._get_renderer()
   1425 ticks_to_draw = self._update_ticks()
-> 1427 self._update_label_position(renderer)
   1429 # go back to just this axis's tick labels
   1430 tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer)

File /usr/local/lib/python3.14/site-packages/matplotlib/axis.py:2615, in XAxis._update_label_position(self, renderer)
   2611     return
   2613 # get bounding boxes for this axis and any siblings
   2614 # that have been set by `fig.align_xlabels()`
-> 2615 bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
   2616 x, y = self.label.get_position()
   2618 if self.label_position == 'bottom':
   2619     # Union with extents of the bottom spine if present, of the axes otherwise.

File /usr/local/lib/python3.14/site-packages/matplotlib/axis.py:2403, in Axis._get_tick_boxes_siblings(self, renderer)
   2401 axis = ax._axis_map[name]
   2402 ticks_to_draw = axis._update_ticks()
-> 2403 tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer)
   2404 bboxes.extend(tlb)
   2405 bboxes2.extend(tlb2)

File /usr/local/lib/python3.14/site-packages/matplotlib/axis.py:1404, in Axis._get_ticklabel_bboxes(self, ticks, renderer)
   1402 def _get_ticklabel_bboxes(self, ticks, renderer):
   1403     """Return lists of bboxes for ticks' label1's and label2's."""
-> 1404     return ([tick.label1.get_window_extent(renderer)
   1405              for tick in ticks
   1406              if tick.label1.get_visible() and tick.label1.get_in_layout()],
   1407             [tick.label2.get_window_extent(renderer)
   1408              for tick in ticks
   1409              if tick.label2.get_visible() and tick.label2.get_in_layout()])

File /usr/local/lib/python3.14/site-packages/matplotlib/text.py:1076, in Text.get_window_extent(self, renderer, dpi)
   1071     raise RuntimeError(
   1072         "Cannot get window extent of text w/o renderer. You likely "
   1073         "want to call 'figure.draw_without_rendering()' first.")
   1075 with cbook._setattr_cm(fig, dpi=dpi):
-> 1076     bbox, _, _ = self._get_layout(self._renderer)
   1077     x, y = self.get_unitless_position()
   1078     x, y = self.get_transform().transform((x, y))

File /usr/local/lib/python3.14/site-packages/matplotlib/text.py:465, in Text._get_layout(self, renderer)
    462             break
    463 if None in (min_ascent, min_descent):
    464     # Fallback to font measurement.
--> 465     _, h, min_descent = _get_text_metrics_with_cache(
    466         renderer, "lp", self._fontproperties,
    467         ismath="TeX" if self.get_usetex() else False,
    468         dpi=dpi)
    469     min_ascent = h - min_descent
    470     line_gap = 0

File /usr/local/lib/python3.14/site-packages/matplotlib/text.py:59, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
     52 get_text_metrics = _get_text_metrics_function(renderer)
     53 # call the function to compute the metrics and return
     54 #
     55 # We pass a copy of the fontprop because FontProperties is both mutable and
     56 # has a `__hash__` that depends on that mutable state.  This is not ideal
     57 # as it means the hash of an object is not stable over time which leads to
     58 # very confusing behavior when used as keys in dictionaries or hashes.
---> 59 return get_text_metrics(text, fontprop.copy(), ismath, dpi)

File /usr/local/lib/python3.14/site-packages/matplotlib/text.py:125, in _get_text_metrics_function.<locals>._text_metrics(text, fontprop, ismath, dpi)
    120     raise RuntimeError(
    121         "Trying to get text metrics for a renderer that no longer exists.  "
    122         "This should never happen and is evidence of a bug elsewhere."
    123         )
    124 # do the actual method call we need and return the result
--> 125 return local_renderer.get_text_width_height_descent(text, fontprop, ismath)

File /usr/local/lib/python3.14/site-packages/matplotlib/backends/backend_agg.py:256, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
    254 _api.check_in_list(["TeX", True, False], ismath=ismath)
    255 if ismath == "TeX":
--> 256     return super().get_text_width_height_descent(s, prop, ismath)
    258 if ismath:
    259     parse = self.mathtext_parser.parse(s, self.dpi, prop)

File /usr/local/lib/python3.14/site-packages/matplotlib/backend_bases.py:589, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
    585 fontsize = prop.get_size_in_points()
    587 if ismath == 'TeX':
    588     # todo: handle properties
--> 589     return self.get_texmanager().get_text_width_height_descent(
    590         s, fontsize, renderer=self)
    592 dpi = self.points_to_pixels(72)
    593 if ismath:

File /usr/local/lib/python3.14/site-packages/matplotlib/texmanager.py:369, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
    367 if tex.strip() == '':
    368     return 0, 0, 0
--> 369 dvipath = cls.make_dvi(tex, fontsize)
    370 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
    371 with dviread.Dvi(dvipath, 72 * dpi_fraction) as dvi:

File /usr/local/lib/python3.14/site-packages/matplotlib/texmanager.py:301, in TexManager.make_dvi(cls, tex, fontsize)
    298 with TemporaryDirectory(dir=dvipath.parent) as tmpdir:
    299     Path(tmpdir, "file.tex").write_text(
    300         cls._get_tex_source(tex, fontsize), encoding='utf-8')
--> 301     cls._run_checked_subprocess(
    302         ["latex", "-interaction=nonstopmode", "-halt-on-error",
    303          "-no-shell-escape", "file.tex"], tex, cwd=tmpdir)
    304     Path(tmpdir, "file.dvi").replace(dvipath)
    305     # Also move the tex source to the main cache directory, but
    306     # only for backcompat.

File /usr/local/lib/python3.14/site-packages/matplotlib/texmanager.py:274, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
    262     raise RuntimeError(
    263         '{prog} was not able to process the following string:\n'
    264         '{tex!r}\n\n'
   (...)    271             exc=exc.output.decode('utf-8', 'backslashreplace'))
    272         ) from None
    273 except (FileNotFoundError, OSError) as exc:
--> 274     raise RuntimeError(
    275         f'Failed to process string with tex because {command[0]} '
    276         'could not be found') from exc
    277 _log.debug(report)
    278 return report

RuntimeError: Failed to process string with tex because latex could not be found
<Figure size 300x600 with 2 Axes>

For a complete reference of all the xLA features implemented in pyXla see the API reference.