Coverage for pyquickhelper/benchhelper/benchmark.py: 94%

293 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-03 02:21 +0200

1""" 

2@file 

3@brief Helpers to benchmark something 

4""" 

5import os 

6from datetime import datetime 

7from time import perf_counter 

8import pickle 

9from ..loghelper import noLOG, CustomLog, fLOGFormat 

10from ..loghelper.flog import get_relative_path 

11from ..pandashelper import df2rst 

12from ..texthelper import apply_template 

13 

14 

15class BenchMark: 

16 """ 

17 Class to help benchmarking. You should overwrite method 

18 *init*, *bench*, *end*, *graphs*. 

19 """ 

20 

21 def __init__(self, name, clog=None, fLOG=noLOG, path_to_images=".", 

22 cache_file=None, pickle_module=None, progressbar=None, 

23 **params): 

24 """ 

25 @param name name of the test 

26 @param clog @see cl CustomLog or string 

27 @param fLOG logging function 

28 @param params extra parameters 

29 @param path_to_images path to images 

30 @param cache_file cache file 

31 @param pickle_module pickle or dill if you need to serialize functions 

32 @param progressbar relies on *tqdm*, example *tnrange* 

33 

34 If *cache_file* is specified, the class will store the results of the 

35 method :meth:`bench <pyquickhelper.benchhelper.benchmark.GridBenchMark.bench>`. 

36 On a second run, the function load the cache 

37 and run modified or new run (in *params_list*). 

38 """ 

39 self._fLOG = fLOG 

40 self._name = name 

41 

42 if isinstance(clog, CustomLog): 

43 self._clog = clog 

44 elif clog is None: 

45 self._clog = None 

46 else: 

47 self._clog = CustomLog(clog) 

48 self._params = params 

49 self._path_to_images = path_to_images 

50 self._cache_file = cache_file 

51 self._pickle = pickle_module if pickle_module is not None else pickle 

52 self._progressbar = progressbar 

53 self._tracelogs = [] 

54 

55 ## 

56 # methods to overwrite 

57 ## 

58 

59 def init(self): 

60 """ 

61 Initialisation. Overwrite this method. 

62 """ 

63 raise NotImplementedError( 

64 "It should be overwritten.") # pragma: no cover 

65 

66 def bench(self, **params): 

67 """ 

68 Runs the benchmark. Overwrite this method. 

69 

70 @param params parameters 

71 @return metrics as a dictionary, appendix as a dictionary 

72 

73 The results of this method will be cached if a *cache_file* was specified in the constructor. 

74 """ 

75 raise NotImplementedError( 

76 "It should be overwritten.") # pragma: no cover 

77 

78 def end(self): 

79 """ 

80 Cleans. Overwrites this method. 

81 """ 

82 raise NotImplementedError( 

83 "It should be overwritten.") # pragma: no cover 

84 

85 def graphs(self, path_to_images): 

86 """ 

87 Builds graphs after the benchmark was run. 

88 

89 @param path_to_images path to images 

90 @return a list of LocalGraph 

91 

92 Every returned graph must contain a function which creates 

93 the graph. The function must accepts two parameters *ax* and 

94 *text*. Example: 

95 

96 :: 

97 

98 def local_graph(ax=None, text=True, figsize=(5,5)): 

99 vx = ... 

100 vy = ... 

101 btrys = set(df["_btry"]) 

102 ymin = df[vy].min() 

103 ymax = df[vy].max() 

104 decy = (ymax - ymin) / 50 

105 colors = cm.rainbow(numpy.linspace(0, 1, len(btrys))) 

106 if len(btrys) == 0: 

107 raise ValueError("The benchmark is empty.") 

108 if ax is None: 

109 fig, ax = plt.subplots(1, 1, figsize=figsize) 

110 ax.grid(True) 

111 for i, btry in enumerate(sorted(btrys)): 

112 subset = df[df["_btry"]==btry] 

113 if subset.shape[0] > 0: 

114 subset.plot(x=vx, y=vy, kind="scatter", label=btry, ax=ax, color=colors[i]) 

115 if text: 

116 tx = subset[vx].mean() 

117 ty = subset[vy].mean() 

118 ax.text(tx, ty + decy, btry, size='small', 

119 color=colors[i], ha='center', va='bottom') 

120 ax.set_xlabel(vx) 

121 ax.set_ylabel(vy) 

122 return ax 

123 """ 

124 return [] 

125 

126 def uncache(self, cache): 

127 """ 

128 overwrite this method to uncache some previous run 

129 """ 

130 pass 

131 

132 ## 

133 # end of methods to overwrite 

134 ## 

135 

136 class LocalGraph: 

137 """ 

138 Information about graphs. 

139 """ 

140 

141 def __init__(self, func_gen, filename=None, title=None, root=None): 

142 """ 

143 @param func_gen function generating the graph 

144 @param filename filename 

145 @param title title 

146 @param root path should be relative to this one 

147 """ 

148 if func_gen is None: 

149 raise ValueError("func_gen cannot be None") # pragma: no cover 

150 if filename is not None: 

151 self.filename = filename 

152 if title is not None: 

153 self.title = title 

154 self.root = root 

155 self.func_gen = func_gen 

156 

157 def plot(self, ax=None, text=True, **kwargs): 

158 """ 

159 Draws the graph again. 

160 

161 @param ax axis 

162 @param text add text on the graph 

163 @param kwargs additional parameters 

164 @return axis 

165 """ 

166 return self.func_gen(ax=ax, text=text, **kwargs) 

167 

168 def add(self, name, value): 

169 """ 

170 Adds an attribute. 

171 

172 @param name name of the attribute 

173 @param value value 

174 """ 

175 setattr(self, name, value) 

176 

177 def to_html(self): 

178 """ 

179 Renders as :epkg:`HTML`. 

180 """ 

181 # deal with relatif path. 

182 if hasattr(self, "filename"): 

183 attr = {} 

184 for k in {"title", "alt", "width", "height"}: 

185 if k not in attr and hasattr(self, k): 

186 attr[k if k != "title" else "alt"] = getattr(self, k) 

187 merge = " ".join(f'{k}="{v}"' 

188 for k, v in attr.items()) 

189 if self.root is not None: 

190 filename = get_relative_path( 

191 self.root, self.filename, exists=False, absolute=False) 

192 else: 

193 filename = self.filename 

194 filename = filename.replace("\\", "/") 

195 return f'<img src="{filename}" {merge}/>' 

196 else: 

197 raise NotImplementedError( 

198 "only files are allowed") # pragma: no cover 

199 

200 def to_rst(self): 

201 """ 

202 Renders as :ekg:`rst`. 

203 """ 

204 # do not consider width or height 

205 # deal with relatif path 

206 if hasattr(self, "filename"): 

207 if self.root is not None: 

208 filename = get_relative_path( 

209 self.root, self.filename, exists=False, absolute=False) 

210 else: 

211 filename = self.filename 

212 filename = filename.replace("\\", "/") 

213 return f'.. image:: {filename}' 

214 else: 

215 raise NotImplementedError( 

216 "only files are allowed") # pragma: no cover 

217 

218 @property 

219 def Name(self): 

220 """ 

221 Returns the name of the benchmark. 

222 """ 

223 return self._name 

224 

225 def fLOG(self, *args, **kwargs): 

226 """ 

227 Logs something. 

228 """ 

229 self._tracelogs.append(fLOGFormat("\n", *args, **kwargs).strip("\n")) 

230 if self._clog: 

231 self._clog(*args, **kwargs) 

232 if self._fLOG: 

233 self._fLOG(*args, **kwargs) 

234 if hasattr(self, "_progressbars") and self._progressbars and len(self._progressbars) > 0: 

235 br = self._progressbars[-1] 

236 br.set_description(fLOGFormat( # pylint: disable=C0207 

237 "\n", *args, **kwargs).strip("\n").split("\n")[0]) 

238 br.refresh() 

239 

240 def run(self, params_list): 

241 """ 

242 Runs the benchmark. 

243 

244 @param params_list list of dictionaries 

245 """ 

246 if not isinstance(params_list, list): 

247 raise TypeError("params_list must be a list") # pragma: no cover 

248 for di in params_list: 

249 if not isinstance(di, dict): 

250 raise TypeError( # pragma: no cover 

251 "params_list must be a list of dictionaries") 

252 

253 # shared variables 

254 cached = {} 

255 meta = dict(level="BenchMark", name=self.Name, nb=len( 

256 params_list), time_begin=datetime.now()) 

257 self._metadata = [] 

258 self._metadata.append(meta) 

259 nb_cached = 0 

260 

261 # cache 

262 def cache_(): 

263 "local function" 

264 if self._cache_file is not None and os.path.exists(self._cache_file): 

265 self.fLOG( 

266 f"[BenchMark.run] retrieve cache '{self._cache_file}'") 

267 with open(self._cache_file, "rb") as f: 

268 cached.update(self._pickle.load(f)) 

269 self.fLOG("[BenchMark.run] number of cached run: {0}".format( 

270 len(cached["params_list"]))) 

271 else: 

272 if self._cache_file is not None: 

273 self.fLOG( 

274 f"[BenchMark.run] cache not found '{self._cache_file}'") 

275 cached.update(dict(metrics=[], appendix=[], params_list=[])) 

276 self.uncache(cached) 

277 

278 # run 

279 def run_(pgar): 

280 "local function" 

281 nonlocal nb_cached 

282 self._metrics = [] 

283 self._appendix = [] 

284 

285 self.fLOG(f"[BenchMark.run] init {self.Name} do") 

286 self.init() 

287 self.fLOG(f"[BenchMark.run] init {self.Name} done") 

288 self.fLOG(f"[BenchMark.run] start {self.Name}") 

289 

290 for i in pgbar: 

291 di = params_list[i] 

292 

293 # check the cache 

294 if i < len(cached["params_list"]) and cached["params_list"][i] == di: 

295 can = True 

296 for v in cached.values(): 

297 if i >= len(v): 

298 # cannot cache 

299 can = False 

300 break 

301 

302 if can: 

303 # can, it checks a file is present 

304 look = "{0}.{1}.clean_cache".format( 

305 self._cache_file, cached["metrics"][i]["_btry"]) 

306 if not os.path.exists(look): 

307 can = False 

308 self.fLOG( 

309 f"[BenchMark.run] file '{look}' was not found --> run again.") 

310 if can: 

311 self._metrics.append(cached["metrics"][i]) 

312 self._appendix.append(cached["appendix"][i]) 

313 self.fLOG( 

314 f"[BenchMark.run] retrieved cached {i + 1}/{len(params_list)}: {di}") 

315 self.fLOG( 

316 f"[BenchMark.run] file '{look}' was found.") 

317 nb_cached += 1 

318 continue 

319 

320 # cache is available 

321 

322 # no cache 

323 self.fLOG( 

324 f"[BenchMark.run] {i + 1}/{len(params_list)}: {di}") 

325 dt = datetime.now() 

326 cl = perf_counter() 

327 tu = self.bench(**di) 

328 cl = perf_counter() - cl 

329 

330 if isinstance(tu, tuple): 

331 tus = [tu] 

332 elif isinstance(tu, list): 

333 tus = tu 

334 else: 

335 raise TypeError( # pragma: no cover 

336 "return of method bench must be a tuple of a list") 

337 

338 # checkings 

339 for tu in tus: 

340 met, app = tu 

341 if len(tu) != 2: 

342 raise TypeError( # pragma: no cover 

343 "Method run should return a tuple with 2 elements.") 

344 if "_btry" not in met: 

345 raise KeyError( # pragma: no cover 

346 "Metrics should contain key '_btry'.") 

347 if "_btry" not in app: 

348 raise KeyError( # pragma: no cover 

349 "Appendix should contain key '_btry'.") 

350 

351 for met, app in tus: 

352 met["_date"] = dt 

353 dt = datetime.now() - dt 

354 if not isinstance(met, dict): 

355 raise TypeError( # pragma: no cover 

356 "metrics should be a dictionary") 

357 if "_time" in met: 

358 raise KeyError( # pragma: no cover 

359 "key _time should not be the returned metrics") 

360 if "_span" in met: 

361 raise KeyError( # pragma: no cover 

362 "key _span should not be the returned metrics") 

363 if "_i" in met: 

364 raise KeyError( # pragma: no cover 

365 "key _i should not be in the returned metrics") 

366 if "_name" in met: 

367 raise KeyError( # pragma: no cover 

368 "key _name should not be the returned metrics") 

369 met["_time"] = cl 

370 met["_span"] = dt 

371 met["_i"] = i 

372 met["_name"] = self.Name 

373 self._metrics.append(met) 

374 app["_i"] = i 

375 self._appendix.append(app) 

376 self.fLOG( 

377 f"[BenchMark.run] {i + 1}/{len(params_list)} end {met}") 

378 

379 def graph_(): 

380 "local function" 

381 self.fLOG(f"[BenchMark.run] graph {self.Name} do") 

382 self._graphs = self.graphs(self._path_to_images) 

383 if self._graphs is None or not isinstance(self._graphs, list): 

384 raise TypeError( # pragma: no cover 

385 "Method graphs does not return anything.") 

386 for tu in self._graphs: 

387 if not isinstance(tu, self.LocalGraph): 

388 raise TypeError( # pragma: no cover 

389 "Method graphs should return a list of LocalGraph.") 

390 self.fLOG(f"[BenchMark.run] graph {self.Name} done") 

391 self.fLOG(f"[BenchMark.run] Received {len(self._graphs)} graphs.") 

392 self.fLOG(f"[BenchMark.run] end {self.Name} do") 

393 self.end() 

394 self.fLOG(f"[BenchMark.run] end {self.Name} done") 

395 meta["time_end"] = datetime.now() 

396 meta["nb_cached"] = nb_cached 

397 

398 # write information about run experiments 

399 def final_(): 

400 "local function" 

401 if self._cache_file is not None: 

402 self.fLOG(f"[BenchMark.run] save cache '{self._cache_file}'") 

403 cached = dict(metrics=self._metrics, 

404 appendix=self._appendix, params_list=params_list) 

405 with open(self._cache_file, "wb") as f: 

406 self._pickle.dump(cached, f) 

407 for di in self._metrics: 

408 look = f"{self._cache_file}.{di['_btry']}.clean_cache" 

409 with open(look, "w") as f: 

410 f.write( 

411 "Remove this file if you want to force a new run.") 

412 self.fLOG(f"[BenchMark.run] wrote '{look}'.") 

413 

414 self.fLOG("[BenchMark.run] done.") 

415 

416 progress = self._progressbar if self._progressbar is not None else range 

417 functions = [cache_, run_, graph_, final_] 

418 pgbar0 = progress(0, len(functions)) 

419 if self._progressbar: 

420 self._progressbars = [pgbar0] 

421 for i in pgbar0: 

422 if i == 1: 

423 pgbar = progress(len(params_list)) 

424 if self._progressbar: 

425 self._progressbars.append(pgbar) 

426 functions[i](pgbar) 

427 if self._progressbar: 

428 self._progressbars.pop() 

429 else: 

430 functions[i]() 

431 

432 self._progressbars = None 

433 return self._metrics, self._metadata 

434 

435 @property 

436 def Metrics(self): 

437 """ 

438 Returns the metrics. 

439 """ 

440 if not hasattr(self, "_metrics"): 

441 raise KeyError( # pragma: no cover 

442 "Method run was not run, no metrics was found.") 

443 return self._metrics 

444 

445 @property 

446 def Metadata(self): 

447 """ 

448 Returns the metrics. 

449 """ 

450 if not hasattr(self, "_metadata"): 

451 raise KeyError( # pragma: no cover 

452 "Method run was not run, no metadata was found.") 

453 return self._metadata 

454 

455 @property 

456 def Appendix(self): 

457 """ 

458 Returns the metrics. 

459 """ 

460 if not hasattr(self, "_appendix"): 

461 raise KeyError( # pragma: no cover 

462 "Method run was not run, no metadata was found.") 

463 return self._appendix 

464 

465 def to_df(self, convert=False, add_link=False, format="html"): 

466 """ 

467 Converts the metrics into a dataframe. 

468 

469 @param convert if True, calls method *_convert* on each cell 

470 @param add_link add hyperlink 

471 @param format format for hyperlinks (html or rst) 

472 @return dataframe 

473 """ 

474 import pandas 

475 df = pandas.DataFrame(self.Metrics) 

476 if convert: 

477 for c, d in zip(df.columns, df.dtypes): 

478 cols = [] 

479 for i in range(df.shape[0]): 

480 cols.append(self._convert(df, i, c, d, df.loc[i, c])) 

481 df[c] = cols 

482 col1 = list(sorted(_ for _ in df.columns if _.startswith("_"))) 

483 col2 = list(sorted(_ for _ in df.columns if not _.startswith("_"))) 

484 df = df[col1 + col2] 

485 if add_link and "_i" in df.columns: 

486 if format == "html": 

487 if "_btry" in df.columns: 

488 df["_btry"] = df.apply( 

489 lambda row: f"<a href=\"#{row['_i']}\">{row['_btry']}</a>", axis=1) 

490 df["_i"] = df["_i"].apply( 

491 lambda s: '<a href="#{0}">{0}</a>'.format(s)) 

492 elif format == "rst": 

493 if "_btry" in df.columns: 

494 df["_btry"] = df.apply( 

495 lambda row: f":ref:`{row['_btry']} <l-{self.Name}-{row['_i']}>`", axis=1) 

496 df["_i"] = df["_i"].apply( 

497 lambda s: ':ref:`{0} <l-{1}-{0}>`'.format(s, self.Name)) 

498 else: 

499 raise ValueError( # pragma: no cover 

500 "Format should be rst or html.") 

501 return df 

502 

503 def meta_to_df(self, convert=False, add_link=False, format="html"): 

504 """ 

505 Converts meta data into a dataframe 

506 

507 @param convert if True, calls method *_convert* on each cell 

508 @param add_link add hyperlink 

509 @param format format for hyperlinks (html or rst) 

510 @return dataframe 

511 """ 

512 import pandas 

513 df = pandas.DataFrame(self.Metadata) 

514 if convert: 

515 for c, d in zip(df.columns, df.dtypes): 

516 cols = [] 

517 for i in range(df.shape[0]): 

518 cols.append(self._convert(df, i, c, d, df.loc[i, c])) 

519 df[c] = cols 

520 col1 = list(sorted(_ for _ in df.columns if _.startswith("_"))) 

521 col2 = list(sorted(_ for _ in df.columns if not _.startswith("_"))) 

522 if add_link and "_i" in df.columns: 

523 if format == "html": 

524 if "_btry" in df.columns: 

525 df["_btry"] = df.apply( 

526 lambda row: f"<a href=\"#{row['_i']}\">{row['_btry']}</a>", axis=1) 

527 df["_i"] = df["_i"].apply( 

528 lambda s: '<a href="#{0}">{0}</a>'.format(s)) 

529 elif format == "rst": 

530 if "_btry" in df.columns: 

531 df["_btry"] = df.apply( 

532 lambda row: f":ref:`{row['_btry']} <l-{self.Name}-{row['_i']}>`", axis=1) 

533 df["_i"] = df["_i"].apply( 

534 lambda s: ':ref:`{0} <l-{1}-{0}>'.format(s, self.Name)) 

535 else: 

536 raise ValueError( # pragma: no cover 

537 "Format should be rst or html.") 

538 return df[col1 + col2] 

539 

540 def report(self, css=None, template_html=None, template_rst=None, engine="mako", filecsv=None, 

541 filehtml=None, filerst=None, params_html=None, title=None, description=None): 

542 """ 

543 Produces a report. 

544 

545 @param css css (will take the default one if empty) 

546 @param template_html template HTML (:epkg:`mako` or :epkg:`jinja2`) 

547 @param template_rst template RST (:epkg:`mako` or :epkg:`jinja2`) 

548 @param engine ``'mako``' or '``jinja2'`` 

549 @param filehtml report will written in this file if not None 

550 @param filecsv metrics will be written as a flat table 

551 @param filerst metrics will be written as a RST table 

552 @param params_html parameter to send to function :epkg:`pandas:DataFrame.to_html` 

553 @param title title (Name if any) 

554 @param description add a description 

555 @return dictionary {format: content} 

556 

557 You can define your own template by looking into the default ones 

558 defines in this class (see the bottom of this file). 

559 By default, HTML and RST report are generated. 

560 """ 

561 if template_html is None: 

562 template_html = BenchMark.default_template_html 

563 if template_rst is None: 

564 template_rst = BenchMark.default_template_rst 

565 if css is None: 

566 css = BenchMark.default_css 

567 if params_html is None: 

568 params_html = dict() 

569 if title is None: 

570 title = self.Name # pragma: no cover 

571 if "escape" not in params_html: 

572 params_html["escape"] = False 

573 

574 for gr in self.Graphs: 

575 gr.add("root", os.path.dirname(filehtml)) 

576 

577 # I don't like that too much as it is not multithreaded. 

578 # Avoid truncation. 

579 import pandas 

580 

581 if description is None: 

582 description = "" # pragma: no cover 

583 

584 contents = {'df': self.to_df()} 

585 

586 # HTML 

587 if template_html is not None and len(template_html) > 0: 

588 old_width = pandas.get_option('display.max_colwidth') 

589 pandas.set_option('display.max_colwidth', None) 

590 res = apply_template(template_html, dict(description=description, title=title, 

591 css=css, bench=self, params_html=params_html)) 

592 # Restore previous value. 

593 pandas.set_option('display.max_colwidth', old_width) 

594 

595 if filehtml is not None: 

596 with open(filehtml, "w", encoding="utf-8") as f: 

597 f.write(res) 

598 contents["html"] = res 

599 

600 # RST 

601 if template_rst is not None and len(template_rst) > 0: 

602 old_width = pandas.get_option('display.max_colwidth') 

603 pandas.set_option('display.max_colwidth', None) 

604 

605 res = apply_template(template_rst, dict(description=description, 

606 title=title, bench=self, df2rst=df2rst)) 

607 

608 # Restore previous value. 

609 pandas.set_option('display.max_colwidth', old_width) 

610 

611 with open(filerst, "w", encoding="utf-8") as f: 

612 f.write(res) 

613 contents["rst"] = res 

614 

615 # CSV 

616 if filecsv is not None: 

617 contents['df'].to_csv( 

618 filecsv, encoding="utf-8", index=False, sep="\t") 

619 return contents 

620 

621 def _convert(self, df, i, col, ty, value): 

622 """ 

623 Converts a value knowing its column, its type 

624 into something readable. 

625 

626 @param df dataframe 

627 @param i line index 

628 @param col column name 

629 @param ty type 

630 @param value value to convert 

631 @return value 

632 """ 

633 return value 

634 

635 @property 

636 def Graphs(self): 

637 """ 

638 Returns images of graphs. 

639 """ 

640 if not hasattr(self, "_graphs"): 

641 raise KeyError("unable to find _graphs") # pragma: no cover 

642 return self._graphs 

643 

644 default_css = """ 

645 .datagrid table { border-collapse: collapse; border-spacing: 0; width: 100%; 

646 table-layout: fixed; font-family: Verdana; font-size: 12px; 

647 word-wrap: break-word; } 

648 

649 .datagrid thead { 

650 cursor: pointer; 

651 background: #c9dff0; 

652 } 

653 .datagrid thead tr th { 

654 font-weight: bold; 

655 padding: 12px 30px; 

656 padding-left: 12px; 

657 } 

658 .datagrid thead tr th span { 

659 padding-right: 10px; 

660 background-repeat: no-repeat; 

661 text-align: left; 

662 } 

663 

664 .datagrid tbody tr { 

665 color: #555; 

666 } 

667 .datagrid tbody td { 

668 text-align: center; 

669 padding: 10px 5px; 

670 } 

671 .datagrid tbody th { 

672 text-align: left; 

673 padding: 10px 5px; 

674 } 

675 """.replace(" ", "") 

676 

677 default_template_html = """ 

678 <html> 

679 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> 

680 <style> 

681 ${css} 

682 </style> 

683 <body> 

684 <h1>${title}</h1> 

685 ${description} 

686 <ul> 

687 <li><a href="#metadata">Metadata</a></li> 

688 <li><a href="#metrics">Metrics</a></li> 

689 <li><a href="#graphs">Graphs</a></li> 

690 <li><a href="#appendix">Appendix</a></li> 

691 </ul> 

692 <h2 id="metadata">Metadata</h2> 

693 <div class="datagrid"> 

694 ${bench.meta_to_df(convert=True, add_link=True).to_html(**params_html)} 

695 </div> 

696 <h2 id="metrics">Metrics</h2> 

697 <div class="datagrid"> 

698 ${bench.to_df(convert=True, add_link=True).to_html(**params_html)} 

699 </div> 

700 % if len(bench.Graphs) > 0: 

701 <h2 id="graphs">Graphs</h2> 

702 % for gr in bench.Graphs: 

703 ${gr.to_html()} 

704 % endfor 

705 % endif 

706 % if len(bench.Appendix) > 0: 

707 <h2 id="appendix">Appendix</h2> 

708 <div class="appendix"> 

709 % for met, app in zip(bench.Metrics, bench.Appendix): 

710 <h3 id="${app["_i"]}">${app["_btry"]}</h3> 

711 <ul> 

712 % for k, v in sorted(app.items()): 

713 % if isinstance(v, str) and "\\n" in v: 

714 <li>I <b>${k}</b>: <pre>${v}</pre></li> 

715 % else: 

716 <li>I <b>${k}</b>: ${v}</li> 

717 % endif 

718 % endfor 

719 % for k, v in sorted(met.items()): 

720 % if isinstance(v, str) and "\\n" in v: 

721 <li>M <b>${k}</b>: <pre>${v}</pre></li> 

722 % else: 

723 <li>M <b>${k}</b>: ${v}</li> 

724 % endif 

725 % endfor 

726 </ul> 

727 % endfor 

728 % endif 

729 </div> 

730 </body> 

731 </html> 

732 """.replace(" ", "") 

733 

734 default_template_rst = """ 

735 

736 .. _lb-${bench.Name}: 

737 

738 ${title} 

739 ${"=" * len(title)} 

740 

741 .. contents:: 

742 :local: 

743 

744 ${description} 

745 

746 Metadata 

747 -------- 

748 

749 ${df2rst(bench.meta_to_df(convert=True, add_link=True, format="rst"), index=True, list_table=True)} 

750 

751 Metrics 

752 -------- 

753 

754 ${df2rst(bench.to_df(convert=True, add_link=True, format="rst"), index=True, list_table=True)} 

755 

756 % if len(bench.Graphs) > 0: 

757 

758 Graphs 

759 ------ 

760 

761 % for gr in bench.Graphs: 

762 ${gr.to_rst()} 

763 % endfor 

764 

765 % endif 

766 

767 % if len(bench.Appendix) > 0: 

768 

769 Appendix 

770 -------- 

771 

772 % for met, app in zip(bench.Metrics, bench.Appendix): 

773 

774 .. _l-${bench.Name}-${app["_i"]}: 

775 

776 ${app["_btry"]} 

777 ${"+" * len(app["_btry"])} 

778 

779 % for k, v in sorted(app.items()): 

780 % if isinstance(v, str) and "\\n" in v: 

781 * I **${k}**: 

782 :: 

783 

784 ${"\\n ".join(v.split("\\n"))} 

785 

786 % else: 

787 * M **${k}**: ${v} 

788 % endif 

789 % endfor 

790 

791 % for k, v in sorted(met.items()): 

792 % if isinstance(v, str) and "\\n" in v: 

793 * **${k}**: 

794 :: 

795 

796 ${"\\n ".join(v.split("\\n"))} 

797 

798 % else: 

799 * **${k}**: ${v} 

800 % endif 

801 % endfor 

802 

803 % endfor 

804 % endif 

805 """.replace(" ", "")