1use std::fmt::Write as _;
36
37use heuropt::core::candidate::Candidate;
38use heuropt::core::objective::ObjectiveSpace;
39
40pub fn pareto_front_svg<D>(
51 front: &[Candidate<D>],
52 objectives: &ObjectiveSpace,
53 width: u32,
54 height: u32,
55 title: &str,
56) -> String {
57 assert_eq!(
58 objectives.len(),
59 2,
60 "pareto_front_svg requires exactly 2 objectives",
61 );
62 let oriented: Vec<[f64; 2]> = front
63 .iter()
64 .map(|c| {
65 let m = objectives.as_minimization(&c.evaluation.objectives);
66 [m[0], m[1]]
67 })
68 .collect();
69 let (xs_label, ys_label) = (
70 objectives.objectives[0].name.as_str(),
71 objectives.objectives[1].name.as_str(),
72 );
73
74 let (xmin, xmax) = bounds(oriented.iter().map(|p| p[0]));
75 let (ymin, ymax) = bounds(oriented.iter().map(|p| p[1]));
76 let xspan = (xmax - xmin).max(1e-12);
77 let yspan = (ymax - ymin).max(1e-12);
78
79 let m_left = 60.0_f64;
81 let m_right = 20.0_f64;
82 let m_top = 40.0_f64;
83 let m_bot = 50.0_f64;
84 let plot_w = width as f64 - m_left - m_right;
85 let plot_h = height as f64 - m_top - m_bot;
86
87 let to_x = |v: f64| m_left + (v - xmin) / xspan * plot_w;
88 let to_y = |v: f64| m_top + plot_h - (v - ymin) / yspan * plot_h;
90
91 let mut out = String::new();
92 let _ = writeln!(
93 out,
94 "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {width} {height}\" \
95 font-family=\"system-ui, sans-serif\" font-size=\"12\">",
96 );
97 let _ = writeln!(
98 out,
99 " <rect x=\"0\" y=\"0\" width=\"{width}\" height=\"{height}\" fill=\"white\"/>",
100 );
101 let _ = writeln!(
102 out,
103 " <text x=\"{x}\" y=\"22\" font-size=\"16\" font-weight=\"bold\">{title}</text>",
104 x = m_left,
105 title = escape_xml(title),
106 );
107
108 let _ = writeln!(
110 out,
111 " <rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" fill=\"none\" stroke=\"#888\" />",
112 m_left, m_top, plot_w, plot_h,
113 );
114
115 for i in 0..=3 {
117 let t = i as f64 / 3.0;
118 let v = xmin + t * xspan;
119 let x = to_x(v);
120 let _ = writeln!(
121 out,
122 " <line x1=\"{x}\" y1=\"{y0}\" x2=\"{x}\" y2=\"{y1}\" stroke=\"#888\" />",
123 y0 = m_top + plot_h,
124 y1 = m_top + plot_h + 5.0,
125 );
126 let _ = writeln!(
127 out,
128 " <text x=\"{x}\" y=\"{y}\" text-anchor=\"middle\">{v:.3}</text>",
129 y = m_top + plot_h + 18.0,
130 );
131 }
132 for i in 0..=3 {
134 let t = i as f64 / 3.0;
135 let v = ymin + t * yspan;
136 let y = to_y(v);
137 let _ = writeln!(
138 out,
139 " <line x1=\"{x0}\" y1=\"{y}\" x2=\"{x1}\" y2=\"{y}\" stroke=\"#888\" />",
140 x0 = m_left - 5.0,
141 x1 = m_left,
142 );
143 let _ = writeln!(
144 out,
145 " <text x=\"{x}\" y=\"{y}\" text-anchor=\"end\" dominant-baseline=\"middle\">{v:.3}</text>",
146 x = m_left - 8.0,
147 );
148 }
149
150 let _ = writeln!(
152 out,
153 " <text x=\"{x}\" y=\"{y}\" text-anchor=\"middle\">{xs_label}</text>",
154 x = m_left + plot_w / 2.0,
155 y = height as f64 - 12.0,
156 xs_label = escape_xml(xs_label),
157 );
158 let _ = writeln!(
159 out,
160 " <text x=\"15\" y=\"{y}\" text-anchor=\"middle\" \
161 transform=\"rotate(-90 15 {y})\">{ys_label}</text>",
162 y = m_top + plot_h / 2.0,
163 ys_label = escape_xml(ys_label),
164 );
165
166 for p in &oriented {
168 let cx = to_x(p[0]);
169 let cy = to_y(p[1]);
170 let _ = writeln!(
171 out,
172 " <circle cx=\"{cx:.2}\" cy=\"{cy:.2}\" r=\"3\" fill=\"#1f77b4\" \
173 stroke=\"#0d4a8a\" stroke-width=\"0.5\" />",
174 );
175 }
176
177 out.push_str("</svg>");
178 out
179}
180
181pub fn convergence_svg(
187 bests: &[f64],
188 width: u32,
189 height: u32,
190 title: &str,
191 y_axis_label: &str,
192 _direction_minimize: bool,
193) -> String {
194 let n = bests.len();
195 if n == 0 {
196 return format!(
197 "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {width} {height}\">\
198 <text x=\"10\" y=\"20\">{}</text></svg>",
199 escape_xml(title)
200 );
201 }
202
203 let (ymin, ymax) = bounds(bests.iter().copied());
204 let yspan = (ymax - ymin).max(1e-12);
205 let xspan = (n - 1).max(1) as f64;
206
207 let m_left = 70.0_f64;
208 let m_right = 20.0_f64;
209 let m_top = 40.0_f64;
210 let m_bot = 50.0_f64;
211 let plot_w = width as f64 - m_left - m_right;
212 let plot_h = height as f64 - m_top - m_bot;
213
214 let to_x = |i: usize| m_left + (i as f64) / xspan * plot_w;
215 let to_y = |v: f64| m_top + plot_h - (v - ymin) / yspan * plot_h;
216
217 let mut out = String::new();
218 let _ = writeln!(
219 out,
220 "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {width} {height}\" \
221 font-family=\"system-ui, sans-serif\" font-size=\"12\">",
222 );
223 let _ = writeln!(
224 out,
225 " <rect x=\"0\" y=\"0\" width=\"{width}\" height=\"{height}\" fill=\"white\"/>",
226 );
227 let _ = writeln!(
228 out,
229 " <text x=\"{x}\" y=\"22\" font-size=\"16\" font-weight=\"bold\">{title}</text>",
230 x = m_left,
231 title = escape_xml(title),
232 );
233 let _ = writeln!(
234 out,
235 " <rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" fill=\"none\" stroke=\"#888\" />",
236 m_left, m_top, plot_w, plot_h,
237 );
238
239 for i in 0..=4 {
241 let t = i as f64 / 4.0;
242 let g = (t * (n - 1) as f64).round() as usize;
243 let x = to_x(g);
244 let _ = writeln!(
245 out,
246 " <line x1=\"{x}\" y1=\"{y0}\" x2=\"{x}\" y2=\"{y1}\" stroke=\"#888\" />",
247 y0 = m_top + plot_h,
248 y1 = m_top + plot_h + 5.0,
249 );
250 let _ = writeln!(
251 out,
252 " <text x=\"{x}\" y=\"{y}\" text-anchor=\"middle\">{g}</text>",
253 y = m_top + plot_h + 18.0,
254 );
255 }
256 for i in 0..=3 {
258 let t = i as f64 / 3.0;
259 let v = ymin + t * yspan;
260 let y = to_y(v);
261 let _ = writeln!(
262 out,
263 " <line x1=\"{x0}\" y1=\"{y}\" x2=\"{x1}\" y2=\"{y}\" stroke=\"#888\" />",
264 x0 = m_left - 5.0,
265 x1 = m_left,
266 );
267 let _ = writeln!(
268 out,
269 " <text x=\"{x}\" y=\"{y}\" text-anchor=\"end\" dominant-baseline=\"middle\">{v:.3e}</text>",
270 x = m_left - 8.0,
271 );
272 }
273
274 let _ = writeln!(
276 out,
277 " <text x=\"{x}\" y=\"{y}\" text-anchor=\"middle\">generation</text>",
278 x = m_left + plot_w / 2.0,
279 y = height as f64 - 12.0,
280 );
281 let _ = writeln!(
282 out,
283 " <text x=\"15\" y=\"{y}\" text-anchor=\"middle\" \
284 transform=\"rotate(-90 15 {y})\">{label}</text>",
285 y = m_top + plot_h / 2.0,
286 label = escape_xml(y_axis_label),
287 );
288
289 let mut points = String::new();
291 for (i, &v) in bests.iter().enumerate() {
292 if i > 0 {
293 points.push(' ');
294 }
295 let _ = write!(points, "{:.2},{:.2}", to_x(i), to_y(v));
296 }
297 let _ = writeln!(
298 out,
299 " <polyline points=\"{points}\" fill=\"none\" stroke=\"#1f77b4\" stroke-width=\"1.5\" />",
300 );
301
302 out.push_str("</svg>");
303 out
304}
305
306fn bounds<I: IntoIterator<Item = f64>>(it: I) -> (f64, f64) {
307 let mut lo = f64::INFINITY;
308 let mut hi = f64::NEG_INFINITY;
309 for v in it {
310 if v.is_finite() {
311 if v < lo {
312 lo = v;
313 }
314 if v > hi {
315 hi = v;
316 }
317 }
318 }
319 if lo.is_infinite() {
320 (0.0, 1.0)
321 } else if (hi - lo).abs() < f64::EPSILON {
322 (lo - 0.5, hi + 0.5)
324 } else {
325 (lo, hi)
326 }
327}
328
329fn escape_xml(s: &str) -> String {
330 s.replace('&', "&")
331 .replace('<', "<")
332 .replace('>', ">")
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use heuropt::core::evaluation::Evaluation;
339 use heuropt::core::objective::Objective;
340
341 #[test]
342 fn pareto_svg_well_formed() {
343 let space = ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")]);
344 let front = vec![
345 Candidate::new((), Evaluation::new(vec![0.0, 1.0])),
346 Candidate::new((), Evaluation::new(vec![1.0, 0.0])),
347 ];
348 let svg = pareto_front_svg(&front, &space, 400, 300, "test");
349 assert!(svg.starts_with("<svg"));
350 assert!(svg.contains("</svg>"));
351 assert!(svg.contains("<circle"));
352 }
353
354 #[test]
355 fn convergence_svg_well_formed() {
356 let bests = vec![10.0, 5.0, 2.0, 1.0, 0.5];
357 let svg = convergence_svg(&bests, 400, 300, "convergence", "best", true);
358 assert!(svg.starts_with("<svg"));
359 assert!(svg.contains("polyline"));
360 }
361
362 #[test]
363 fn convergence_empty_returns_valid_svg() {
364 let svg = convergence_svg(&[], 200, 100, "empty", "y", true);
365 assert!(svg.contains("<svg"));
366 assert!(svg.contains("</svg>"));
367 }
368}