feat(heuropt-plot): v0.1.0 — SVG visualization companion crate

Adds heuropt-plot, a tiny SVG-only plotter that takes heuropt
results and emits scatter plots (pareto_front_svg) and line plots
(convergence_svg). No heavy 'plotters' or 'tiny-skia' dep — hand-
rolled SVG so the crate adds <100 KB to a build.

Workspace setup: root Cargo.toml gains [workspace] with members =
['.', 'heuropt-plot']. heuropt-plot has its own version (0.1.0) and
publishes independently against heuropt 0.7+.

Adds examples/visualize.rs that wires it all up: NSGA-II on Schaffer
N.1, observer closure recording per-generation hypervolume, two SVGs
written to disk.
This commit is contained in:
2026-05-05 15:26:22 -06:00
parent 41122b7d48
commit 8cf518200a
8 changed files with 634 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 450" font-family="system-ui, sans-serif" font-size="12">
<rect x="0" y="0" width="700" height="450" fill="white"/>
<text x="70" y="22" font-size="16" font-weight="bold">NSGA-II on Schaffer N.1 — hypervolume per generation</text>
<rect x="70" y="40" width="610" height="360" fill="none" stroke="#888" />
<line x1="70" y1="400" x2="70" y2="405" stroke="#888" />
<text x="70" y="418" text-anchor="middle">0</text>
<line x1="222.5" y1="400" x2="222.5" y2="405" stroke="#888" />
<text x="222.5" y="418" text-anchor="middle">25</text>
<line x1="375" y1="400" x2="375" y2="405" stroke="#888" />
<text x="375" y="418" text-anchor="middle">50</text>
<line x1="527.5" y1="400" x2="527.5" y2="405" stroke="#888" />
<text x="527.5" y="418" text-anchor="middle">75</text>
<line x1="680" y1="400" x2="680" y2="405" stroke="#888" />
<text x="680" y="418" text-anchor="middle">100</text>
<line x1="65" y1="400" x2="70" y2="400" stroke="#888" />
<text x="62" y="400" text-anchor="end" dominant-baseline="middle">9.552e1</text>
<line x1="65" y1="279.999999999999" x2="70" y2="279.999999999999" stroke="#888" />
<text x="62" y="279.999999999999" text-anchor="end" dominant-baseline="middle">9.608e1</text>
<line x1="65" y1="160.00000000000102" x2="70" y2="160.00000000000102" stroke="#888" />
<text x="62" y="160.00000000000102" text-anchor="end" dominant-baseline="middle">9.664e1</text>
<line x1="65" y1="40" x2="70" y2="40" stroke="#888" />
<text x="62" y="40" text-anchor="end" dominant-baseline="middle">9.721e1</text>
<text x="375" y="438" text-anchor="middle">generation</text>
<text x="15" y="220" text-anchor="middle" transform="rotate(-90 15 220)">hypervolume</text>
<polyline points="70.00,400.00 76.10,113.85 82.20,59.99 88.30,44.22 94.40,45.84 100.50,47.93 106.60,45.55 112.70,40.78 118.80,43.34 124.90,41.17 131.00,42.71 137.10,44.97 143.20,43.62 149.30,43.69 155.40,46.67 161.50,40.97 167.60,41.76 173.70,41.27 179.80,46.13 185.90,42.18 192.00,44.17 198.10,42.62 204.20,41.90 210.30,42.69 216.40,40.00 222.50,42.66 228.60,48.64 234.70,47.22 240.80,42.14 246.90,45.35 253.00,48.46 259.10,42.98 265.20,44.96 271.30,44.55 277.40,44.30 283.50,43.54 289.60,43.88 295.70,42.70 301.80,44.10 307.90,46.88 314.00,46.56 320.10,42.39 326.20,44.45 332.30,45.03 338.40,46.44 344.50,44.44 350.60,45.79 356.70,49.23 362.80,42.47 368.90,44.42 375.00,50.51 381.10,44.45 387.20,42.54 393.30,42.50 399.40,42.83 405.50,40.56 411.60,49.78 417.70,47.03 423.80,44.17 429.90,45.73 436.00,42.66 442.10,44.31 448.20,49.69 454.30,41.19 460.40,41.14 466.50,41.87 472.60,41.88 478.70,41.41 484.80,43.24 490.90,45.61 497.00,45.55 503.10,45.16 509.20,46.10 515.30,42.66 521.40,42.46 527.50,48.78 533.60,46.56 539.70,42.49 545.80,43.23 551.90,47.51 558.00,44.28 564.10,45.05 570.20,43.75 576.30,44.15 582.40,42.29 588.50,41.54 594.60,43.25 600.70,41.56 606.80,48.07 612.90,42.87 619.00,42.68 625.10,43.69 631.20,45.47 637.30,42.93 643.40,40.77 649.50,42.53 655.60,42.89 661.70,44.33 667.80,41.27 673.90,44.06 680.00,41.55" fill="none" stroke="#1f77b4" stroke-width="1.5" />
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

+73
View File
@@ -0,0 +1,73 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 450" font-family="system-ui, sans-serif" font-size="12">
<rect x="0" y="0" width="700" height="450" fill="white"/>
<text x="60" y="22" font-size="16" font-weight="bold">NSGA-II on Schaffer N.1 — final Pareto front</text>
<rect x="60" y="40" width="620" height="360" fill="none" stroke="#888" />
<line x1="60" y1="400" x2="60" y2="405" stroke="#888" />
<text x="60" y="418" text-anchor="middle">0.000</text>
<line x1="266.66666666666663" y1="400" x2="266.66666666666663" y2="405" stroke="#888" />
<text x="266.66666666666663" y="418" text-anchor="middle">1.333</text>
<line x1="473.3333333333333" y1="400" x2="473.3333333333333" y2="405" stroke="#888" />
<text x="473.3333333333333" y="418" text-anchor="middle">2.667</text>
<line x1="680" y1="400" x2="680" y2="405" stroke="#888" />
<text x="680" y="418" text-anchor="middle">4.000</text>
<line x1="55" y1="400" x2="60" y2="400" stroke="#888" />
<text x="52" y="400" text-anchor="end" dominant-baseline="middle">0.000</text>
<line x1="55" y1="280" x2="60" y2="280" stroke="#888" />
<text x="52" y="280" text-anchor="end" dominant-baseline="middle">1.333</text>
<line x1="55" y1="160" x2="60" y2="160" stroke="#888" />
<text x="52" y="160" text-anchor="end" dominant-baseline="middle">2.666</text>
<line x1="55" y1="40" x2="60" y2="40" stroke="#888" />
<text x="52" y="40" text-anchor="end" dominant-baseline="middle">3.999</text>
<text x="370" y="438" text-anchor="middle">f1</text>
<text x="15" y="220" text-anchor="middle" transform="rotate(-90 15 220)">f2</text>
<circle cx="680.00" cy="400.00" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="60.00" cy="40.00" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="544.53" cy="395.16" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="596.60" cy="398.25" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="147.76" cy="259.91" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="77.82" cy="151.68" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="166.09" cy="276.21" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="512.44" cy="392.35" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="457.04" cy="385.63" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="122.47" cy="232.25" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="133.03" cy="244.68" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="92.63" cy="186.20" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="277.90" cy="340.31" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="329.99" cy="358.35" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="86.30" cy="173.00" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="83.00" cy="165.28" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="479.72" cy="388.69" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="71.88" cy="132.73" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="177.45" cy="285.16" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="60.12" cy="49.89" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="139.30" cy="251.43" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="228.69" cy="317.60" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="661.27" cy="399.92" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="247.24" cy="326.94" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="97.14" cy="194.62" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="291.01" cy="345.35" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="188.72" cy="293.31" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="237.44" cy="322.14" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="62.32" cy="82.64" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="400.75" cy="375.91" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="314.75" cy="353.60" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="206.51" cy="304.92" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="436.65" cy="382.48" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="263.18" cy="334.19" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="344.89" cy="362.64" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="67.81" cy="116.23" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="61.00" cy="68.34" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="60.73" cy="64.24" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="641.54" cy="399.64" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="115.80" cy="223.58" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="216.38" cy="310.79" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="169.95" cy="279.34" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="384.40" cy="372.44" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="65.45" cy="104.28" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="633.74" cy="399.48" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="64.21" cy="96.82" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="107.71" cy="212.00" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="414.47" cy="378.59" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="616.88" cy="399.02" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
<circle cx="419.20" cy="379.46" r="3" fill="#1f77b4" stroke="#0d4a8a" stroke-width="0.5" />
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB