Scattering Angles from EOB Data

This tutorial demonstrates how to compute scattering angles from hyperbolic encounters using Effective One Body (EOB) waveforms.

In hyperbolic encounters, two compact objects approach each other with sufficient energy to scatter rather than merge. The scattering angle quantifies the deflection of the trajectory.

Setup

First, we import the necessary modules:

%matplotlib inline
%config InlineBackend.figure_format = 'retina'

import PyART.models.teob as teob
from PyART.analysis.scattering_angle import ScatteringAngle
import matplotlib.pyplot as plt 
from PyART.utils.utils import D1
import numpy as np

Define Configurations

We’ll test several different impact parameters (b) to see how they affect the scattering angle. Each configuration has:

  • b: impact parameter

  • chi1, chi2: dimensionless spins

  • E0: initial energy

  • p: dimensionless momentum parameter

configurations = [
    {'b': 9.678, 'chi1':0., 'chi2':0., 'E0':1.0226, 'p':0.11456439, 'eob':None, 'scat':None},
    {'b':10.000, 'chi1':0., 'chi2':0., 'E0':1.0226, 'p':0.11456439, 'eob':None, 'scat':None},
    {'b':11.000, 'chi1':0., 'chi2':0., 'E0':1.0226, 'p':0.11456439, 'eob':None, 'scat':None},
    {'b':12.000, 'chi1':0., 'chi2':0., 'E0':1.0226, 'p':0.11456439, 'eob':None, 'scat':None},
    {'b':13.000, 'chi1':0., 'chi2':0., 'E0':1.0226, 'p':0.11456439, 'eob':None, 'scat':None},
]
n_conf = len(configurations)

# Initial separation and symmetric mass ratio
r0 = 100
nu = 0.25

Generate EOB Data and Compute Scattering Angles

For each configuration, we:

  1. Set up the EOB parameters

  2. Generate the EOB waveform

  3. Compute the scattering angle (if not a capture)

cutoff_min = 25
for i in range(n_conf):
    conf = configurations[i]
    J0 = conf['p']*conf['b']/nu
    
    eobpars = teob.CreateDict(r_hyp=r0, H_hyp=conf['E0'], J_hyp=J0, q=1, 
                             chi1z=conf['chi1'], chi2z=conf['chi2'])
    eob = teob.Waveform_EOB(pars=eobpars)
    conf['eob']  = eob
    E_final = eob.dyn['E'][-1]
    
    print(f"Configuration {i+1}:")
    print(f"  Impact parameter b = {conf['b']:.3f}")
    print(f"  E0, J0 = {conf['E0']:.5f}, {J0:.5f}")
    print(f"  Final energy = {E_final:.5f}")
    
    if E_final > 1:
        # Scattering event (not a capture)
        scat = ScatteringAngle(puncts=eob.dyn, nmin=2, nmax=10, n_extract=None,
                               hypfit=True,
                               r_cutoff_in_low=cutoff_min,  r_cutoff_in_high=eob.dyn['r'][0],
                               r_cutoff_out_low=cutoff_min, r_cutoff_out_high=None, verbose=False)
        conf['scat'] = scat
        print(f"  Scattering angle χ = {scat.chi:.2f}°")
    else:
        print('  Result: Capture!')
    print()
Configuration 1:
  Impact parameter b = 9.678
  E0, J0 = 1.02260, 4.43502
  Final energy = 1.00007
  Scattering angle χ = 402.33°

Configuration 2:
  Impact parameter b = 10.000
  E0, J0 = 1.02260, 4.58258
  Final energy = 1.01214
  Scattering angle χ = 252.82°

Configuration 3:
  Impact parameter b = 11.000
  E0, J0 = 1.02260, 5.04083
  Final energy = 1.01960
  Scattering angle χ = 157.45°

Configuration 4:
  Impact parameter b = 12.000
  E0, J0 = 1.02260, 5.49909
  Final energy = 1.02121
  Scattering angle χ = 122.41°

Configuration 5:
  Impact parameter b = 13.000
  E0, J0 = 1.02260, 5.95735
  Final energy = 1.02181
  Scattering angle χ = 102.21°

Visualize Results

Now let’s plot the results for all scattering configurations:

  1. Real part of the (2,2) mode strain

  2. The Weyl scalar ψ₄

  3. Radial distance vs time

  4. Trajectories in the x-y plane with scattering angles

plt.figure(figsize=(12,9))

for conf in configurations:
    eob  = conf['eob']
    scat = conf['scat']
    
    if scat is None:
        continue
    
    # Plot waveform
    plt.subplot(2,2,1)
    plt.plot(eob.u, eob.hlm[(2,2)]['real'])
    plt.xlim([-100, 100])
    plt.xlabel('Time (M)')
    plt.ylabel(r'Re[$h_{22}$]')
    plt.title('Waveform (2,2) mode')
    plt.grid(True, alpha=0.3)

    # Plot psi4
    dh   = D1(eob.hlm[(2,2)]['z'], eob.u, 4)
    psi4 = D1(dh, eob.u, 4)
    b      = conf['b']
    chi_BH = conf['chi1']
    
    plt.subplot(2,2,2)
    plt.plot(eob.u, -psi4.real, label=f'b={b:.3f}, χ={chi_BH}')
    plt.xlim([-100, 100])
    plt.xlabel('Time (M)')
    plt.ylabel(r'Re[$-\psi_4$]')
    plt.title('Weyl scalar')
    plt.legend()
    plt.grid(True, alpha=0.3)

    # Plot radial distance
    x = eob.dyn['r']*np.cos(eob.dyn['phi'])
    y = eob.dyn['r']*np.sin(eob.dyn['phi'])
    r = np.sqrt(x**2 + y**2)
    
    plt.subplot(2,2,3)
    plt.plot(eob.dyn['t'], r)
    plt.xlabel('Time (M)')
    plt.ylabel('Radial distance r (M)')
    plt.title('Radial distance vs time')
    plt.grid(True, alpha=0.3)

    # Plot trajectory
    plt.subplot(2,2,4)
    plt.plot(x, y, label=f'χ={scat.chi:.2f}°')
    plt.xlabel('x (M)')
    plt.ylabel('y (M)')
    plt.title('Trajectories with scattering angles')
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.axis('equal')

plt.tight_layout()
plt.show()
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/subprocess.py:466, in check_output(timeout, *popenargs, **kwargs)
    464     kwargs['input'] = empty
--> 466 return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
    467            **kwargs).stdout

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/subprocess.py:548, in run(input, capture_output, timeout, check, *popenargs, **kwargs)
    546     kwargs['stderr'] = PIPE
--> 548 with Popen(*popenargs, **kwargs) as process:
    549     try:

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/subprocess.py:1026, 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)
   1023             self.stderr = io.TextIOWrapper(self.stderr,
   1024                     encoding=encoding, errors=errors)
-> 1026     self._execute_child(args, executable, preexec_fn, close_fds,
   1027                         pass_fds, cwd, env,
   1028                         startupinfo, creationflags, shell,
   1029                         p2cread, p2cwrite,
   1030                         c2pread, c2pwrite,
   1031                         errread, errwrite,
   1032                         restore_signals,
   1033                         gid, gids, uid, umask,
   1034                         start_new_session, process_group)
   1035 except:
   1036     # Cleanup if the child failed starting.

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/subprocess.py:1955, 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)
   1954 if err_filename is not None:
-> 1955     raise child_exception_type(errno_num, err_msg, err_filename)
   1956 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)
Cell In[4], line 56
     52     plt.legend()
     53     plt.grid(True, alpha=0.3)
     54     plt.axis('equal')
     55 
---> 56 plt.tight_layout()
     57 plt.show()

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/pyplot.py:2992, in tight_layout(pad, h_pad, w_pad, rect)
   2984 @_copy_docstring_and_deprecators(Figure.tight_layout)
   2985 def tight_layout(
   2986     *,
   (...)   2990     rect: tuple[float, float, float, float] | None = None,
   2991 ) -> None:
-> 2992     gcf().tight_layout(pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/figure.py:3665, in Figure.tight_layout(self, pad, h_pad, w_pad, rect)
   3663 previous_engine = self.get_layout_engine()
   3664 self.set_layout_engine(engine)
-> 3665 engine.execute(self)
   3666 if previous_engine is not None and not isinstance(
   3667     previous_engine, (TightLayoutEngine, PlaceHolderLayoutEngine)
   3668 ):
   3669     _api.warn_external('The figure layout has changed to tight')

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/layout_engine.py:188, in TightLayoutEngine.execute(self, fig)
    186 renderer = fig._get_renderer()
    187 with getattr(renderer, "_draw_disabled", nullcontext)():
--> 188     kwargs = get_tight_layout_figure(
    189         fig, fig.axes, get_subplotspec_list(fig.axes), renderer,
    190         pad=info['pad'], h_pad=info['h_pad'], w_pad=info['w_pad'],
    191         rect=info['rect'])
    192 if kwargs:
    193     fig.subplots_adjust(**kwargs)

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/_tight_layout.py:266, in get_tight_layout_figure(fig, axes_list, subplotspec_list, renderer, pad, h_pad, w_pad, rect)
    261         return {}
    262     span_pairs.append((
    263         slice(ss.rowspan.start * div_row, ss.rowspan.stop * div_row),
    264         slice(ss.colspan.start * div_col, ss.colspan.stop * div_col)))
--> 266 kwargs = _auto_adjust_subplotpars(fig, renderer,
    267                                   shape=(max_nrows, max_ncols),
    268                                   span_pairs=span_pairs,
    269                                   subplot_list=subplot_list,
    270                                   ax_bbox_list=ax_bbox_list,
    271                                   pad=pad, h_pad=h_pad, w_pad=w_pad)
    273 # kwargs can be none if tight_layout fails...
    274 if rect is not None and kwargs is not None:
    275     # if rect is given, the whole subplots area (including
    276     # labels) will fit into the rect instead of the
   (...)    280     # auto_adjust_subplotpars twice, where the second run
    281     # with adjusted rect parameters.

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/_tight_layout.py:82, in _auto_adjust_subplotpars(fig, renderer, shape, span_pairs, subplot_list, ax_bbox_list, pad, h_pad, w_pad, rect)
     80 for ax in subplots:
     81     if ax.get_visible():
---> 82         bb += [martist._get_tightbbox_for_layout_only(ax, renderer)]
     84 tight_bbox_raw = Bbox.union(bb)
     85 tight_bbox = fig.transFigure.inverted().transform_bbox(tight_bbox_raw)

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/axis.py:1404, in <listcomp>(.0)
   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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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
Error in callback <function _draw_all_if_interactive at 0x7fc694f9f880> (for post_execute), with arguments args (),kwargs {}:
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/subprocess.py:466, in check_output(timeout, *popenargs, **kwargs)
    464     kwargs['input'] = empty
--> 466 return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
    467            **kwargs).stdout

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/subprocess.py:548, in run(input, capture_output, timeout, check, *popenargs, **kwargs)
    546     kwargs['stderr'] = PIPE
--> 548 with Popen(*popenargs, **kwargs) as process:
    549     try:

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/subprocess.py:1026, 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)
   1023             self.stderr = io.TextIOWrapper(self.stderr,
   1024                     encoding=encoding, errors=errors)
-> 1026     self._execute_child(args, executable, preexec_fn, close_fds,
   1027                         pass_fds, cwd, env,
   1028                         startupinfo, creationflags, shell,
   1029                         p2cread, p2cwrite,
   1030                         c2pread, c2pwrite,
   1031                         errread, errwrite,
   1032                         restore_signals,
   1033                         gid, gids, uid, umask,
   1034                         start_new_session, process_group)
   1035 except:
   1036     # Cleanup if the child failed starting.

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/subprocess.py:1955, 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)
   1954 if err_filename is not None:
-> 1955     raise child_exception_type(errno_num, err_msg, err_filename)
   1956 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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/figure.py:3282, in Figure.draw(self, renderer)
   3279             # ValueError can occur when resizing a window.
   3281     self.patch.draw(renderer)
-> 3282     mimage._draw_list_compositing_images(
   3283         renderer, self, artists, self.suppressComposite)
   3285     renderer.close_group('figure')
   3286 finally:

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/image.py:133, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
    131 if not_composite or not has_images:
    132     for a in artists:
--> 133         a.draw(renderer)
    134 else:
    135     # Composite any adjacent images together
    136     image_group = []

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/axes/_base.py:3331, in _AxesBase.draw(self, renderer)
   3328     for spine in self.spines.values():
   3329         artists.remove(spine)
-> 3331 self._update_title_position(renderer)
   3333 if not self.axison:
   3334     for _axis in self._axis_map.values():

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/axes/_base.py:3270, in _AxesBase._update_title_position(self, renderer)
   3268 if title.get_text():
   3269     for ax in axs:
-> 3270         ax.yaxis.get_tightbbox(renderer)  # update offsetText
   3271         # A hidden offset text (e.g. on the shared y axis of an
   3272         # inner subplot) is not drawn, so it must not move the
   3273         # title: its tight bbox is non-finite and would otherwise
   3274         # push the title to infinity.
   3275         if (ax.yaxis.offsetText.get_visible()
   3276                 and ax.yaxis.offsetText.get_text()):

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/axis.py:2842, in YAxis._update_label_position(self, renderer)
   2838     return
   2840 # get bounding boxes for this axis and any siblings
   2841 # that have been set by `fig.align_ylabels()`
-> 2842 bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
   2843 x, y = self.label.get_position()
   2845 if self.label_position == 'left':
   2846     # Union with extents of the left spine if present, of the axes otherwise.

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/axis.py:1404, in <listcomp>(.0)
   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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/subprocess.py:466, in check_output(timeout, *popenargs, **kwargs)
    464     kwargs['input'] = empty
--> 466 return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
    467            **kwargs).stdout

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/subprocess.py:548, in run(input, capture_output, timeout, check, *popenargs, **kwargs)
    546     kwargs['stderr'] = PIPE
--> 548 with Popen(*popenargs, **kwargs) as process:
    549     try:

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/subprocess.py:1026, 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)
   1023             self.stderr = io.TextIOWrapper(self.stderr,
   1024                     encoding=encoding, errors=errors)
-> 1026     self._execute_child(args, executable, preexec_fn, close_fds,
   1027                         pass_fds, cwd, env,
   1028                         startupinfo, creationflags, shell,
   1029                         p2cread, p2cwrite,
   1030                         c2pread, c2pwrite,
   1031                         errread, errwrite,
   1032                         restore_signals,
   1033                         gid, gids, uid, umask,
   1034                         start_new_session, process_group)
   1035 except:
   1036     # Cleanup if the child failed starting.

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/subprocess.py:1955, 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)
   1954 if err_filename is not None:
-> 1955     raise child_exception_type(errno_num, err_msg, err_filename)
   1956 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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/figure.py:3282, in Figure.draw(self, renderer)
   3279             # ValueError can occur when resizing a window.
   3281     self.patch.draw(renderer)
-> 3282     mimage._draw_list_compositing_images(
   3283         renderer, self, artists, self.suppressComposite)
   3285     renderer.close_group('figure')
   3286 finally:

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/image.py:133, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
    131 if not_composite or not has_images:
    132     for a in artists:
--> 133         a.draw(renderer)
    134 else:
    135     # Composite any adjacent images together
    136     image_group = []

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/axes/_base.py:3331, in _AxesBase.draw(self, renderer)
   3328     for spine in self.spines.values():
   3329         artists.remove(spine)
-> 3331 self._update_title_position(renderer)
   3333 if not self.axison:
   3334     for _axis in self._axis_map.values():

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/axes/_base.py:3270, in _AxesBase._update_title_position(self, renderer)
   3268 if title.get_text():
   3269     for ax in axs:
-> 3270         ax.yaxis.get_tightbbox(renderer)  # update offsetText
   3271         # A hidden offset text (e.g. on the shared y axis of an
   3272         # inner subplot) is not drawn, so it must not move the
   3273         # title: its tight bbox is non-finite and would otherwise
   3274         # push the title to infinity.
   3275         if (ax.yaxis.offsetText.get_visible()
   3276                 and ax.yaxis.offsetText.get_text()):

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/axis.py:2842, in YAxis._update_label_position(self, renderer)
   2838     return
   2840 # get bounding boxes for this axis and any siblings
   2841 # that have been set by `fig.align_ylabels()`
-> 2842 bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
   2843 x, y = self.label.get_position()
   2845 if self.label_position == 'left':
   2846     # Union with extents of the left spine if present, of the axes otherwise.

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/matplotlib/axis.py:1404, in <listcomp>(.0)
   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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/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 1200x900 with 4 Axes>

Summary

This tutorial demonstrated:

  • How to set up hyperbolic encounter parameters

  • How to generate EOB waveforms for scattering scenarios

  • How to compute scattering angles using the ScatteringAngle class

  • How to visualize the waveforms and trajectories

The scattering angle provides important information about the dynamics of hyperbolic encounters and can be used to validate EOB models against numerical relativity simulations.