Skip to main content

cqlib_core/visualization/state/
plot.rs

1// This code is part of Cqlib.
2//
3// (C) Copyright China Telecom Quantum Group 2026
4//
5// This code is licensed under the Apache License, Version 2.0. You may
6// obtain a copy of this license in the LICENSE.txt file in the root directory
7// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8//
9// Any modifications or derivative works of this code must retain this
10// copyright notice, and modified files need to carry a notice indicating
11// that they have been altered from the originals.
12
13//! Bloch-vector, state-city, and Pauli-vector rendering for quantum states.
14
15use 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
21/// Plot a single Bloch vector as SVG.
22///
23/// # Arguments
24///
25/// * `vector` - Bloch coordinates `(x, y, z)` with components typically in `[-1, 1]`.
26/// * `options` - Plot styling options such as title and figure size.
27///
28/// # Returns
29///
30/// SVG markup as a UTF-8 string.
31///
32/// # Examples
33///
34/// ```rust
35/// use cqlib_core::visualization::{StatePlotOptions, plot_bloch_vector};
36///
37/// let svg = plot_bloch_vector([0.0, 0.0, 1.0], &StatePlotOptions::default()).unwrap();
38/// assert!(svg.contains("<svg"));
39/// ```
40pub 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
51/// Plot one reduced Bloch vector per qubit as SVG.
52///
53/// # Arguments
54///
55/// * `state` - Input core state object accepted by state visualization routines.
56/// * `options` - Plot styling options such as title, colors, and bit order.
57///
58/// # Returns
59///
60/// SVG markup as a UTF-8 string.
61///
62/// # Errors
63///
64/// Returns [`VisualizationError::InvalidInput`] when the state payload is invalid.
65///
66/// # Examples
67///
68/// ```rust
69/// use cqlib_core::qis::Statevector;
70/// use cqlib_core::visualization::{StatePlotOptions, plot_bloch_multivector};
71/// use num_complex::Complex64;
72///
73/// let state = Statevector::from_state(
74///     1,
75///     vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)],
76/// ).unwrap();
77/// let svg = plot_bloch_multivector(&state, &StatePlotOptions::default()).unwrap();
78/// assert!(svg.contains("<svg"));
79/// ```
80pub 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
96/// Plot real and imaginary density-matrix components as an SVG state-city view.
97///
98/// # Arguments
99///
100/// * `state` - Input core state object accepted by state visualization routines.
101/// * `options` - Plot styling options such as title, opacity, and bit order.
102///
103/// # Returns
104///
105/// SVG markup as a UTF-8 string.
106///
107/// # Errors
108///
109/// Returns [`VisualizationError::InvalidInput`] when the state payload is invalid.
110///
111/// # Examples
112///
113/// ```rust
114/// use cqlib_core::qis::Statevector;
115/// use cqlib_core::visualization::{StatePlotOptions, plot_state_city};
116/// use num_complex::Complex64;
117///
118/// let state = Statevector::from_state(
119///     1,
120///     vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)],
121/// ).unwrap();
122/// let svg = plot_state_city(&state, &StatePlotOptions::default()).unwrap();
123/// assert!(svg.contains("Re[rho]"));
124/// ```
125pub 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    // Labels are display-order dependent; matrix lookup must map each displayed
133    // row and column back to the stored density-matrix index independently.
134    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
196/// Plot Pauli-basis expectation values as an SVG bar chart.
197///
198/// # Arguments
199///
200/// * `state` - Input core state object accepted by state visualization routines.
201/// * `options` - Plot styling options such as title, colors, and bit order.
202///
203/// # Returns
204///
205/// SVG markup as a UTF-8 string.
206///
207/// # Errors
208///
209/// Returns [`VisualizationError::InvalidInput`] when the state payload is invalid.
210///
211/// # Examples
212///
213/// ```rust
214/// use cqlib_core::qis::Statevector;
215/// use cqlib_core::visualization::{StatePlotOptions, plot_state_paulivec};
216/// use num_complex::Complex64;
217///
218/// let state = Statevector::from_state(
219///     1,
220///     vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)],
221/// ).unwrap();
222/// let svg = plot_state_paulivec(&state, &StatePlotOptions::default()).unwrap();
223/// assert!(svg.contains("<svg"));
224/// ```
225pub 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
309/// Render SVG markup to `.svg` or rasterized `.png`.
310///
311/// # Arguments
312///
313/// * `svg` - SVG markup produced by a state plotting routine.
314/// * `output_path` - Target file path. `.svg` writes vector output, `.png` writes raster output.
315///
316/// # Errors
317///
318/// Returns [`VisualizationError::Io`] or [`VisualizationError::SvgRenderFailed`] when file
319/// output or PNG rasterization fails.
320///
321/// # Examples
322///
323/// ```no_run
324/// use cqlib_core::qis::Statevector;
325/// use cqlib_core::visualization::{
326///     StatePlotOptions, plot_bloch_multivector, render_state_plot_to_file,
327/// };
328/// use num_complex::Complex64;
329///
330/// let state = Statevector::from_state(
331///     1,
332///     vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)],
333/// ).unwrap();
334/// let svg = plot_bloch_multivector(&state, &StatePlotOptions::default()).unwrap();
335/// render_state_plot_to_file(&svg, "bloch.svg").unwrap();
336/// ```
337pub 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
374/// Draw a pseudo-3D Bloch sphere and one vector arrow into the active SVG buffer.
375///
376/// The sphere is still pure SVG: grid curves are sampled in logical Bloch coordinates
377/// and projected into two dimensions by [`project_bloch_point`].
378fn 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
449/// Draw one labelled Bloch axis through the sphere center.
450fn 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
492/// Project a Bloch-sphere point into SVG coordinates.
493///
494/// The returned depth is reserved for callers that need front/back ordering; current
495/// grid rendering keeps a deterministic draw order instead.
496fn 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
504/// Reorder qubit-indexed vectors for display without changing their labels.
505fn 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/// Draw one real or imaginary density-matrix panel for the state-city plot.
516///
517/// Cell area scales with `sqrt(abs(value) / max_abs)` so small amplitudes remain visible
518/// while the sign is encoded by color.
519#[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
591/// Map a displayed basis row or column back to the density-matrix storage index.
592fn 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
600/// Reverse bit order within the fixed-width basis index.
601fn 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
611/// Start a white-background SVG document.
612fn 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
618/// SVG line element with escaped color attribute.
619fn 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
626/// SVG text element with escaped text and attributes.
627fn 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
636/// Convert projected points into an SVG path.
637fn 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}