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>(
226 state: &S,
227 options: &StatePlotOptions,
228) -> Result<String, VisualizationError> {
229 let (num_qubits, rho) = state_to_density_matrix(state)?;
230 let coeffs = pauli_coefficients(num_qubits, &rho, options.reverse_bits)?;
231 let max_abs = coeffs
232 .iter()
233 .map(|(_, value)| value.abs())
234 .fold(0.0_f64, f64::max)
235 .max(1.0);
236 let (width, height) = options
237 .figsize
238 .map(|(w, h)| (w.max(4.0) * 100.0, h.max(3.0) * 100.0))
239 .unwrap_or((820.0, 480.0));
240 let left = 66.0;
241 let right = 28.0;
242 let top = if options.title.is_some() { 58.0 } else { 34.0 };
243 let bottom = 94.0;
244 let plot_w = width - left - right;
245 let plot_h = height - top - bottom;
246 let zero_y = top + plot_h / 2.0;
247 let bar_w = (plot_w / coeffs.len().max(1) as f64 * 0.72).clamp(2.0, 28.0);
248 let color_pos = options
249 .color
250 .first()
251 .map(String::as_str)
252 .unwrap_or("#4569d4");
253 let color_neg = options
254 .color
255 .get(1)
256 .map(String::as_str)
257 .unwrap_or("#d64b5f");
258
259 let mut out = svg_start(width, height);
260 if let Some(title) = &options.title {
261 out.push_str(&svg_text(width / 2.0, 28.0, title, 18, "middle", "#202124"));
262 }
263 for tick in -2..=2 {
264 let value = tick as f64 * max_abs / 2.0;
265 let y = zero_y - (value / max_abs) * (plot_h / 2.0);
266 out.push_str(&svg_line(left, y, left + plot_w, y, "#dfe3eb", 1.0));
267 out.push_str(&svg_text(
268 left - 8.0,
269 y + 4.0,
270 &format!("{value:.2}"),
271 11,
272 "end",
273 "#5f6673",
274 ));
275 }
276 out.push_str(&svg_line(left, top, left, top + plot_h, "#303642", 1.2));
277 out.push_str(&svg_line(
278 left,
279 zero_y,
280 left + plot_w,
281 zero_y,
282 "#303642",
283 1.2,
284 ));
285
286 for (idx, (label, value)) in coeffs.iter().enumerate() {
287 let cx = left + (idx as f64 + 0.5) * plot_w / coeffs.len() as f64;
288 let h = (value.abs() / max_abs) * (plot_h / 2.0);
289 let y = if *value >= 0.0 { zero_y - h } else { zero_y };
290 let color = if *value >= 0.0 { color_pos } else { color_neg };
291 out.push_str(&format!(
292 "<rect x=\"{:.3}\" y=\"{y:.3}\" width=\"{bar_w:.3}\" height=\"{:.3}\" rx=\"2\" fill=\"{}\"/>",
293 cx - bar_w / 2.0,
294 h.max(1.0),
295 escape_attr(color)
296 ));
297 if coeffs.len() <= 64 {
298 let ly = top + plot_h + 18.0;
299 out.push_str(&format!(
300 "<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>",
301 escape_text(label)
302 ));
303 }
304 }
305 out.push_str("</svg>");
306 Ok(out)
307}
308
309pub fn render_state_plot_to_file(svg: &str, output_path: &str) -> Result<(), VisualizationError> {
338 render_svg_to_file(svg, output_path, 1.0)
339}
340
341fn render_bloch_grid(
342 items: &[(String, [f64; 3])],
343 title: Option<&str>,
344 options: &StatePlotOptions,
345) -> String {
346 let count = items.len().max(1);
347 let cols = count.min(4);
348 let rows = count.div_ceil(cols);
349 let title_h = if title.is_some() { 42.0 } else { 14.0 };
350 let (width, height) = options
351 .figsize
352 .map(|(w, h)| (w.max(3.0) * 100.0, h.max(3.0) * 100.0))
353 .unwrap_or((
354 (cols as f64 * 270.0).max(300.0),
355 rows as f64 * 270.0 + title_h,
356 ));
357 let mut out = svg_start(width, height);
358 if let Some(title) = title {
359 out.push_str(&svg_text(width / 2.0, 28.0, title, 18, "middle", "#202124"));
360 }
361 let cell_w = width / cols as f64;
362 let cell_h = (height - title_h) / rows as f64;
363 for (idx, (label, vector)) in items.iter().enumerate() {
364 let row = idx / cols;
365 let col = idx % cols;
366 let cx = col as f64 * cell_w + cell_w / 2.0;
367 let cy = title_h + row as f64 * cell_h + cell_h / 2.0 + 8.0;
368 draw_bloch(&mut out, cx, cy, cell_w.min(cell_h) * 0.39, label, *vector);
369 }
370 out.push_str("</svg>");
371 out
372}
373
374fn draw_bloch(out: &mut String, cx: f64, cy: f64, r: f64, label: &str, vector: [f64; 3]) {
379 let grad_id = format!("bloch-grad-{:.0}-{:.0}", cx, cy);
380 let arrow_id = format!("bloch-arrow-{:.0}-{:.0}", cx, cy);
381 let clip_id = format!("bloch-clip-{:.0}-{:.0}", cx, cy);
382 out.push_str(&format!(
383 "<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>",
384 escape_attr(&grad_id),
385 escape_attr(&arrow_id),
386 escape_attr(&clip_id)
387 ));
388 out.push_str("<g data-cqlib-bloch-3d=\"true\">");
389 out.push_str(&format!(
390 "<circle cx=\"{cx:.3}\" cy=\"{cy:.3}\" r=\"{r:.3}\" fill=\"url(#{})\" stroke=\"#c7c7c7\" stroke-width=\"1.0\"/>",
391 escape_attr(&grad_id)
392 ));
393 out.push_str(&format!(
394 "<g clip-path=\"url(#{})\">",
395 escape_attr(&clip_id)
396 ));
397
398 for z in [
399 -0.83, -0.67, -0.50, -0.33, -0.17, 0.17, 0.33, 0.50, 0.67, 0.83,
400 ] {
401 let radius = (1.0_f64 - z * z).sqrt();
402 let points = (0..=96)
403 .map(|idx| {
404 let theta = 2.0 * std::f64::consts::PI * idx as f64 / 96.0;
405 project_bloch_point(cx, cy, r, [radius * theta.cos(), radius * theta.sin(), z])
406 })
407 .collect::<Vec<_>>();
408 out.push_str(&svg_projected_path(&points, "#8c8c8c", 0.55, 0.36, None));
409 }
410
411 for phi in (0..180).step_by(15) {
412 let phi = (phi as f64).to_radians();
413 let points = (0..=96)
414 .map(|idx| {
415 let theta = std::f64::consts::PI * idx as f64 / 96.0;
416 project_bloch_point(
417 cx,
418 cy,
419 r,
420 [
421 theta.sin() * phi.cos(),
422 theta.sin() * phi.sin(),
423 theta.cos(),
424 ],
425 )
426 })
427 .collect::<Vec<_>>();
428 out.push_str(&svg_projected_path(&points, "#8c8c8c", 0.50, 0.30, None));
429 }
430
431 draw_reference_circle(out, cx, cy, r, ReferenceCircle::Equator);
432 draw_reference_circle(out, cx, cy, r, ReferenceCircle::PrimeMeridian);
433 out.push_str("</g>");
434
435 draw_bloch_axis(out, cx, cy, r, [1.0, 0.0, 0.0], "X");
436 draw_bloch_axis(out, cx, cy, r, [0.0, 1.0, 0.0], "Y");
437 draw_bloch_axis(out, cx, cy, r, [0.0, 0.0, 1.0], "Z");
438
439 let (ex, ey, _) = project_bloch_point(cx, cy, r * 0.92, vector);
440 let (sx, sy, _) = project_bloch_point(cx, cy, r, [0.0, 0.0, 0.0]);
441 out.push_str(&format!(
442 "<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(#{})\"/>",
443 escape_attr(&arrow_id)
444 ));
445 out.push_str("</g>");
446 out.push_str(&svg_text(cx, cy - r - 16.0, label, 18, "middle", "#000000"));
447}
448
449fn draw_bloch_axis(out: &mut String, cx: f64, cy: f64, r: f64, axis: [f64; 3], label: &str) {
451 let neg = [-axis[0], -axis[1], -axis[2]];
452 let (x1, y1, _) = project_bloch_point(cx, cy, r, neg);
453 let (x2, y2, _) = project_bloch_point(cx, cy, r, axis);
454 out.push_str(&svg_line(x1, y1, x2, y2, "#444444", 1.0));
455 let (tx, ty, _) = project_bloch_point(cx, cy, r * 1.08, axis);
456 out.push_str(&svg_text(tx, ty + 4.0, label, 15, "middle", "#000000"));
457}
458
459#[derive(Clone, Copy)]
460enum ReferenceCircle {
461 Equator,
462 PrimeMeridian,
463}
464
465fn draw_reference_circle(out: &mut String, cx: f64, cy: f64, r: f64, circle: ReferenceCircle) {
466 let points = (0..=160)
467 .map(|idx| {
468 let theta = 2.0 * std::f64::consts::PI * idx as f64 / 160.0;
469 let point = match circle {
470 ReferenceCircle::Equator => [theta.cos(), theta.sin(), 0.0],
471 ReferenceCircle::PrimeMeridian => [0.0, theta.sin(), theta.cos()],
472 };
473 project_bloch_point(cx, cy, r, point)
474 })
475 .collect::<Vec<_>>();
476 match circle {
477 ReferenceCircle::Equator => {
478 out.push_str(&svg_projected_path(&points, "#000000", 1.10, 0.92, None));
479 }
480 ReferenceCircle::PrimeMeridian => {
481 out.push_str(&svg_projected_path(
482 &points,
483 "#000000",
484 1.00,
485 0.88,
486 Some("5 4"),
487 ));
488 }
489 }
490}
491
492fn project_bloch_point(cx: f64, cy: f64, r: f64, point: [f64; 3]) -> (f64, f64, f64) {
497 let [x, y, z] = point;
498 let sx = 0.88 * x + 0.42 * y;
499 let sy = 0.08 * x - 0.40 * y - 0.90 * z;
500 let depth = 0.54 * x + 0.58 * y + 0.24 * z;
501 (cx + sx * r, cy + sy * r, depth)
502}
503
504fn order_by_display(
506 mut vectors: Vec<(usize, [f64; 3])>,
507 reverse_bits: bool,
508) -> Vec<(usize, [f64; 3])> {
509 if reverse_bits {
510 vectors.reverse();
511 }
512 vectors
513}
514
515#[allow(clippy::too_many_arguments)]
520fn draw_matrix_panel<F>(
521 out: &mut String,
522 title: &str,
523 x: f64,
524 y: f64,
525 w: f64,
526 h: f64,
527 dim: usize,
528 labels: &[String],
529 value_at: F,
530 max_abs: f64,
531 alpha: f64,
532) where
533 F: Fn(usize, usize) -> f64,
534{
535 out.push_str(&svg_text(
536 x + w / 2.0,
537 y + 18.0,
538 title,
539 14,
540 "middle",
541 "#202124",
542 ));
543 let title_h = 32.0;
544 let left_label_w = if dim <= 16 { 34.0 } else { 10.0 };
545 let bottom_label_h = if dim <= 16 { 46.0 } else { 12.0 };
546 let right_pad = 10.0;
547 let available_w = (w - left_label_w - right_pad).max(40.0);
548 let available_h = (h - title_h - bottom_label_h).max(40.0);
549 let grid = available_w.min(available_h).max(40.0);
550 let gx = x + left_label_w + ((available_w - grid) / 2.0).max(0.0);
551 let gy = y + title_h + ((available_h - grid) / 2.0).max(0.0);
552 let cell = grid / dim as f64;
553 out.push_str(&format!(
554 "<rect x=\"{gx:.3}\" y=\"{gy:.3}\" width=\"{grid:.3}\" height=\"{grid:.3}\" fill=\"#f7f8fb\" stroke=\"#c7ceda\"/>"
555 ));
556 for row in 0..dim {
557 for col in 0..dim {
558 let value = value_at(row, col);
559 let mag = (value.abs() / max_abs).min(1.0);
560 let fill = if value >= 0.0 { "#4569d4" } else { "#d64b5f" };
561 let inset = cell * (1.0 - mag.sqrt()) * 0.42;
562 let rx = gx + col as f64 * cell + inset;
563 let ry = gy + row as f64 * cell + inset;
564 let size = (cell - 2.0 * inset).max(1.0);
565 out.push_str(&format!(
566 "<rect x=\"{rx:.3}\" y=\"{ry:.3}\" width=\"{size:.3}\" height=\"{size:.3}\" fill=\"{fill}\" fill-opacity=\"{:.3}\"/>",
567 alpha.clamp(0.05, 1.0)
568 ));
569 }
570 }
571 if dim <= 16 {
572 for (idx, label) in labels.iter().enumerate() {
573 let tx = gx + (idx as f64 + 0.5) * cell;
574 let ty = gy + grid + 14.0;
575 out.push_str(&format!(
576 "<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>",
577 escape_text(label)
578 ));
579 out.push_str(&svg_text(
580 gx - 6.0,
581 gy + (idx as f64 + 0.5) * cell + 3.0,
582 label,
583 12,
584 "end",
585 "#303642",
586 ));
587 }
588 }
589}
590
591fn display_to_density_index(index: usize, num_qubits: usize, reverse_bits: bool) -> usize {
593 if reverse_bits {
594 bit_reverse_index(index, num_qubits)
595 } else {
596 index
597 }
598}
599
600fn bit_reverse_index(index: usize, num_bits: usize) -> usize {
602 let mut out = 0usize;
603 for bit in 0..num_bits {
604 if ((index >> bit) & 1) == 1 {
605 out |= 1usize << (num_bits - 1 - bit);
606 }
607 }
608 out
609}
610
611fn svg_start(width: f64, height: f64) -> String {
613 format!(
614 "<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\"/>"
615 )
616}
617
618fn svg_line(x1: f64, y1: f64, x2: f64, y2: f64, color: &str, width: f64) -> String {
620 format!(
621 "<line x1=\"{x1:.3}\" y1=\"{y1:.3}\" x2=\"{x2:.3}\" y2=\"{y2:.3}\" stroke=\"{}\" stroke-width=\"{width:.3}\"/>",
622 escape_attr(color)
623 )
624}
625
626fn svg_text(x: f64, y: f64, text: &str, size: u8, anchor: &str, color: &str) -> String {
628 format!(
629 "<text x=\"{x:.3}\" y=\"{y:.3}\" font-family=\"Arial, sans-serif\" font-size=\"{size}\" fill=\"{}\" text-anchor=\"{}\">{}</text>",
630 escape_attr(color),
631 escape_attr(anchor),
632 escape_text(text)
633 )
634}
635
636fn svg_projected_path(
638 points: &[(f64, f64, f64)],
639 color: &str,
640 width: f64,
641 opacity: f64,
642 dasharray: Option<&str>,
643) -> String {
644 if points.is_empty() {
645 return String::new();
646 }
647 let mut path = format!("M {:.3} {:.3}", points[0].0, points[0].1);
648 for (x, y, _) in points.iter().skip(1) {
649 path.push_str(&format!(" L {x:.3} {y:.3}"));
650 }
651 let dash = dasharray
652 .map(|value| format!(" stroke-dasharray=\"{}\"", escape_attr(value)))
653 .unwrap_or_default();
654 format!(
655 "<path d=\"{path}\" fill=\"none\" stroke=\"{}\" stroke-width=\"{width:.3}\" stroke-opacity=\"{opacity:.3}\"{dash}/>",
656 escape_attr(color)
657 )
658}