Skip to content

Commit ca9faa1

Browse files
committed
Visualizations to GitHub pages
1 parent 2142cbf commit ca9faa1

File tree

5 files changed

+161
-1
lines changed

5 files changed

+161
-1
lines changed

.github/workflows/gh-pages.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ jobs:
3838
run: src/python/build_site.py
3939
- name: Create overview
4040
run: bmp-create-overview --html-file=_site/index.html
41+
- name: Create reports
42+
run: python3 vis/generate_petab_reports.py --output-dir=_site/reports
4143
- name: Setup Pages
4244
uses: actions/configure-pages@v5
4345
- name: Upload artifact

src/python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ dependencies = [
2828

2929
[project.optional-dependencies]
3030
dev = ["pre-commit", "pytest", "ruff"]
31-
site = ["bokeh>=3.7.3"]
31+
site = ["bokeh>=3.7.3", "petab[vis]", "Jinja2>=3.0.3"]
3232

3333
[project.scripts]
3434
bmp-petablint = "benchmark_models_petab.check_petablint:main"

vis/generate_petab_reports.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import argparse
2+
import base64
3+
import io
4+
from pathlib import Path
5+
6+
import matplotlib.pyplot as plt
7+
import pandas as pd
8+
import petab.v1
9+
import petab.v1.visualize.plotter
10+
from jinja2 import Environment, FileSystemLoader
11+
from petab.visualize import plot_problem
12+
13+
import benchmark_models_petab as bmp
14+
15+
# plot simulation line without markers
16+
petab.v1.visualize.plotter.simulation_line_kwargs["marker"] = ""
17+
18+
def _plot_problem(problem: petab.v1.Problem, sim_df: pd.DataFrame) -> str:
19+
"""Plot measurement points and simulation line for one observable and return base64 png."""
20+
plot_problem(problem, simulations_df=sim_df)
21+
fig = plt.gcf()
22+
23+
buf = io.BytesIO()
24+
fig.savefig(buf, format="png", dpi=150)
25+
plt.close(fig)
26+
buf.seek(0)
27+
img_b64 = base64.b64encode(buf.read()).decode("ascii")
28+
return img_b64
29+
30+
31+
def generate_report_for_model(problem_id: str, template_env: Environment, out_dir: Path):
32+
problem = bmp.get_problem(problem_id)
33+
if problem is None:
34+
return
35+
36+
sim_df = bmp.get_simulation_df(problem_id)
37+
38+
images: list[dict[str, str]] = []
39+
img = _plot_problem(problem, sim_df)
40+
images.append({"id": problem_id, "img": img})
41+
42+
template = template_env.get_template("problem_report.html.jinja")
43+
rendered = template.render(model_name=problem_id, images=images)
44+
45+
out_dir.mkdir(parents=True, exist_ok=True)
46+
out_path = out_dir / f"{problem_id}.html"
47+
with open(out_path, "w", encoding="utf-8") as fh:
48+
fh.write(rendered)
49+
print(f"Wrote {out_path}")
50+
51+
52+
def generate_index(problem_id_list: list[str], template_env: Environment, out_dir: Path):
53+
"""Render an index page with links to per-problem reports."""
54+
template = template_env.get_template("index.html.jinja")
55+
rendered = template.render(models=problem_id_list, count=len(problem_id_list))
56+
57+
out_dir.mkdir(parents=True, exist_ok=True)
58+
out_path = out_dir / "index.html"
59+
with open(out_path, "w", encoding="utf-8") as fh:
60+
fh.write(rendered)
61+
print(f"Wrote {out_path}")
62+
63+
64+
def main(output_dir: Path = None):
65+
if output_dir is None:
66+
output_dir = Path(__file__).parent / "reports"
67+
templates_dir = Path(__file__).parent / "templates"
68+
69+
env = Environment(loader=FileSystemLoader(templates_dir), autoescape=True)
70+
problem_id_list = list(bmp.MODELS)
71+
72+
for problem_id in problem_id_list:
73+
try:
74+
generate_report_for_model(problem_id, env, output_dir)
75+
except Exception as e:
76+
print(f"Skipping {problem_id} due to error: {e}")
77+
78+
try:
79+
generate_index(problem_id_list, env, output_dir)
80+
except Exception as e:
81+
print(f"Failed to write index: {e}")
82+
83+
if __name__ == "__main__":
84+
parser = argparse.ArgumentParser(description="Generate PETab problem reports")
85+
parser.add_argument(
86+
"--output-dir",
87+
"-o",
88+
type=Path,
89+
default=None,
90+
help="Directory to write reports to (default: vis/reports)",
91+
)
92+
args = parser.parse_args()
93+
94+
main(args.output_dir)

vis/templates/index.html.jinja

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<title>PEtab problems overview</title>
6+
<style>
7+
body { font-family: Arial, sans-serif; margin: 20px; }
8+
ul { list-style: none; padding: 0; }
9+
li { margin: 6px 0; }
10+
a { text-decoration: none; color: #0366d6; }
11+
a:hover { text-decoration: underline; }
12+
</style>
13+
</head>
14+
<body>
15+
<h1>PEtab problems ({{ count }})</h1>
16+
{% if models %}
17+
<ul>
18+
{% for m in models %}
19+
<li><a href="{{ m }}.html">{{ m }}</a></li>
20+
{% endfor %}
21+
</ul>
22+
{% else %}
23+
<p>No problems found.</p>
24+
{% endif %}
25+
</body>
26+
</html>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!-- html -->
2+
<!doctype html>
3+
<html lang="en">
4+
<head>
5+
<meta charset="utf-8">
6+
<title>{{ model_name }} - PEtab report</title>
7+
<style>
8+
body { font-family: Arial, sans-serif; margin: 20px; }
9+
.figure { margin-bottom: 24px; }
10+
img { max-width: 100%; height: auto; border: 1px solid #ccc; }
11+
.top-nav { margin-bottom: 12px; }
12+
.top-nav a { text-decoration: none; color: #0366d6; margin-right: 12px; }
13+
.top-nav a:hover { text-decoration: underline; }
14+
</style>
15+
</head>
16+
<body>
17+
<div class="top-nav">
18+
<a href="index.html">&larr; Back to index</a>
19+
<a href="https://github.com/Benchmarking-Initiative/Benchmark-Models-PEtab/tree/master/Benchmark-Models/{{ model_name|urlencode }}" target="_blank" rel="noopener noreferrer">
20+
View PEtab files on GitHub
21+
</a>
22+
</div>
23+
24+
<h1>{{ model_name }}</h1>
25+
{% if images %}
26+
{% for img in images %}
27+
<div class="figure">
28+
{% if images|length > 1 %}
29+
<h3>{{ img.id }}</h3>
30+
{% endif %}
31+
<img src="data:image/png;base64,{{ img.img }}" alt="{{ img.id }}">
32+
</div>
33+
{% endfor %}
34+
{% else %}
35+
<p>No observables or data available for this problem.</p>
36+
{% endif %}
37+
</body>
38+
</html>

0 commit comments

Comments
 (0)