Sensitivity Analysis and Probabilistic Estimation of Reservoir Emissions
This notebook demonstrates how to:
Run parametric uncertainty analysis with SALib and the Sobol method
Visualize the parametric sensitivity / uncertainty on various plots
Compute the sensitivities across many scenarios (e.g. reservoirs)
Present emission predictions as probability density plots
NOTE:
Requires SALib library - Python implementations of commonly used sensitivity analysis methods, including Sobol, Morris, and FAST methods - see: https://app.readthedocs.org/projects/salib/downloads/pdf/stable/
< Modifying Configuration Parametrers | Contents
QUICK DESCRIPTION:
In this example, we leverage Re-Emission’s capability to dynamically alter its configuration parameters, such as emission model regression coefficients, pre-impoundment emissions or nutrient exports to perform sensitivity analysis and Monte-Carlo simulations of reservoir emissions under parametric uncertainties. The analysis is performed using Python’s SALib package. ReEmission’s salib
module contains interfaces to SALib methods and functions allowing seamless integration with
SALib enabling global sensitivity analysis to parametric uncertainties as well as input uncertainties.
This notebook performs a simple analysis to briefly demonstrate the capability of ReEmission’s salib
module. For simplicity, it is restricted to testing model sensitivity to parametric uncertainties stemming from emission regression equations only. Sensitivity to other parametric uncertainties and to input uncertainties have not been investigated here.
[1]:
import pathlib
import os
from typing import List
from functools import partial
import gdown
from rich import print as rprint
%matplotlib inline
import matplotlib.pyplot as plt
try:
import reemission
except ImportError:
print("Unable to import reemission. Please ensure it is installed.")
%pip install git+https://github.com/tomjanus/reemission.git --quiet
from reemission.salib.runners import SALibProblem, SobolAnalyser # type: ignore
from reemission.salib.wrappers import ReEmissionSALibWrapper # type: ignore
from reemission.salib.visualize import SobolResultVisualizer, SobolScenarioResultsVisualizer # type: ignore
from reemission.salib.specloaders import (
ReEmissionSALibSpecLoader,
set_unit_input_distribution_using_rel_diffrence) # type: ignore
from reemission.input import Inputs # type: ignore
from reemission.salib.runners import SobolResults, SobolScenarioResults # type: ignore
# Constants
REL_DIFF = 0.1
# Get the directory where this notebook is located
notebook_dir = pathlib.Path().resolve()
# Define file paths relative to notebook location
inputs_file = notebook_dir / 'inputs_sensitivity_test.json'
spec_file = notebook_dir / 'uncertain_parameter_specification.yaml'
if not inputs_file.exists():
# Download the required input file from an external link
!gdown 1YHjA9HfulLV6wqwzryHZiFFc1kvXKFWL -O {str(inputs_file)}
if not spec_file.exists():
# Download the required input file from an external link
!gdown 1punHksgsZhj7Tq7IPPLCLqa0AHkDrLmx -O {str(spec_file)}
2. Sensitivity Analysis using Re-Emission
Confidence intervals for some parameters were derived from G-Res Tool Technical Documentation
[2]:
sc_results: List[SobolResults] = []
sc_names: List[str] = []
seed = 42
selected_reservoir_index = 1
# Run SOBOL analysis for a subset of UK reservoirs - can take a bit of time
inputs = Inputs.fromfile("inputs_sensitivity_test.json")
reservoirs_list_uk = list(inputs.inputs.keys())
rprint(f"Number of reservoirs: {len(reservoirs_list_uk)}")
selected_reservoirs = reservoirs_list_uk[:]
uk_input_file = pathlib.Path("inputs_sensitivity_test.json").resolve()
spec_file = "uncertain_parameter_specification.yaml"
inputs = Inputs.fromfile(uk_input_file)
for res_no, reservoir in enumerate(selected_reservoirs):
rprint(f"Running SOBOL analysis for reservoir: {reservoir} - {res_no + 1} out of {len(selected_reservoirs)}")
selected_input = inputs.get_input(reservoir) # Use a single reservoir
# Set the relative +/- difference for the inputs with missing distributions
rel_difference: float = 0.1
# Load the SALib specification for the re-emission model
reemission_salib_spec = ReEmissionSALibSpecLoader(
spec_file=spec_file,
input=selected_input,
missing_input_dist_handler =
partial(
set_unit_input_distribution_using_rel_diffrence,
rel_difference=rel_difference)
)
# Var names for visualization
var_names = reemission_salib_spec.var_name_map
# Create a list of variables from the SALib specification
reemission_variables = reemission_salib_spec.list_of_variables
# Create a list of accessors from the SALib specification
accessors = reemission_salib_spec.accessors
reemission_salib_problem = SALibProblem.from_variables(
reemission_variables
)
reemission_salib_model = ReEmissionSALibWrapper.from_variables(
variables = reemission_variables,
input = selected_input,
emission = 'total_net',
accessors = accessors
)
analyser = SobolAnalyser(
problem = reemission_salib_problem,
variables = reemission_variables,
model = reemission_salib_model,
num_samples = 512
)
sc_result = analyser.run_sobol()
object.__setattr__(sc_result, 'nominal_output', sc_result.nominal_output[0])
sc_results.append(sc_result)
sc_names.append(reservoir)
sc_results = SobolScenarioResults(
sc_names=sc_names,
results=sc_results
)
var_names = sc_results.var_names
visualizer = SobolResultVisualizer(
sc_results.results[selected_reservoir_index],
par_name_map = var_names)
scenario_visualizer = SobolScenarioResultsVisualizer(sc_results)
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
axes = axes.flatten()
scenario_visualizer.plot_S1_ST(
ax=axes[0],
x_label_rotation = 90,
title=f'Sobol Indices for regression coefficients')
scenario_visualizer.plot_variance_contributions_by_group(
ax=axes[1],
title="Variance contributions by uncertainty group",)
scenario_visualizer.plot_outputs_per_scenarios(
ax=axes[2],
#scenario_names = [f'{ix}' for ix in range(len(sc_results.scenario_names))],
x_label_rotation = 90,
sorting='desc',
title="Total net emission predictions for multiple reservoirs",
width=0.085,
component_colors=['#ff7f0e', '#1f77b4', '#2ca02c', '#9467bd', '#d62728', '#8c564b'])
visualizer.plot_output_kde(
ax=axes[3],
xlims=(-1400,-1000),
title=f"Total net emissions under parameter uncertainty - {sc_names[selected_reservoir_index]}")
fig.savefig(pathlib.Path('reemission_sobol_paper.png'))
Number of reservoirs: 5
Running SOBOL analysis for reservoir: Katrine - 1 out of 5
/opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/SALib/util/__init__.py:274: FutureWarning: unique with argument that is not not a Series, Index, ExtensionArray, or np.ndarray is deprecated and will raise in a future version.
names = list(pd.unique(groups))
Running SOBOL analysis for reservoir: Black Esk - 2 out of 5
Running SOBOL analysis for reservoir: Whiteadder - 3 out of 5
Running SOBOL analysis for reservoir: St Mary's - 4 out of 5
Running SOBOL analysis for reservoir: Alaw - 5 out of 5
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/texmanager.py:250, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
249 try:
--> 250 report = subprocess.check_output(
251 command, cwd=cwd if cwd is not None else cls._texcache,
252 stderr=subprocess.STDOUT)
253 except FileNotFoundError as exc:
File /opt/hostedtoolcache/Python/3.11.13/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.13/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.13/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.13/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[2], line 66
64 fig, axes = plt.subplots(2, 2, figsize=(10, 8))
65 axes = axes.flatten()
---> 66 scenario_visualizer.plot_S1_ST(
67 ax=axes[0],
68 x_label_rotation = 90,
69 title=f'Sobol Indices for regression coefficients')
71 scenario_visualizer.plot_variance_contributions_by_group(
72 ax=axes[1],
73 title="Variance contributions by uncertainty group",)
75 scenario_visualizer.plot_outputs_per_scenarios(
76 ax=axes[2],
77 #scenario_names = [f'{ix}' for ix in range(len(sc_results.scenario_names))],
(...) 81 width=0.085,
82 component_colors=['#ff7f0e', '#1f77b4', '#2ca02c', '#9467bd', '#d62728', '#8c564b'])
File ~/work/reemission/reemission/src/reemission/salib/visualize.py:131, in SobolScenarioResultsVisualizer.plot_S1_ST(self, ax, title, x_label_rotation, confidence_level, tight_layout)
129 ax.legend()
130 if tight_layout:
--> 131 plt.tight_layout()
132 return ax
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/pyplot.py:2844, in tight_layout(pad, h_pad, w_pad, rect)
2836 @_copy_docstring_and_deprecators(Figure.tight_layout)
2837 def tight_layout(
2838 *,
(...) 2842 rect: tuple[float, float, float, float] | None = None,
2843 ) -> None:
-> 2844 gcf().tight_layout(pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/figure.py:3640, in Figure.tight_layout(self, pad, h_pad, w_pad, rect)
3638 previous_engine = self.get_layout_engine()
3639 self.set_layout_engine(engine)
-> 3640 engine.execute(self)
3641 if previous_engine is not None and not isinstance(
3642 previous_engine, (TightLayoutEngine, PlaceHolderLayoutEngine)
3643 ):
3644 _api.warn_external('The figure layout has changed to tight')
File /opt/hostedtoolcache/Python/3.11.13/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.13/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.13/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.13/x64/lib/python3.11/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
1396 """
1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
1399 when encountering third-party subclasses that do not support it.
1400 """
1401 try:
-> 1402 return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
1403 except TypeError:
1404 return obj.get_tightbbox(*args, **kwargs)
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/axes/_base.py:4564, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
4562 for axis in self._axis_map.values():
4563 if self.axison and axis.get_visible():
-> 4564 ba = martist._get_tightbbox_for_layout_only(axis, renderer)
4565 if ba:
4566 bb.append(ba)
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
1396 """
1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
1399 when encountering third-party subclasses that do not support it.
1400 """
1401 try:
-> 1402 return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
1403 except TypeError:
1404 return obj.get_tightbbox(*args, **kwargs)
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/axis.py:1353, in Axis.get_tightbbox(self, renderer, for_layout_only)
1350 renderer = self.get_figure(root=True)._get_renderer()
1351 ticks_to_draw = self._update_ticks()
-> 1353 self._update_label_position(renderer)
1355 # go back to just this axis's tick labels
1356 tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer)
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/axis.py:2448, in XAxis._update_label_position(self, renderer)
2444 return
2446 # get bounding boxes for this axis and any siblings
2447 # that have been set by `fig.align_xlabels()`
-> 2448 bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
2449 x, y = self.label.get_position()
2451 if self.label_position == 'bottom':
2452 # Union with extents of the bottom spine if present, of the axes otherwise.
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/axis.py:2241, in Axis._get_tick_boxes_siblings(self, renderer)
2239 axis = ax._axis_map[name]
2240 ticks_to_draw = axis._update_ticks()
-> 2241 tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer)
2242 bboxes.extend(tlb)
2243 bboxes2.extend(tlb2)
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/axis.py:1332, in Axis._get_ticklabel_bboxes(self, ticks, renderer)
1330 if renderer is None:
1331 renderer = self.get_figure(root=True)._get_renderer()
-> 1332 return ([tick.label1.get_window_extent(renderer)
1333 for tick in ticks if tick.label1.get_visible()],
1334 [tick.label2.get_window_extent(renderer)
1335 for tick in ticks if tick.label2.get_visible()])
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/axis.py:1332, in <listcomp>(.0)
1330 if renderer is None:
1331 renderer = self.get_figure(root=True)._get_renderer()
-> 1332 return ([tick.label1.get_window_extent(renderer)
1333 for tick in ticks if tick.label1.get_visible()],
1334 [tick.label2.get_window_extent(renderer)
1335 for tick in ticks if tick.label2.get_visible()])
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
964 raise RuntimeError(
965 "Cannot get window extent of text w/o renderer. You likely "
966 "want to call 'figure.draw_without_rendering()' first.")
968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969 bbox, info, descent = self._get_layout(self._renderer)
970 x, y = self.get_unitless_position()
971 x, y = self.get_transform().transform((x, y))
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
370 ys = []
372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
374 renderer, "lp", self._fontproperties,
375 ismath="TeX" if self.get_usetex() else False,
376 dpi=self.get_figure(root=True).dpi)
377 min_dy = (lp_h - lp_d) * self._linespacing
379 for i, line in enumerate(lines):
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
67 # Cached based on a copy of fontprop so that later in-place mutations of
68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
70 weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
73 @functools.lru_cache(4096)
74 def _get_text_metrics_with_cache_impl(
75 renderer_ref, text, fontprop, ismath, dpi):
76 # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77 return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
209 _api.check_in_list(["TeX", True, False], ismath=ismath)
210 if ismath == "TeX":
--> 211 return super().get_text_width_height_descent(s, prop, ismath)
213 if ismath:
214 ox, oy, width, height, descent, font_image = \
215 self.mathtext_parser.parse(s, self.dpi, prop)
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
562 fontsize = prop.get_size_in_points()
564 if ismath == 'TeX':
565 # todo: handle properties
--> 566 return self.get_texmanager().get_text_width_height_descent(
567 s, fontsize, renderer=self)
569 dpi = self.points_to_pixels(72)
570 if ismath:
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/texmanager.py:363, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
361 if tex.strip() == '':
362 return 0, 0, 0
--> 363 dvifile = cls.make_dvi(tex, fontsize)
364 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
365 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/texmanager.py:295, in TexManager.make_dvi(cls, tex, fontsize)
293 with TemporaryDirectory(dir=cwd) as tmpdir:
294 tmppath = Path(tmpdir)
--> 295 cls._run_checked_subprocess(
296 ["latex", "-interaction=nonstopmode", "--halt-on-error",
297 f"--output-directory={tmppath.name}",
298 f"{texfile.name}"], tex, cwd=cwd)
299 (tmppath / Path(dvifile).name).replace(dvifile)
300 return dvifile
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/texmanager.py:254, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
250 report = subprocess.check_output(
251 command, cwd=cwd if cwd is not None else cls._texcache,
252 stderr=subprocess.STDOUT)
253 except FileNotFoundError as exc:
--> 254 raise RuntimeError(
255 f'Failed to process string with tex because {command[0]} '
256 'could not be found') from exc
257 except subprocess.CalledProcessError as exc:
258 raise RuntimeError(
259 '{prog} was not able to process the following string:\n'
260 '{tex!r}\n\n'
(...) 267 exc=exc.output.decode('utf-8', 'backslashreplace'))
268 ) from None
RuntimeError: Failed to process string with tex because latex could not be found
Error in callback <function _draw_all_if_interactive at 0x7f74ba828400> (for post_execute), with arguments args (),kwargs {}:
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/texmanager.py:250, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
249 try:
--> 250 report = subprocess.check_output(
251 command, cwd=cwd if cwd is not None else cls._texcache,
252 stderr=subprocess.STDOUT)
253 except FileNotFoundError as exc:
File /opt/hostedtoolcache/Python/3.11.13/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.13/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.13/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.13/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.13/x64/lib/python3.11/site-packages/matplotlib/pyplot.py:279, in _draw_all_if_interactive()
277 def _draw_all_if_interactive() -> None:
278 if matplotlib.is_interactive():
--> 279 draw_all()
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/_pylab_helpers.py:131, in Gcf.draw_all(cls, force)
129 for manager in cls.get_all_fig_managers():
130 if force or manager.canvas.figure.stale:
--> 131 manager.canvas.draw_idle()
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/backend_bases.py:1893, in FigureCanvasBase.draw_idle(self, *args, **kwargs)
1891 if not self._is_idle_drawing:
1892 with self._idle_draw_cntx():
-> 1893 self.draw(*args, **kwargs)
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/backends/backend_agg.py:382, in FigureCanvasAgg.draw(self)
379 # Acquire a lock on the shared font cache.
380 with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
381 else nullcontext()):
--> 382 self.figure.draw(self.renderer)
383 # A GUI class may be need to update a window using this draw, so
384 # don't forget to call the superclass.
385 super().draw()
File /opt/hostedtoolcache/Python/3.11.13/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.13/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.13/x64/lib/python3.11/site-packages/matplotlib/figure.py:3257, in Figure.draw(self, renderer)
3254 # ValueError can occur when resizing a window.
3256 self.patch.draw(renderer)
-> 3257 mimage._draw_list_compositing_images(
3258 renderer, self, artists, self.suppressComposite)
3260 renderer.close_group('figure')
3261 finally:
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/image.py:134, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
132 if not_composite or not has_images:
133 for a in artists:
--> 134 a.draw(renderer)
135 else:
136 # Composite any adjacent images together
137 image_group = []
File /opt/hostedtoolcache/Python/3.11.13/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.13/x64/lib/python3.11/site-packages/matplotlib/axes/_base.py:3190, in _AxesBase.draw(self, renderer)
3187 for spine in self.spines.values():
3188 artists.remove(spine)
-> 3190 self._update_title_position(renderer)
3192 if not self.axison:
3193 for _axis in self._axis_map.values():
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/axes/_base.py:3134, in _AxesBase._update_title_position(self, renderer)
3132 if title.get_text():
3133 for ax in axs:
-> 3134 ax.yaxis.get_tightbbox(renderer) # update offsetText
3135 if ax.yaxis.offsetText.get_text():
3136 bb = ax.yaxis.offsetText.get_tightbbox(renderer)
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/axis.py:1353, in Axis.get_tightbbox(self, renderer, for_layout_only)
1350 renderer = self.get_figure(root=True)._get_renderer()
1351 ticks_to_draw = self._update_ticks()
-> 1353 self._update_label_position(renderer)
1355 # go back to just this axis's tick labels
1356 tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer)
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/axis.py:2675, in YAxis._update_label_position(self, renderer)
2671 return
2673 # get bounding boxes for this axis and any siblings
2674 # that have been set by `fig.align_ylabels()`
-> 2675 bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
2676 x, y = self.label.get_position()
2678 if self.label_position == 'left':
2679 # Union with extents of the left spine if present, of the axes otherwise.
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/axis.py:2241, in Axis._get_tick_boxes_siblings(self, renderer)
2239 axis = ax._axis_map[name]
2240 ticks_to_draw = axis._update_ticks()
-> 2241 tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer)
2242 bboxes.extend(tlb)
2243 bboxes2.extend(tlb2)
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/axis.py:1332, in Axis._get_ticklabel_bboxes(self, ticks, renderer)
1330 if renderer is None:
1331 renderer = self.get_figure(root=True)._get_renderer()
-> 1332 return ([tick.label1.get_window_extent(renderer)
1333 for tick in ticks if tick.label1.get_visible()],
1334 [tick.label2.get_window_extent(renderer)
1335 for tick in ticks if tick.label2.get_visible()])
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/axis.py:1332, in <listcomp>(.0)
1330 if renderer is None:
1331 renderer = self.get_figure(root=True)._get_renderer()
-> 1332 return ([tick.label1.get_window_extent(renderer)
1333 for tick in ticks if tick.label1.get_visible()],
1334 [tick.label2.get_window_extent(renderer)
1335 for tick in ticks if tick.label2.get_visible()])
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
964 raise RuntimeError(
965 "Cannot get window extent of text w/o renderer. You likely "
966 "want to call 'figure.draw_without_rendering()' first.")
968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969 bbox, info, descent = self._get_layout(self._renderer)
970 x, y = self.get_unitless_position()
971 x, y = self.get_transform().transform((x, y))
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/text.py:373, in Text._get_layout(self, renderer)
370 ys = []
372 # Full vertical extent of font, including ascenders and descenders:
--> 373 _, lp_h, lp_d = _get_text_metrics_with_cache(
374 renderer, "lp", self._fontproperties,
375 ismath="TeX" if self.get_usetex() else False,
376 dpi=self.get_figure(root=True).dpi)
377 min_dy = (lp_h - lp_d) * self._linespacing
379 for i, line in enumerate(lines):
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
67 # Cached based on a copy of fontprop so that later in-place mutations of
68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
70 weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
73 @functools.lru_cache(4096)
74 def _get_text_metrics_with_cache_impl(
75 renderer_ref, text, fontprop, ismath, dpi):
76 # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77 return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/backends/backend_agg.py:211, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
209 _api.check_in_list(["TeX", True, False], ismath=ismath)
210 if ismath == "TeX":
--> 211 return super().get_text_width_height_descent(s, prop, ismath)
213 if ismath:
214 ox, oy, width, height, descent, font_image = \
215 self.mathtext_parser.parse(s, self.dpi, prop)
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/backend_bases.py:566, in RendererBase.get_text_width_height_descent(self, s, prop, ismath)
562 fontsize = prop.get_size_in_points()
564 if ismath == 'TeX':
565 # todo: handle properties
--> 566 return self.get_texmanager().get_text_width_height_descent(
567 s, fontsize, renderer=self)
569 dpi = self.points_to_pixels(72)
570 if ismath:
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/texmanager.py:363, in TexManager.get_text_width_height_descent(cls, tex, fontsize, renderer)
361 if tex.strip() == '':
362 return 0, 0, 0
--> 363 dvifile = cls.make_dvi(tex, fontsize)
364 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1
365 with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi:
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/texmanager.py:295, in TexManager.make_dvi(cls, tex, fontsize)
293 with TemporaryDirectory(dir=cwd) as tmpdir:
294 tmppath = Path(tmpdir)
--> 295 cls._run_checked_subprocess(
296 ["latex", "-interaction=nonstopmode", "--halt-on-error",
297 f"--output-directory={tmppath.name}",
298 f"{texfile.name}"], tex, cwd=cwd)
299 (tmppath / Path(dvifile).name).replace(dvifile)
300 return dvifile
File /opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/matplotlib/texmanager.py:254, in TexManager._run_checked_subprocess(cls, command, tex, cwd)
250 report = subprocess.check_output(
251 command, cwd=cwd if cwd is not None else cls._texcache,
252 stderr=subprocess.STDOUT)
253 except FileNotFoundError as exc:
--> 254 raise RuntimeError(
255 f'Failed to process string with tex because {command[0]} '
256 'could not be found') from exc
257 except subprocess.CalledProcessError as exc:
258 raise RuntimeError(
259 '{prog} was not able to process the following string:\n'
260 '{tex!r}\n\n'
(...) 267 exc=exc.output.decode('utf-8', 'backslashreplace'))
268 ) from None
RuntimeError: Failed to process string with tex because latex could not be found