Skip to main content

quantrs2_core/state_visualization_3d/
husimi.rs

1//! Husimi Q-distribution visualization for quantum states.
2//!
3//! Evaluates Q(θ, φ) = |⟨α(θ,φ)|^⊗n |ψ⟩|² on a 64×64 grid over
4//! the sphere (θ ∈ \[0,π\], φ ∈ \[0,2π\]), where |α(θ,φ)⟩ is the
5//! single-qubit SU(2) coherent state and the n-qubit coherent state
6//! is the tensor product |α⟩^⊗n.
7
8use scirs2_core::ndarray::Array1;
9use scirs2_core::Complex64;
10use serde_json::{json, Value};
11
12use crate::error::{QuantRS2Error, QuantRS2Result};
13
14/// Resolution of the Husimi grid (64 points per axis).
15const GRID_SIZE: usize = 64;
16
17/// Compute the n-qubit coherent state |α(θ,φ)⟩^⊗n.
18///
19/// The single-qubit coherent state is:
20///   |α⟩ = cos(θ/2)|0⟩ + e^{iφ} sin(θ/2)|1⟩
21///
22/// For n qubits the coherent state is the tensor product |α⟩⊗n.
23fn coherent_state_tensor(theta: f64, phi: f64, n_qubits: usize) -> Array1<Complex64> {
24    let dim = 1usize << n_qubits;
25    // Single-qubit coherent-state amplitudes
26    let c0 = Complex64::new((theta / 2.0).cos(), 0.0);
27    let c1 = Complex64::from_polar((theta / 2.0).sin(), phi);
28
29    let mut state = Array1::zeros(dim);
30    // Each basis index encodes n bits; the k-th qubit contributes c0 or c1
31    // according to whether bit k of the index is 0 or 1.
32    // Qubit 0 is the most significant bit (MSB convention).
33    for idx in 0..dim {
34        let mut amp = Complex64::new(1.0, 0.0);
35        for q in 0..n_qubits {
36            let bit = (idx >> (n_qubits - 1 - q)) & 1;
37            amp *= if bit == 0 { c0 } else { c1 };
38        }
39        state[idx] = amp;
40    }
41    state
42}
43
44/// Compute Q(θ, φ) = |⟨α(θ,φ)|ψ⟩|²  (no extra 1/π factor needed here
45/// since we only use Q for visualisation, not for phase-space integrals).
46fn husimi_q(state: &Array1<Complex64>, theta: f64, phi: f64, n_qubits: usize) -> f64 {
47    let coherent = coherent_state_tensor(theta, phi, n_qubits);
48    let inner: Complex64 = coherent
49        .iter()
50        .zip(state.iter())
51        .map(|(c, s)| c.conj() * s)
52        .sum();
53    inner.norm_sqr()
54}
55
56/// Returns a Plotly-JSON string for a Husimi Q-distribution surface plot.
57///
58/// The Q-function is evaluated on a 64×64 grid of (θ, φ) angles
59/// and rendered as a 3D surface.
60pub fn husimi_plotly_json(state: &Array1<Complex64>, n_qubits: usize) -> QuantRS2Result<String> {
61    let dim = 1usize << n_qubits;
62    if state.len() != dim {
63        return Err(QuantRS2Error::InvalidInput(format!(
64            "State length {} does not match 2^{} = {}",
65            state.len(),
66            n_qubits,
67            dim
68        )));
69    }
70    if n_qubits == 0 {
71        return Err(QuantRS2Error::InvalidInput(
72            "n_qubits must be > 0".to_string(),
73        ));
74    }
75
76    let pi = std::f64::consts::PI;
77    let n = GRID_SIZE;
78
79    let theta_vals: Vec<f64> = (0..n).map(|i| pi * (i as f64) / ((n - 1) as f64)).collect();
80    let phi_vals: Vec<f64> = (0..n)
81        .map(|j| 2.0 * pi * (j as f64) / ((n - 1) as f64))
82        .collect();
83
84    // Compute Q on the grid; store as (n_theta × n_phi) outer → inner
85    let mut z_matrix: Vec<Vec<f64>> = Vec::with_capacity(n);
86    let mut x_sph: Vec<Vec<f64>> = Vec::with_capacity(n);
87    let mut y_sph: Vec<Vec<f64>> = Vec::with_capacity(n);
88    let mut z_sph: Vec<Vec<f64>> = Vec::with_capacity(n);
89
90    for &theta in &theta_vals {
91        let mut row_z = Vec::with_capacity(n);
92        let mut row_x = Vec::with_capacity(n);
93        let mut row_y = Vec::with_capacity(n);
94        let mut row_zs = Vec::with_capacity(n);
95        for &phi in &phi_vals {
96            let q_val = husimi_q(state, theta, phi, n_qubits);
97            row_z.push(q_val);
98            row_x.push(theta.sin() * phi.cos());
99            row_y.push(theta.sin() * phi.sin());
100            row_zs.push(theta.cos());
101        }
102        z_matrix.push(row_z);
103        x_sph.push(row_x);
104        y_sph.push(row_y);
105        z_sph.push(row_zs);
106    }
107
108    // Sparse labels for heatmap axes (every 8th gridpoint)
109    let phi_axis_labels: Vec<String> = phi_vals
110        .iter()
111        .step_by(8)
112        .map(|&p| format!("{:.2}π", p / std::f64::consts::PI))
113        .collect();
114    let phi_axis_vals: Vec<usize> = (0..n).step_by(8).collect();
115    let theta_axis_labels: Vec<String> = theta_vals
116        .iter()
117        .step_by(8)
118        .map(|&t| format!("{:.2}π", t / std::f64::consts::PI))
119        .collect();
120    let theta_axis_vals: Vec<usize> = (0..n).step_by(8).collect();
121
122    // Two traces: heatmap (θ,φ) and 3D surface on sphere
123    let heatmap = json!({
124        "type": "heatmap",
125        "z": z_matrix,
126        "colorscale": "Viridis",
127        "colorbar": {"title": "Q(θ,φ)"},
128        "xaxis": "x",
129        "yaxis": "y"
130    });
131
132    let surface3d = json!({
133        "type": "surface",
134        "x": x_sph,
135        "y": y_sph,
136        "z": z_sph,
137        "surfacecolor": z_matrix,
138        "colorscale": "Viridis",
139        "showscale": true,
140        "colorbar": {"title": "Q(θ,φ)"},
141        "scene": "scene"
142    });
143
144    let figure = json!({
145        "data": [surface3d, heatmap],
146        "layout": {
147            "title": "Husimi Q-Distribution",
148            "scene": {
149                "xaxis": {"title": "x"},
150                "yaxis": {"title": "y"},
151                "zaxis": {"title": "z"},
152                "aspectmode": "cube",
153                "camera": {"eye": {"x": 1.5, "y": 1.5, "z": 1.2}},
154                "domain": {"x": [0.0, 0.6], "y": [0.0, 1.0]}
155            },
156            "xaxis": {
157                "title": "φ (radians/π)",
158                "domain": [0.65, 1.0],
159                "tickvals": phi_axis_vals,
160                "ticktext": phi_axis_labels
161            },
162            "yaxis": {
163                "title": "θ (radians/π)",
164                "tickvals": theta_axis_vals,
165                "ticktext": theta_axis_labels
166            },
167            "height": 600,
168            "showlegend": false
169        }
170    });
171
172    serde_json::to_string(&figure).map_err(QuantRS2Error::from)
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use scirs2_core::Complex64;
179
180    fn state_zero_1q() -> Array1<Complex64> {
181        Array1::from(vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)])
182    }
183
184    fn state_plus_1q() -> Array1<Complex64> {
185        let inv_sqrt2 = 1.0 / 2.0_f64.sqrt();
186        Array1::from(vec![
187            Complex64::new(inv_sqrt2, 0.0),
188            Complex64::new(inv_sqrt2, 0.0),
189        ])
190    }
191
192    #[test]
193    fn test_husimi_zero_state_peak() {
194        let state = state_zero_1q();
195        let pi = std::f64::consts::PI;
196        let n = GRID_SIZE;
197
198        // For |0⟩, Q(θ,φ) = cos²(θ/2), which is maximum at θ=0
199        let q_at_north = husimi_q(&state, 0.0, 0.0, 1);
200        let q_at_equator = husimi_q(&state, pi / 2.0, 0.0, 1);
201        let q_at_south = husimi_q(&state, pi, 0.0, 1);
202
203        assert!(
204            q_at_north > q_at_equator,
205            "Q peak at θ=0 should be > equator: {} vs {}",
206            q_at_north,
207            q_at_equator
208        );
209        assert!(
210            q_at_north > q_at_south,
211            "Q peak at θ=0 should be > south: {} vs {}",
212            q_at_north,
213            q_at_south
214        );
215        assert!(
216            (q_at_north - 1.0).abs() < 1e-10,
217            "Q(0,0) for |0⟩ should be 1.0, got {}",
218            q_at_north
219        );
220
221        // Check that the grid maximum is at the first theta point (closest to 0)
222        let theta_step = pi / ((n - 1) as f64);
223        let q_first_row_max = (0..GRID_SIZE)
224            .map(|j| {
225                let phi = 2.0 * pi * (j as f64) / ((n - 1) as f64);
226                husimi_q(&state, theta_step * 0.0, phi, 1)
227            })
228            .fold(f64::NEG_INFINITY, f64::max);
229        let q_last_row_max = (0..GRID_SIZE)
230            .map(|j| {
231                let phi = 2.0 * pi * (j as f64) / ((n - 1) as f64);
232                husimi_q(&state, pi, phi, 1)
233            })
234            .fold(f64::NEG_INFINITY, f64::max);
235        assert!(
236            q_first_row_max > q_last_row_max,
237            "Q peak should be at north (θ≈0)"
238        );
239    }
240
241    #[test]
242    fn test_husimi_plus_state_peak() {
243        // |+⟩ = (|0⟩+|1⟩)/√2 → coherent state at θ=π/2, φ=0
244        // Q(π/2, 0) should be 1.0 for |+⟩
245        let state = state_plus_1q();
246        let pi = std::f64::consts::PI;
247
248        let q_at_equator_0 = husimi_q(&state, pi / 2.0, 0.0, 1);
249        let q_at_equator_pi = husimi_q(&state, pi / 2.0, pi, 1);
250        let q_at_north = husimi_q(&state, 0.0, 0.0, 1);
251
252        assert!(
253            q_at_equator_0 > q_at_north,
254            "Q(π/2, 0) should exceed Q(0, 0) for |+⟩: {} vs {}",
255            q_at_equator_0,
256            q_at_north
257        );
258        assert!(
259            q_at_equator_0 > q_at_equator_pi,
260            "Q(π/2, 0) should exceed Q(π/2, π) for |+⟩: {} vs {}",
261            q_at_equator_0,
262            q_at_equator_pi
263        );
264        // Q(π/2, 0) = |cos(π/4)·1 + e^{0}·sin(π/4)·1/√2|² ... let's compute:
265        // |α(π/2, 0)⟩ = (|0⟩+|1⟩)/√2 = |+⟩, so Q(π/2, 0) = |⟨+|+⟩|² = 1
266        assert!(
267            (q_at_equator_0 - 1.0).abs() < 1e-10,
268            "Q(π/2, 0) for |+⟩ should be 1.0, got {}",
269            q_at_equator_0
270        );
271    }
272
273    #[test]
274    fn test_husimi_json_valid() {
275        let state = state_zero_1q();
276        let json_str = husimi_plotly_json(&state, 1).expect("Husimi JSON failed");
277        let _parsed: serde_json::Value =
278            serde_json::from_str(&json_str).expect("Output should be valid JSON");
279    }
280}