Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ on:
# reference reads the sources.
- 'src/**'
- 'examples/**'
- 'benchmarks/**'
- 'fpm.toml'
pull_request:
paths: *paths
Expand Down Expand Up @@ -107,6 +108,13 @@ jobs:
sphinx-build --version
ford --version

# Adds benchmarks/results/github-actions.json, which docs/conf.py renders
# on the benchmarks page. Not committed; timings from the CI runner.
- name: Run benchmarks
run: |
pip install -r benchmarks/requirements.txt
python benchmarks/run_benchmarks.py --repeats 5 --save github-actions

- name: Build Docs
env:
# Picked up by docs/conf.py as fplotlib_gallery_compiler.
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ __pycache__/
docs/_build/
docs/auto_gallery/
docs/sg_execution_times.rst

# generated docs content
docs/_generated/
benchmarks/results/github-actions.json
29 changes: 29 additions & 0 deletions benchmarks/animation.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
program bench_animation
use fplotlib
implicit none
integer, parameter :: n = 240, n_frames = 40
real(dp), parameter :: pi = 3.14159265358979323846_dp
real(dp) :: x(n), y(n), envelope(n), phase
integer :: frame, i

do i = 1, n
x(i) = 10.0_dp*real(i - 1, dp)/real(n - 1, dp)
end do
envelope = exp(-0.15_dp*x)

do frame = 1, n_frames
phase = 2.0_dp*pi*real(frame - 1, dp)/real(n_frames, dp)
y = sin(2.0_dp*x - phase)*envelope
call clf()
call plot(x, y, "b-", lw=2.0_dp)
call fill_between(x, y, color="b", alpha=0.2_dp)
call xlim(0.0_dp, 10.0_dp)
call ylim(-1.1_dp, 1.1_dp)
call title("Travelling wave")
call xlabel("x")
call ylabel("displacement")
call grid(.true.)
call add_frame()
end do
call save_animation("animation_f.gif", fps=20.0_dp)
end program bench_animation
36 changes: 36 additions & 0 deletions benchmarks/animation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import sys
import time
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.animation import FuncAnimation, PillowWriter


def main(ext="gif"):
n_frames = 40
x = np.linspace(0, 10, 240)
envelope = np.exp(-0.15*x)
fig, ax = plt.subplots()

# Every frame is cleared and redrawn, like clf() + plot() in the Fortran program.
def draw(frame):
ax.clear()
y = np.sin(2*x - 2*np.pi*frame/n_frames)*envelope
ax.plot(x, y, "b-", lw=2)
ax.fill_between(x, y, color="b", alpha=0.2)
ax.set_xlim(0, 10)
ax.set_ylim(-1.1, 1.1)
ax.set_title("Travelling wave")
ax.set_xlabel("x")
ax.set_ylabel("displacement")
ax.grid(True)

anim = FuncAnimation(fig, draw, frames=n_frames)
anim.save("animation_py." + ext, writer=PillowWriter(fps=20))


if __name__ == "__main__":
t0 = time.perf_counter()
main(*sys.argv[1:2])
print(f"RUN_SECONDS {time.perf_counter() - t0:.6f}")
31 changes: 31 additions & 0 deletions benchmarks/complex.f90
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
program bench_complex
use fplotlib
implicit none
integer, parameter :: n = 5000
real(dp) :: x(n), y1(n), y2(n), y3(n)
integer :: i
character(len=:), allocatable :: ext
character(len=16) :: arg

ext = "png"
if (command_argument_count() > 0) then
call get_command_argument(1, arg)
ext = trim(arg)
end if
do i = 1, n
x(i) = 20.0_dp*real(i - 1, dp)/real(n - 1, dp)
y1(i) = sin(x(i))*exp(-0.1_dp*x(i))
y2(i) = cos(2.0_dp*x(i))*exp(-0.05_dp*x(i))
y3(i) = 0.5_dp*sin(0.5_dp*x(i))
end do
call plot(x, y1, "b-", label="damped sin", lw=2.0_dp)
call plot(x, y2, "r--", label="damped cos")
call plot(x, y3, "g-", label="slow sin")
call fill_between(x, y1, y2, alpha=0.2_dp)
call title("Complex benchmark")
call xlabel("time")
call ylabel("amplitude")
call grid(.true.)
call legend()
call savefig("complex_f." // ext)
end program bench_complex
27 changes: 27 additions & 0 deletions benchmarks/complex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import sys
import time
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np

def main(ext="png"):
x = np.linspace(0, 20, 5000)
y1 = np.sin(x)*np.exp(-0.1*x)
y2 = np.cos(2*x)*np.exp(-0.05*x)
y3 = 0.5*np.sin(0.5*x)
plt.plot(x, y1, "b-", label="damped sin", lw=2)
plt.plot(x, y2, "r--", label="damped cos")
plt.plot(x, y3, "g-", label="slow sin")
plt.fill_between(x, y1, y2, alpha=0.2)
plt.title("Complex benchmark")
plt.xlabel("time")
plt.ylabel("amplitude")
plt.grid(True)
plt.legend()
plt.savefig("complex_py." + ext)

if __name__ == "__main__":
t0 = time.perf_counter()
main(*sys.argv[1:2])
print(f"RUN_SECONDS {time.perf_counter() - t0:.6f}")
67 changes: 67 additions & 0 deletions benchmarks/render_results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Turn benchmarks/results/*.json into a Markdown page with collapsible tables.

Used by docs/conf.py, so the docs always show whatever is in the results
folder. Usage: python benchmarks/render_results.py OUTPUT.md
"""
import html
import json
import sys
from pathlib import Path

RESULTS = Path(__file__).resolve().parent / "results"
MACHINE_LABELS = [
("cpu", "CPU"), ("cpu_ghz", "Clock speed (GHz)"), ("os", "OS"),
("fortran_compiler", "Fortran compiler"), ("fortran_flags", "Fortran flags"),
("fpm", "fpm"), ("python", "Python"), ("matplotlib", "Matplotlib"),
("numpy", "NumPy"),
]


def e(x):
return html.escape(str(x))


def section(path, data):
m = data["machine"]
out = [f"<details><summary><b>{e(m.get('cpu', path.stem))}</b> &mdash; "
f"{e(m.get('os', ''))} &mdash; {e(data.get('date', ''))} "
f"(<code>{e(path.name)}</code>)</summary>", "<h4>Machine</h4>", "<table>"]
for key, label in MACHINE_LABELS:
if m.get(key) is not None:
out.append(f"<tr><th>{label}</th><td>{e(m[key])}</td></tr>")
out.append("</table>")
out.append(f"<h4>Results (median of {e(data.get('repeats', '?'))} runs)</h4>")
out.append("<table><tr><th>Benchmark</th><th>fplotlib (s)</th>"
"<th>matplotlib (s)</th></tr>")
results = {}
for r in data["results"]:
variant = r["variant"]
if variant.startswith(("fplotlib", "Fortran compile")):
column = "fplotlib"
elif variant.startswith(("matplotlib", "Python imports")):
column = "matplotlib"
else:
raise ValueError(f"Unknown benchmark variant: {variant}")
timings = results.setdefault(r["benchmark"], {})
if column in timings:
raise ValueError(f"Duplicate {column} result for {r['benchmark']}")
timings[column] = f"{r['seconds']:.4f}"
for benchmark, timings in results.items():
out.append(f"<tr><td>{e(benchmark)}</td><td>{timings.get('fplotlib', '')}</td>"
f"<td>{timings.get('matplotlib', '')}</td></tr>")
out.append("</table></details>")
return "\n".join(out)


def render():
blocks = []
for path in sorted(RESULTS.glob("*.json")):
blocks.append("```{raw} html\n" + section(path, json.loads(path.read_text()))
+ "\n```\n")
if not blocks:
return "*No results have been recorded yet.*\n"
return "\n".join(blocks)


if __name__ == "__main__":
Path(sys.argv[1]).write_text(render())
2 changes: 2 additions & 0 deletions benchmarks/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
matplotlib
numpy
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
{
"date": "2026-11-07",
"repeats": 5,
"machine": {
"cpu": "Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz",
"cpu_ghz": 0.4,
"os": "Linux",
"fortran_compiler": "GNU Fortran (Ubuntu 15.2.0-16ubuntu1) 15.2.0",
"fortran_flags": "-O3 -march=native",
"fpm": "Version: 0.13.0, alpha",
"python": "CPython 3.14.4",
"matplotlib": "3.11.2",
"numpy": "2.3.5"
},
"results": [
{
"benchmark": "Compile library",
"variant": "fplotlib (fpm build, -O3)",
"seconds": 21.074636
},
{
"benchmark": "Compile simple program",
"variant": "Fortran compile to binary",
"seconds": 0.110779
},
{
"benchmark": "Import simple script",
"variant": "Python imports (matplotlib, numpy)",
"seconds": 0.43922
},
{
"benchmark": "Run simple -> png",
"variant": "fplotlib",
"seconds": 0.048328
},
{
"benchmark": "Run simple -> png",
"variant": "matplotlib (excl. imports)",
"seconds": 0.074703
},
{
"benchmark": "Run simple -> pdf",
"variant": "fplotlib",
"seconds": 0.002185
},
{
"benchmark": "Run simple -> pdf",
"variant": "matplotlib (excl. imports)",
"seconds": 0.133638
},
{
"benchmark": "Compile complex program",
"variant": "Fortran compile to binary",
"seconds": 0.11186
},
{
"benchmark": "Import complex script",
"variant": "Python imports (matplotlib, numpy)",
"seconds": 0.425692
},
{
"benchmark": "Run complex -> png",
"variant": "fplotlib",
"seconds": 0.069669
},
{
"benchmark": "Run complex -> png",
"variant": "matplotlib (excl. imports)",
"seconds": 0.152285
},
{
"benchmark": "Run complex -> pdf",
"variant": "fplotlib",
"seconds": 0.056001
},
{
"benchmark": "Run complex -> pdf",
"variant": "matplotlib (excl. imports)",
"seconds": 0.226113
},
{
"benchmark": "Compile animation program",
"variant": "Fortran compile to binary",
"seconds": 0.090391
},
{
"benchmark": "Import animation script",
"variant": "Python imports (matplotlib, numpy)",
"seconds": 0.43272
},
{
"benchmark": "Run animation -> gif",
"variant": "fplotlib",
"seconds": 0.966446
},
{
"benchmark": "Run animation -> gif",
"variant": "matplotlib (excl. imports)",
"seconds": 4.471388
}
]
}
18 changes: 18 additions & 0 deletions benchmarks/results/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Benchmark results

One JSON file per machine and run. Each records the CPU and its clock speed, the OS (Windows, Linux or macOS), Fortran compiler
and flags, Python, Matplotlib and NumPy versions, and the timings.

To contribute a result:

```
pip install -r benchmarks/requirements.txt
python benchmarks/run_benchmarks.py --repeats 5 --save
```

This needs `fpm` and `gfortran` on the PATH. It writes
`benchmarks/results/<date>-<cpu>.json`; commit that file and open a pull
request. The docs list every file here as a collapsible section.

`github-actions.json` is produced by the docs workflow at build time and is
not committed.
Loading
Loading