quantrs2_core/state_visualization_3d/
husimi.rs1use scirs2_core::ndarray::Array1;
9use scirs2_core::Complex64;
10use serde_json::{json, Value};
11
12use crate::error::{QuantRS2Error, QuantRS2Result};
13
14const GRID_SIZE: usize = 64;
16
17fn coherent_state_tensor(theta: f64, phi: f64, n_qubits: usize) -> Array1<Complex64> {
24 let dim = 1usize << n_qubits;
25 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 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
44fn 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
56pub 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 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 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 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 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 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 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 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}