Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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('{0}="{1}"'.format(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 '<img src="{0}" {1}/>'.format(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 '.. image:: {0}'.format(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("[BenchMark.run] retrieve cache '{0}'".format( 

266 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("[BenchMark.run] cache not found '{0}'".format( 

274 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("[BenchMark.run] init {0} do".format(self.Name)) 

286 self.init() 

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

288 self.fLOG("[BenchMark.run] start {0}".format(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 "[BenchMark.run] file '{0}' was not found --> run again.".format(look)) 

310 if can: 

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

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

313 self.fLOG( 

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

315 self.fLOG( 

316 "[BenchMark.run] file '{0}' was found.".format(look)) 

317 nb_cached += 1 

318 continue 

319 

320 # cache is available 

321 

322 # no cache 

323 self.fLOG( 

324 "[BenchMark.run] {0}/{1}: {2}".format(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 "[BenchMark.run] {0}/{1} end {2}".format(i + 1, len(params_list), met)) 

378 

379 def graph_(): 

380 "local function" 

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

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("[BenchMark.run] graph {0} done".format(self.Name)) 

391 self.fLOG("[BenchMark.run] Received {0} graphs.".format( 

392 len(self._graphs))) 

393 self.fLOG("[BenchMark.run] end {0} do".format(self.Name)) 

394 self.end() 

395 self.fLOG("[BenchMark.run] end {0} done".format(self.Name)) 

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

397 meta["nb_cached"] = nb_cached 

398 

399 # write information about run experiments 

400 def final_(): 

401 "local function" 

402 if self._cache_file is not None: 

403 self.fLOG("[BenchMark.run] save cache '{0}'".format( 

404 self._cache_file)) 

405 cached = dict(metrics=self._metrics, 

406 appendix=self._appendix, params_list=params_list) 

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

408 self._pickle.dump(cached, f) 

409 for di in self._metrics: 

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

411 self._cache_file, di["_btry"]) 

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

413 f.write( 

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

415 self.fLOG("[BenchMark.run] wrote '{0}'.".format(look)) 

416 

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

418 

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

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

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

422 if self._progressbar: 

423 self._progressbars = [pgbar0] 

424 for i in pgbar0: 

425 if i == 1: 

426 pgbar = progress(len(params_list)) 

427 if self._progressbar: 

428 self._progressbars.append(pgbar) 

429 functions[i](pgbar) 

430 if self._progressbar: 

431 self._progressbars.pop() 

432 else: 

433 functions[i]() 

434 

435 self._progressbars = None 

436 return self._metrics, self._metadata 

437 

438 @property 

439 def Metrics(self): 

440 """ 

441 Returns the metrics. 

442 """ 

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

444 raise KeyError( # pragma: no cover 

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

446 return self._metrics 

447 

448 @property 

449 def Metadata(self): 

450 """ 

451 Returns the metrics. 

452 """ 

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

454 raise KeyError( # pragma: no cover 

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

456 return self._metadata 

457 

458 @property 

459 def Appendix(self): 

460 """ 

461 Returns the metrics. 

462 """ 

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

464 raise KeyError( # pragma: no cover 

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

466 return self._appendix 

467 

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

469 """ 

470 Converts the metrics into a dataframe. 

471 

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

473 @param add_link add hyperlink 

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

475 @return dataframe 

476 """ 

477 import pandas 

478 df = pandas.DataFrame(self.Metrics) 

479 if convert: 

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

481 cols = [] 

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

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

484 df[c] = cols 

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

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

487 df = df[col1 + col2] 

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

489 if format == "html": 

490 if "_btry" in df.columns: 

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

492 lambda row: '<a href="#{0}">{1}</a>'.format(row["_i"], row["_btry"]), axis=1) 

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

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

495 elif format == "rst": 

496 if "_btry" in df.columns: 

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

498 lambda row: ':ref:`{1} <l-{2}-{0}>`'.format(row["_i"], row["_btry"], self.Name), axis=1) 

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

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

501 else: 

502 raise ValueError( # pragma: no cover 

503 "Format should be rst or html.") 

504 return df 

505 

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

507 """ 

508 Converts meta data into a dataframe 

509 

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

511 @param add_link add hyperlink 

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

513 @return dataframe 

514 """ 

515 import pandas 

516 df = pandas.DataFrame(self.Metadata) 

517 if convert: 

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

519 cols = [] 

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

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

522 df[c] = cols 

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

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

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

526 if format == "html": 

527 if "_btry" in df.columns: 

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

529 lambda row: '<a href="#{0}">{1}</a>'.format(row["_i"], row["_btry"]), axis=1) 

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

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

532 elif format == "rst": 

533 if "_btry" in df.columns: 

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

535 lambda row: ':ref:`{1} <l-{2}-{0}>`'.format(row["_i"], row["_btry"], self.Name), axis=1) 

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

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

538 else: 

539 raise ValueError( # pragma: no cover 

540 "Format should be rst or html.") 

541 return df[col1 + col2] 

542 

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

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

545 """ 

546 Produces a report. 

547 

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

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

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

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

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

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

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

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

556 @param title title (Name if any) 

557 @param description add a description 

558 @return dictionary {format: content} 

559 

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

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

562 By default, HTML and RST report are generated. 

563 """ 

564 if template_html is None: 

565 template_html = BenchMark.default_template_html 

566 if template_rst is None: 

567 template_rst = BenchMark.default_template_rst 

568 if css is None: 

569 css = BenchMark.default_css 

570 if params_html is None: 

571 params_html = dict() 

572 if title is None: 

573 title = self.Name # pragma: no cover 

574 if "escape" not in params_html: 

575 params_html["escape"] = False 

576 

577 for gr in self.Graphs: 

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

579 

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

581 # Avoid truncation. 

582 import pandas 

583 

584 if description is None: 

585 description = "" # pragma: no cover 

586 

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

588 

589 # HTML 

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

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

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

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

594 css=css, bench=self, params_html=params_html)) 

595 # Restore previous value. 

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

597 

598 if filehtml is not None: 

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

600 f.write(res) 

601 contents["html"] = res 

602 

603 # RST 

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

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

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

607 

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

609 title=title, bench=self, df2rst=df2rst)) 

610 

611 # Restore previous value. 

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

613 

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

615 f.write(res) 

616 contents["rst"] = res 

617 

618 # CSV 

619 if filecsv is not None: 

620 contents['df'].to_csv( 

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

622 return contents 

623 

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

625 """ 

626 Converts a value knowing its column, its type 

627 into something readable. 

628 

629 @param df dataframe 

630 @param i line index 

631 @param col column name 

632 @param ty type 

633 @param value value to convert 

634 @return value 

635 """ 

636 return value 

637 

638 @property 

639 def Graphs(self): 

640 """ 

641 Returns images of graphs. 

642 """ 

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

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

645 return self._graphs 

646 

647 default_css = """ 

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

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

650 word-wrap: break-word; } 

651 

652 .datagrid thead { 

653 cursor: pointer; 

654 background: #c9dff0; 

655 } 

656 .datagrid thead tr th { 

657 font-weight: bold; 

658 padding: 12px 30px; 

659 padding-left: 12px; 

660 } 

661 .datagrid thead tr th span { 

662 padding-right: 10px; 

663 background-repeat: no-repeat; 

664 text-align: left; 

665 } 

666 

667 .datagrid tbody tr { 

668 color: #555; 

669 } 

670 .datagrid tbody td { 

671 text-align: center; 

672 padding: 10px 5px; 

673 } 

674 .datagrid tbody th { 

675 text-align: left; 

676 padding: 10px 5px; 

677 } 

678 """.replace(" ", "") 

679 

680 default_template_html = """ 

681 <html> 

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

683 <style> 

684 ${css} 

685 </style> 

686 <body> 

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

688 ${description} 

689 <ul> 

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

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

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

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

694 </ul> 

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

696 <div class="datagrid"> 

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

698 </div> 

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

700 <div class="datagrid"> 

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

702 </div> 

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

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

705 % for gr in bench.Graphs: 

706 ${gr.to_html()} 

707 % endfor 

708 % endif 

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

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

711 <div class="appendix"> 

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

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

714 <ul> 

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

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

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

718 % else: 

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

720 % endif 

721 % endfor 

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

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

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

725 % else: 

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

727 % endif 

728 % endfor 

729 </ul> 

730 % endfor 

731 % endif 

732 </div> 

733 </body> 

734 </html> 

735 """.replace(" ", "") 

736 

737 default_template_rst = """ 

738 

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

740 

741 ${title} 

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

743 

744 .. contents:: 

745 :local: 

746 

747 ${description} 

748 

749 Metadata 

750 -------- 

751 

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

753 

754 Metrics 

755 -------- 

756 

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

758 

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

760 

761 Graphs 

762 ------ 

763 

764 % for gr in bench.Graphs: 

765 ${gr.to_rst()} 

766 % endfor 

767 

768 % endif 

769 

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

771 

772 Appendix 

773 -------- 

774 

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

776 

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

778 

779 ${app["_btry"]} 

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

781 

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

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

784 * I **${k}**: 

785 :: 

786 

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

788 

789 % else: 

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

791 % endif 

792 % endfor 

793 

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

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

796 * **${k}**: 

797 :: 

798 

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

800 

801 % else: 

802 * **${k}**: ${v} 

803 % endif 

804 % endfor 

805 

806 % endfor 

807 % endif 

808 """.replace(" ", "")