Coverage for sparkle/platform/latex.py: 91%
79 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-05 14:48 +0000
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-05 14:48 +0000
1#!/usr/bin/env python3
2# -*- coding: UTF-8 -*-
3"""Helper classes/method for LaTeX and bibTeX."""
4from shutil import which
5from pathlib import Path
6import subprocess
7from enum import Enum
9import numpy as np
10import pandas as pd
11import plotly.express as px
12import plotly.io as pio
14pio.kaleido.scope.mathjax = None # Bug fix for kaleido
17class ReportType(str, Enum):
18 """enum for separating different types of reports."""
19 ALGORITHM_SELECTION = "algorithm_selection"
20 ALGORITHM_CONFIGURATION = "algorithm_configuration"
21 PARALLEL_PORTFOLIO = "parallel_portfolio"
24def check_tex_commands_exist(latex_directory_path: Path) -> None:
25 """Raise an exception if one of the latex commands is not present."""
26 if which("bibtex") is None or which("pdflatex") is None:
27 raise Exception("Error: It seems like latex is not available on your system.\n"
28 "You can install latex and run the command again, "
29 f"or copy the source files in {latex_directory_path} on your "
30 "local machine to generate the report.")
33def underscore_for_latex(string: str) -> str:
34 """Return the input str with the underscores escaped for use in LaTeX.
36 Args:
37 string: A given str with underscores.
39 Returns:
40 The corresponding str with underscores escaped.
41 """
42 return string.replace("_", "\\_")
45def list_to_latex(content: list | list[tuple]) -> str:
46 """Convert a list to LaTeX.
48 Args:
49 content: The list to convert. If a tuple, first item will be boldface.
51 Returns:
52 The list as LaTeX str.
53 """
54 if len(content) == 0:
55 return "\\item"
56 if isinstance(content[0], tuple):
57 return "".join(f"\\item \\textbf{{{item[0]}}}{item[1]}" for item in content)
58 return "".join(f"\\item {item}\n" for item in content)
61def generate_comparison_plot(points: list,
62 figure_filename: str,
63 xlabel: str = "default",
64 ylabel: str = "optimised",
65 title: str = "",
66 scale: str = "log",
67 limit: str = "magnitude",
68 limit_min: float = 0.2,
69 limit_max: float = 0.2,
70 replace_zeros: bool = True,
71 magnitude_lines: int = 2147483647,
72 output_dir: Path = None) -> None:
73 """Create comparison plots between two different solvers/portfolios.
75 Args:
76 points: list of points which represents with the performance results of
77 (solverA, solverB)
78 figure_filename: filename without filetype (e.g., .jpg) to save the figure to.
79 xlabel: Name of solverA (default: default)
80 ylabel: Name of solverB (default: optimised)
81 title: Display title in the image (default: None)
82 scale: [linear, log] (default: linear)
83 limit: The method to compute the axis limits in the figure
84 [absolute, relative, magnitude] (default: relative)
85 absolute: Uses the limit_min/max values as absolute values
86 relative: Decreases/increases relatively to the min/max values found in the
87 points. E.g., min/limit_min and max*limit_max
88 magnitude: Increases the order of magnitude(10) of the min/max values in the
89 points. E.g., 10**floor(log10(min)-limit_min)
90 and 10**ceil(log10(max)+limit_max)
91 limit_min: Value used to compute the minimum limit
92 limit_max: Value used to compute the maximum limit
93 computing the figure limits. This is only relevant for runtime objectives
94 replace_zeros: Replaces zeros valued performances to a very small value to make
95 plotting on log-scale possible
96 magnitude_lines: Draw magnitude lines (only supported for log scale)
97 output_dir: directory path to place the figure and its intermediate files in
98 (default: current working directory)
99 """
100 output_dir = Path() if output_dir is None else Path(output_dir)
102 df = pd.DataFrame(points, columns=[xlabel, ylabel])
103 if replace_zeros and (df < 0).any(axis=None):
104 # Log scale cannot deal with negative and zero values, set to smallest non zero
105 df[df < 0] = np.nextafter(0, 1)
107 # process range values
108 min_point_value = df.min(numeric_only=True).min()
109 max_point_value = df.max(numeric_only=True).max()
111 if limit == "absolute":
112 min_value = limit_min
113 max_value = limit_max
114 elif limit == "relative":
115 min_value = (min_point_value * (1 / limit_min) if min_point_value > 0
116 else min_point_value * limit_min)
117 max_value = (max_point_value * limit_max if max_point_value > 0
118 else max_point_value * (1 / limit_max))
119 elif limit == "magnitude":
120 min_value = 10 ** (np.floor(np.log10(min_point_value)) - limit_min)
121 max_value = 10 ** (np.ceil(np.log10(max_point_value)) + limit_max)
123 if scale == "log" and np.min(points) <= 0:
124 raise Exception("Cannot plot negative and zero values on a log scales")
126 output_plot = output_dir / f"{figure_filename}.pdf"
127 log_scale = scale == "log"
128 fig = px.scatter(data_frame=df, x=xlabel, y=ylabel,
129 range_x=[min_value, max_value], range_y=[min_value, max_value],
130 title=title, log_x=log_scale, log_y=log_scale,
131 width=500, height=500)
132 # Add in the seperation line
133 fig.add_shape(type="line", x0=0, y0=0, x1=max_value, y1=max_value,
134 line=dict(color="lightgrey", width=1))
135 fig.update_traces(marker=dict(color="RoyalBlue", symbol="x"))
136 fig.update_layout(
137 plot_bgcolor="white"
138 )
139 fig.update_xaxes(
140 type="linear" if not log_scale else "log",
141 mirror=True,
142 tickmode="linear",
143 ticks="outside",
144 tick0=0,
145 dtick=100 if not log_scale else 1,
146 showline=True,
147 linecolor="black",
148 gridcolor="lightgrey"
149 )
150 fig.update_yaxes(
151 type="linear" if not log_scale else "log",
152 mirror=True,
153 tickmode="linear",
154 ticks="outside",
155 tick0=0,
156 dtick=100 if not log_scale else 1,
157 showline=True,
158 linecolor="black",
159 gridcolor="lightgrey"
160 )
161 fig.write_image(output_plot)
164def fill_template_tex(template_tex: str, variables: dict) -> str:
165 """Given a latex template, replaces all the @@ variables using the dict.
167 Args:
168 template_tex: The template to be populated
169 variables: Variable names (key) with their target (value)
171 Returns:
172 The populated latex string.
173 """
174 for variable_key, target_value in variables.items():
175 variable = f"@@{variable_key}@@"
176 target_value = str(target_value)
177 # We don't modify variable names in the Latex file
178 if "\\includegraphics" not in target_value and "\\label" not in target_value:
179 # Rectify underscores in target_value
180 target_value = target_value.replace("_", r"\textunderscore ")
181 template_tex = template_tex.replace(variable, target_value)
182 return template_tex
185def compile_pdf(latex_files_path: Path, latex_report_filename: Path) -> Path:
186 """Compile the given latex files to a PDF.
188 Args:
189 latex_files_path: Path to the directory with source files
190 where the report will be generated.
191 latex_report_filename: Name of the output files.
193 Returns:
194 Path to the newly generated report in PDF format.
195 """
196 pdf_process = subprocess.run(["pdflatex", "-interaction=nonstopmode",
197 f"{latex_report_filename}.tex"],
198 cwd=latex_files_path, capture_output=True)
200 if pdf_process.returncode != 0:
201 print(f"[{pdf_process.returncode}] ERROR generating with PDFLatex command:\n"
202 f"{pdf_process.stdout.decode()}\n {pdf_process.stderr.decode()}\n")
204 bibtex_process = subprocess.run(["bibtex", f"{latex_report_filename}.aux"],
205 cwd=latex_files_path, capture_output=True)
207 if bibtex_process.returncode != 0:
208 print("ERROR whilst generating with Bibtex command:"
209 f"{bibtex_process.stdout} {bibtex_process.stderr}")
211 # TODO: Fix compilation for references
212 # (~\ref[] yields [?] in pdf, re-running command fixes it)
213 # We have to re-run the same pdf command to take in the updates bib files from bibtex
214 # But Bibtex cannot function without .aux file produced by pdflatex. Hence run twice.
215 pdf_process = subprocess.run(["pdflatex", "-interaction=nonstopmode",
216 f"{latex_report_filename}.tex"],
217 cwd=latex_files_path, capture_output=True)
219 return Path(latex_files_path / latex_report_filename).with_suffix(".pdf")
222def generate_report(latex_source_path: Path,
223 latex_template_name: str,
224 target_path: Path,
225 report_name: str,
226 variable_dict: dict) -> None:
227 """General steps to generate a report.
229 Args:
230 latex_source_path: The path to the template
231 latex_template_name: The template name
232 target_path: The directory where the result should be placed
233 report_name: The name of the pdf (without suffix)
234 variable_dict: TBD
235 """
236 latex_template_filepath = latex_source_path / latex_template_name
238 report_content = latex_template_filepath.open("r").read()
239 report_content = fill_template_tex(report_content, variable_dict)
241 target_path.mkdir(parents=True, exist_ok=True)
242 latex_report_filepath = target_path / report_name
243 latex_report_filepath = latex_report_filepath.with_suffix(".tex")
244 Path(latex_report_filepath).open("w+").write(report_content)
246 check_tex_commands_exist(target_path)
247 report_path = compile_pdf(target_path, report_name)
249 print(f"Report is placed at: {report_path}")