1use super::data::{StateVisualizationSource, basis_labels, state_to_density_matrix};
16use super::math::{clamp_bloch, local_bloch_vectors, pauli_coefficients};
17use super::options::StatePlotOptions;
18use crate::visualization::VisualizationError;
19use crate::visualization::svg::{escape_attr, escape_text, render_svg_to_file};
20
21pub fn plot_bloch_vector(
41 vector: [f64; 3],
42 options: &StatePlotOptions,
43) -> Result<String, VisualizationError> {
44 Ok(render_bloch_grid(
45 &[("q0".to_string(), clamp_bloch(vector))],
46 options.title.as_deref(),
47 options,
48 ))
49}
50
51pub fn plot_bloch_multivector<S: StateVisualizationSource + ?Sized>(
81 state: &S,
82 options: &StatePlotOptions,
83) -> Result<String, VisualizationError> {
84 let vectors = local_bloch_vectors(state)?;
85 let ordered = order_by_display(vectors, options.reverse_bits)
86 .into_iter()
87 .map(|(qubit, vector)| (format!("q{qubit}"), vector))
88 .collect::<Vec<_>>();
89 Ok(render_bloch_grid(
90 &ordered,
91 options.title.as_deref(),
92 options,
93 ))
94}
95
96pub fn plot_state_city<S: StateVisualizationSource + ?Sized>(
126 state: &S,
127 options: &StatePlotOptions,
128) -> Result<String, VisualizationError> {
129 let (num_qubits, rho) = state_to_density_matrix(state)?;
130 let dim = 1usize << num_qubits;
131 let labels = basis_labels(num_qubits, options.reverse_bits);
132 let matrix_value = |row: usize, col: usize| {
135 let data_row = display_to_density_index(row, num_qubits, options.reverse_bits);
136 let data_col = display_to_density_index(col, num_qubits, options.reverse_bits);
137 rho[data_row * dim + data_col]
138 };
139 let max_abs = rho
140 .iter()
141 .map(|value| value.norm())
142 .fold(0.0_f64, f64::max)
143 .max(1e-12);
144 let (width, height) = options
145 .figsize
146 .map(|(w, h)| (w.max(4.0) * 100.0, h.max(3.0) * 100.0))
147 .unwrap_or((980.0, 520.0));
148 let mut out = svg_start(width, height);
149 if let Some(title) = &options.title {
150 out.push_str(&svg_text(width / 2.0, 28.0, title, 18, "middle", "#202124"));
151 }
152 let top = if options.title.is_some() { 52.0 } else { 28.0 };
153 let has_imaginary = rho.iter().any(|value| value.im.abs() > 1e-12);
154 let panel_gap = 32.0;
155 let side_margin = 36.0;
156 let panel_h = height - top - 34.0;
157 let panel_w = if has_imaginary {
158 (width - 2.0 * side_margin - panel_gap) / 2.0
159 } else {
160 width - 2.0 * side_margin
161 };
162 draw_matrix_panel(
163 &mut out,
164 "Re[rho]",
165 side_margin,
166 top,
167 panel_w,
168 panel_h,
169 dim,
170 &labels,
171 |row, col| matrix_value(row, col).re,
172 max_abs,
173 options.alpha,
174 );
175 if has_imaginary {
176 draw_matrix_panel(
177 &mut out,
178 "Im[rho]",
179 side_margin + panel_w + panel_gap,
180 top,
181 panel_w,
182 panel_h,
183 dim,
184 &labels,
185 |row, col| matrix_value(row, col).im,
186 max_abs,
187 options.alpha,
188 );
189 } else {
190 out.push_str("<!-- Im[rho] omitted: zero imaginary component -->");
191 }
192 out.push_str("</svg>");
193 Ok(out)
194}
195
196pub fn plot_state_paulivec<S: StateVisualizationSource + ?Sized>(
231 state: &S,
232 options: &StatePlotOptions,
233) -> Result<String, VisualizationError> {
234 let (num_qubits, rho) = state_to_density_matrix(state)?;
235 let coeffs = pauli_coefficients(num_qubits, &rho, options.reverse_bits)?;
236 let max_abs = coeffs
237 .iter()
238 .map(|(_, value)| value.abs())
239 .fold(0.0_f64, f64::max)
240 .max(1.0);
241 let (width, height) = options
242 .figsize
243 .map(|(w, h)| (w.max(4.0) * 100.0, h.max(3.0) * 100.0))
244 .unwrap_or((820.0, 480.0));
245 let left = 66.0;
246 let right = 28.0;
247 let top = if options.title.is_some() { 58.0 } else { 34.0 };
248 let bottom = 94.0;
249 let plot_w = width - left - right;
250 let plot_h = height - top - bottom;
251 let zero_y = top + plot_h / 2.0;
252 let bar_w = (plot_w / coeffs.len().max(1) as f64 * 0.72).clamp(2.0, 28.0);
253 let color_pos = options
254 .color
255 .first()
256 .map(String::as_str)
257 .unwrap_or("#4569d4");
258 let color_neg = options
259 .color
260 .get(1)
261 .map(String::as_str)
262 .unwrap_or("#d64b5f");
263
264 let mut out = svg_start(width, height);
265 if let Some(title) = &options.title {
266 out.push_str(&svg_text(width / 2.0, 28.0, title, 18, "middle", "#202124"));
267 }
268 for tick in -2..=2 {
269 let value = tick as f64 * max_abs / 2.0;
270 let y = zero_y - (value / max_abs) * (plot_h / 2.0);
271 out.push_str(&svg_line(left, y, left + plot_w, y, "#dfe3eb", 1.0));
272 out.push_str(&svg_text(
273 left - 8.0,
274 y + 4.0,
275 &format!("{value:.2}"),
276 11,
277 "end",
278 "#5f6673",
279 ));
280 }
281 out.push_str(&svg_line(left, top, left, top + plot_h, "#303642", 1.2));
282 out.push_str(&svg_line(
283 left,
284 zero_y,
285 left + plot_w,
286 zero_y,
287 "#303642",
288 1.2,
289 ));
290
291 for (idx, (label, value)) in coeffs.iter().enumerate() {
292 let cx = left + (idx as f64 + 0.5) * plot_w / coeffs.len() as f64;
293 let h = (value.abs() / max_abs) * (plot_h / 2.0);
294 let y = if *value >= 0.0 { zero_y - h } else { zero_y };
295 let color = if *value >= 0.0 { color_pos } else { color_neg };
296 out.push_str(&format!(
297 "<rect x=\"{:.3}\" y=\"{y:.3}\" width=\"{bar_w:.3}\" height=\"{:.3}\" rx=\"2\" fill=\"{}\"/>",
298 cx - bar_w / 2.0,
299 h.max(1.0),
300 escape_attr(color)
301 ));
302 if coeffs.len() <= 64 {
303 let ly = top + plot_h + 18.0;
304 out.push_str(&format!(
305 "<text x=\"{cx:.3}\" y=\"{ly:.3}\" font-family=\"Arial, sans-serif\" font-size=\"10\" fill=\"#303642\" text-anchor=\"end\" transform=\"rotate(-55 {cx:.3} {ly:.3})\">{}</text>",
306 escape_text(label)
307 ));
308 }
309 }
310 out.push_str("</svg>");
311 Ok(out)
312}
313
314pub fn render_state_plot_to_file(svg: &str, output_path: &str) -> Result<(), VisualizationError> {
343 render_svg_to_file(svg, output_path, 1.0)
344}
345
346fn render_bloch_grid(
347 items: &[(String, [f64; 3])],
348 title: Option<&str>,
349 options: &StatePlotOptions,
350) -> String {
351 let count = items.len().max(1);
352 let cols = count.min(4);
353 let rows = count.div_ceil(cols);
354 let title_h = if title.is_some() { 42.0 } else { 14.0 };
355 let (width, height) = options
356 .figsize
357 .map(|(w, h)| (w.max(3.0) * 100.0, h.max(3.0) * 100.0))
358 .unwrap_or((
359 (cols as f64 * 270.0).max(300.0),
360 rows as f64 * 270.0 + title_h,
361 ));
362 let mut out = svg_start(width, height);
363 if let Some(title) = title {
364 out.push_str(&svg_text(width / 2.0, 28.0, title, 18, "middle", "#202124"));
365 }
366 let cell_w = width / cols as f64;
367 let cell_h = (height - title_h) / rows as f64;
368 for (idx, (label, vector)) in items.iter().enumerate() {
369 let row = idx / cols;
370 let col = idx % cols;
371 let cx = col as f64 * cell_w + cell_w / 2.0;
372 let cy = title_h + row as f64 * cell_h + cell_h / 2.0 + 8.0;
373 draw_bloch(&mut out, cx, cy, cell_w.min(cell_h) * 0.39, label, *vector);
374 }
375 out.push_str("</svg>");
376 out
377}
378
379fn draw_bloch(out: &mut String, cx: f64, cy: f64, r: f64, label: &str, vector: [f64; 3]) {
384 let grad_id = format!("bloch-grad-{:.0}-{:.0}", cx, cy);
385 let arrow_id = format!("bloch-arrow-{:.0}-{:.0}", cx, cy);
386 let clip_id = format!("bloch-clip-{:.0}-{:.0}", cx, cy);
387 out.push_str(&format!(
388 "<defs><radialGradient id=\"{}\" cx=\"36%\" cy=\"24%\" r=\"78%\"><stop offset=\"0%\" stop-color=\"#ffffff\" stop-opacity=\"0.88\"/><stop offset=\"58%\" stop-color=\"#d7e7ee\" stop-opacity=\"0.50\"/><stop offset=\"100%\" stop-color=\"#b6ccd6\" stop-opacity=\"0.38\"/></radialGradient><marker id=\"{}\" markerWidth=\"7\" markerHeight=\"7\" refX=\"6.0\" refY=\"3.5\" orient=\"auto\" markerUnits=\"strokeWidth\"><path d=\"M0,0 L7,3.5 L0,7 z\" fill=\"#D62728\"/></marker><clipPath id=\"{}\"><circle cx=\"{cx:.3}\" cy=\"{cy:.3}\" r=\"{r:.3}\"/></clipPath></defs>",
389 escape_attr(&grad_id),
390 escape_attr(&arrow_id),
391 escape_attr(&clip_id)
392 ));
393 out.push_str("<g data-cqlib-bloch-3d=\"true\">");
394 out.push_str(&format!(
395 "<circle cx=\"{cx:.3}\" cy=\"{cy:.3}\" r=\"{r:.3}\" fill=\"url(#{})\" stroke=\"#c7c7c7\" stroke-width=\"1.0\"/>",
396 escape_attr(&grad_id)
397 ));
398 out.push_str(&format!(
399 "<g clip-path=\"url(#{})\">",
400 escape_attr(&clip_id)
401 ));
402
403 for z in [
404 -0.83, -0.67, -0.50, -0.33, -0.17, 0.17, 0.33, 0.50, 0.67, 0.83,
405 ] {
406 let radius = (1.0_f64 - z * z).sqrt();
407 let points = (0..=96)
408 .map(|idx| {
409 let theta = 2.0 * std::f64::consts::PI * idx as f64 / 96.0;
410 project_bloch_point(cx, cy, r, [radius * theta.cos(), radius * theta.sin(), z])
411 })
412 .collect::<Vec<_>>();
413 out.push_str(&svg_projected_path(&points, "#8c8c8c", 0.55, 0.36, None));
414 }
415
416 for phi in (0..180).step_by(15) {
417 let phi = (phi as f64).to_radians();
418 let points = (0..=96)
419 .map(|idx| {
420 let theta = std::f64::consts::PI * idx as f64 / 96.0;
421 project_bloch_point(
422 cx,
423 cy,
424 r,
425 [
426 theta.sin() * phi.cos(),
427 theta.sin() * phi.sin(),
428 theta.cos(),
429 ],
430 )
431 })
432 .collect::<Vec<_>>();
433 out.push_str(&svg_projected_path(&points, "#8c8c8c", 0.50, 0.30, None));
434 }
435
436 draw_reference_circle(out, cx, cy, r, ReferenceCircle::Equator);
437 draw_reference_circle(out, cx, cy, r, ReferenceCircle::PrimeMeridian);
438 out.push_str("</g>");
439
440 draw_bloch_axis(out, cx, cy, r, [1.0, 0.0, 0.0], "X");
441 draw_bloch_axis(out, cx, cy, r, [0.0, 1.0, 0.0], "Y");
442 draw_bloch_axis(out, cx, cy, r, [0.0, 0.0, 1.0], "Z");
443
444 let (ex, ey, _) = project_bloch_point(cx, cy, r * 0.92, vector);
445 let (sx, sy, _) = project_bloch_point(cx, cy, r, [0.0, 0.0, 0.0]);
446 out.push_str(&format!(
447 "<line x1=\"{sx:.3}\" y1=\"{sy:.3}\" x2=\"{ex:.3}\" y2=\"{ey:.3}\" stroke=\"#D62728\" stroke-width=\"2.3\" stroke-linecap=\"round\" marker-end=\"url(#{})\"/>",
448 escape_attr(&arrow_id)
449 ));
450 out.push_str("</g>");
451 out.push_str(&svg_text(cx, cy - r - 16.0, label, 18, "middle", "#000000"));
452}
453
454fn draw_bloch_axis(out: &mut String, cx: f64, cy: f64, r: f64, axis: [f64; 3], label: &str) {
456 let neg = [-axis[0], -axis[1], -axis[2]];
457 let (x1, y1, _) = project_bloch_point(cx, cy, r, neg);
458 let (x2, y2, _) = project_bloch_point(cx, cy, r, axis);
459 out.push_str(&svg_line(x1, y1, x2, y2, "#444444", 1.0));
460 let (tx, ty, _) = project_bloch_point(cx, cy, r * 1.08, axis);
461 out.push_str(&svg_text(tx, ty + 4.0, label, 15, "middle", "#000000"));
462}
463
464#[derive(Clone, Copy)]
465enum ReferenceCircle {
466 Equator,
467 PrimeMeridian,
468}
469
470fn draw_reference_circle(out: &mut String, cx: f64, cy: f64, r: f64, circle: ReferenceCircle) {
471 let points = (0..=160)
472 .map(|idx| {
473 let theta = 2.0 * std::f64::consts::PI * idx as f64 / 160.0;
474 let point = match circle {
475 ReferenceCircle::Equator => [theta.cos(), theta.sin(), 0.0],
476 ReferenceCircle::PrimeMeridian => [0.0, theta.sin(), theta.cos()],
477 };
478 project_bloch_point(cx, cy, r, point)
479 })
480 .collect::<Vec<_>>();
481 match circle {
482 ReferenceCircle::Equator => {
483 out.push_str(&svg_projected_path(&points, "#000000", 1.10, 0.92, None));
484 }
485 ReferenceCircle::PrimeMeridian => {
486 out.push_str(&svg_projected_path(
487 &points,
488 "#000000",
489 1.00,
490 0.88,
491 Some("5 4"),
492 ));
493 }
494 }
495}
496
497fn project_bloch_point(cx: f64, cy: f64, r: f64, point: [f64; 3]) -> (f64, f64, f64) {
502 let [x, y, z] = point;
503 let sx = 0.88 * x + 0.42 * y;
504 let sy = 0.08 * x - 0.40 * y - 0.90 * z;
505 let depth = 0.54 * x + 0.58 * y + 0.24 * z;
506 (cx + sx * r, cy + sy * r, depth)
507}
508
509fn order_by_display(
511 mut vectors: Vec<(usize, [f64; 3])>,
512 reverse_bits: bool,
513) -> Vec<(usize, [f64; 3])> {
514 if reverse_bits {
515 vectors.reverse();
516 }
517 vectors
518}
519
520#[allow(clippy::too_many_arguments)]
525fn draw_matrix_panel<F>(
526 out: &mut String,
527 title: &str,
528 x: f64,
529 y: f64,
530 w: f64,
531 h: f64,
532 dim: usize,
533 labels: &[String],
534 value_at: F,
535 max_abs: f64,
536 alpha: f64,
537) where
538 F: Fn(usize, usize) -> f64,
539{
540 out.push_str(&svg_text(
541 x + w / 2.0,
542 y + 18.0,
543 title,
544 14,
545 "middle",
546 "#202124",
547 ));
548 let title_h = 32.0;
549 let left_label_w = if dim <= 16 { 34.0 } else { 10.0 };
550 let bottom_label_h = if dim <= 16 { 46.0 } else { 12.0 };
551 let right_pad = 10.0;
552 let available_w = (w - left_label_w - right_pad).max(40.0);
553 let available_h = (h - title_h - bottom_label_h).max(40.0);
554 let grid = available_w.min(available_h).max(40.0);
555 let gx = x + left_label_w + ((available_w - grid) / 2.0).max(0.0);
556 let gy = y + title_h + ((available_h - grid) / 2.0).max(0.0);
557 let cell = grid / dim as f64;
558 out.push_str(&format!(
559 "<rect x=\"{gx:.3}\" y=\"{gy:.3}\" width=\"{grid:.3}\" height=\"{grid:.3}\" fill=\"#f7f8fb\" stroke=\"#c7ceda\"/>"
560 ));
561 for row in 0..dim {
562 for col in 0..dim {
563 let value = value_at(row, col);
564 let mag = (value.abs() / max_abs).min(1.0);
565 let fill = if value >= 0.0 { "#4569d4" } else { "#d64b5f" };
566 let inset = cell * (1.0 - mag.sqrt()) * 0.42;
567 let rx = gx + col as f64 * cell + inset;
568 let ry = gy + row as f64 * cell + inset;
569 let size = (cell - 2.0 * inset).max(1.0);
570 out.push_str(&format!(
571 "<rect x=\"{rx:.3}\" y=\"{ry:.3}\" width=\"{size:.3}\" height=\"{size:.3}\" fill=\"{fill}\" fill-opacity=\"{:.3}\"/>",
572 alpha.clamp(0.05, 1.0)
573 ));
574 }
575 }
576 if dim <= 16 {
577 for (idx, label) in labels.iter().enumerate() {
578 let tx = gx + (idx as f64 + 0.5) * cell;
579 let ty = gy + grid + 14.0;
580 out.push_str(&format!(
581 "<text x=\"{tx:.3}\" y=\"{ty:.3}\" font-family=\"Arial, sans-serif\" font-size=\"12\" fill=\"#303642\" text-anchor=\"end\" transform=\"rotate(-55 {tx:.3} {ty:.3})\">{}</text>",
582 escape_text(label)
583 ));
584 out.push_str(&svg_text(
585 gx - 6.0,
586 gy + (idx as f64 + 0.5) * cell + 3.0,
587 label,
588 12,
589 "end",
590 "#303642",
591 ));
592 }
593 }
594}
595
596fn display_to_density_index(index: usize, num_qubits: usize, reverse_bits: bool) -> usize {
598 if reverse_bits {
599 bit_reverse_index(index, num_qubits)
600 } else {
601 index
602 }
603}
604
605fn bit_reverse_index(index: usize, num_bits: usize) -> usize {
607 let mut out = 0usize;
608 for bit in 0..num_bits {
609 if ((index >> bit) & 1) == 1 {
610 out |= 1usize << (num_bits - 1 - bit);
611 }
612 }
613 out
614}
615
616fn svg_start(width: f64, height: f64) -> String {
618 format!(
619 "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{width:.0}\" height=\"{height:.0}\" viewBox=\"0 0 {width:.3} {height:.3}\"><rect width=\"100%\" height=\"100%\" fill=\"white\"/>"
620 )
621}
622
623fn svg_line(x1: f64, y1: f64, x2: f64, y2: f64, color: &str, width: f64) -> String {
625 format!(
626 "<line x1=\"{x1:.3}\" y1=\"{y1:.3}\" x2=\"{x2:.3}\" y2=\"{y2:.3}\" stroke=\"{}\" stroke-width=\"{width:.3}\"/>",
627 escape_attr(color)
628 )
629}
630
631fn svg_text(x: f64, y: f64, text: &str, size: u8, anchor: &str, color: &str) -> String {
633 format!(
634 "<text x=\"{x:.3}\" y=\"{y:.3}\" font-family=\"Arial, sans-serif\" font-size=\"{size}\" fill=\"{}\" text-anchor=\"{}\">{}</text>",
635 escape_attr(color),
636 escape_attr(anchor),
637 escape_text(text)
638 )
639}
640
641fn svg_projected_path(
643 points: &[(f64, f64, f64)],
644 color: &str,
645 width: f64,
646 opacity: f64,
647 dasharray: Option<&str>,
648) -> String {
649 if points.is_empty() {
650 return String::new();
651 }
652 let mut path = format!("M {:.3} {:.3}", points[0].0, points[0].1);
653 for (x, y, _) in points.iter().skip(1) {
654 path.push_str(&format!(" L {x:.3} {y:.3}"));
655 }
656 let dash = dasharray
657 .map(|value| format!(" stroke-dasharray=\"{}\"", escape_attr(value)))
658 .unwrap_or_default();
659 format!(
660 "<path d=\"{path}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{width:.3}\" stroke-opacity=\"{opacity:.3}\"{dash}/>",
661 escape_attr(color)
662 )
663}