quantum-study-utils 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,137 @@
1
+ """
2
+ Internal utilities for display module.
3
+
4
+ Provides helper functions for variable introspection, type conversions,
5
+ and statevector validation.
6
+ """
7
+
8
+ import inspect
9
+ from typing import Any, Tuple
10
+ import numpy as np
11
+
12
+
13
+ def get_variable_name(value: Any, fallback: str = "Result") -> str:
14
+ """
15
+ Extract the variable name of a value from the calling scope.
16
+
17
+ This function introspects the call stack to find the original variable name
18
+ that was passed to the function. Useful for display functions to auto-label output.
19
+
20
+ Args:
21
+ value: The value whose variable name should be found
22
+ fallback: Default name if variable name cannot be determined
23
+
24
+ Returns:
25
+ The variable name as a string, or fallback if not found
26
+
27
+ Example:
28
+ >>> my_variable = [1, 2, 3]
29
+ >>> name = get_variable_name(my_variable)
30
+ >>> name
31
+ 'my_variable'
32
+ """
33
+ frame = inspect.currentframe()
34
+ try:
35
+ # Go back 2 frames: get_variable_name -> caller -> original context
36
+ caller_frame = frame.f_back.f_back
37
+ if caller_frame is None:
38
+ return fallback
39
+
40
+ # Search for matching object in local scope
41
+ for name, candidate in caller_frame.f_locals.items():
42
+ if candidate is value and not name.startswith('_'):
43
+ return name
44
+ return fallback
45
+ finally:
46
+ # Prevent reference cycles
47
+ del frame
48
+
49
+
50
+ def as_complex_array(value: Any) -> np.ndarray:
51
+ """
52
+ Convert a value to a complex numpy array.
53
+
54
+ Handles Qiskit Statevector objects and numpy arrays transparently.
55
+
56
+ Args:
57
+ value: Value to convert (array-like or object with .data attribute)
58
+
59
+ Returns:
60
+ Complex numpy array
61
+
62
+ Raises:
63
+ TypeError: If value cannot be converted to array
64
+ """
65
+ # Handle Qiskit Statevector and similar objects with .data attribute
66
+ data = getattr(value, "data", value)
67
+ return np.asarray(data, dtype=complex)
68
+
69
+
70
+ def validate_qubit_count(size: int) -> int:
71
+ """
72
+ Validate statevector size and return qubit count.
73
+
74
+ A statevector for n qubits must have length 2^n. This function validates
75
+ this constraint and returns the number of qubits.
76
+
77
+ Args:
78
+ size: Length of statevector
79
+
80
+ Returns:
81
+ Number of qubits
82
+
83
+ Raises:
84
+ ValueError: If size is not a power of 2 or less than 2
85
+
86
+ Example:
87
+ >>> validate_qubit_count(4) # 2^2 = 4
88
+ 2
89
+ >>> validate_qubit_count(3)
90
+ Traceback (most recent call last):
91
+ ...
92
+ ValueError: Statevector length must be a power of two (at least 2).
93
+ """
94
+ if size < 2 or (size & (size - 1)): # Check if power of 2
95
+ raise ValueError(
96
+ f"Statevector length must be a power of two (at least 2). Got {size}."
97
+ )
98
+ # bit_length() - 1 gives log2(size)
99
+ return size.bit_length() - 1
100
+
101
+
102
+ def normalize_statevector(statevector: np.ndarray) -> Tuple[np.ndarray, float]:
103
+ """
104
+ Normalize a statevector and return norm squared.
105
+
106
+ Args:
107
+ statevector: Input statevector (1D array)
108
+
109
+ Returns:
110
+ Tuple of (normalized vector, norm_squared)
111
+
112
+ Raises:
113
+ ValueError: If statevector has norm close to zero
114
+ """
115
+ if statevector.ndim != 1:
116
+ raise ValueError(f"Expected 1D array, got {statevector.ndim}D")
117
+
118
+ norm_sq = float(np.vdot(statevector, statevector).real)
119
+
120
+ if np.isclose(norm_sq, 0):
121
+ raise ValueError(f"Statevector has zero norm: {norm_sq}")
122
+
123
+ return statevector / np.sqrt(norm_sq), norm_sq
124
+
125
+
126
+ def amplitude_is_near_zero(amplitude: complex, threshold: float = 1e-10) -> bool:
127
+ """
128
+ Check if an amplitude is effectively zero.
129
+
130
+ Args:
131
+ amplitude: Complex amplitude to check
132
+ threshold: Magnitude threshold below which amplitude is considered zero
133
+
134
+ Returns:
135
+ True if amplitude magnitude is below threshold
136
+ """
137
+ return np.abs(amplitude) < threshold
@@ -0,0 +1,388 @@
1
+ """
2
+ High-level display functions for quantum objects.
3
+
4
+ These are the main user-facing functions for displaying:
5
+ - Quantum states and amplitudes
6
+ - Probability distributions
7
+ - Measurement counts
8
+ - Quantum operators
9
+ """
10
+
11
+ import numpy as np
12
+ from typing import Dict, Optional, Union
13
+ from IPython.display import HTML, display, Math
14
+
15
+ from ._utils import (
16
+ get_variable_name,
17
+ as_complex_array,
18
+ validate_qubit_count,
19
+ normalize_statevector,
20
+ amplitude_is_near_zero,
21
+ )
22
+ from .formatters import (
23
+ AcademicNumberFormatter,
24
+ DiracExpressionGenerator,
25
+ LatexMatrixFormatter,
26
+ )
27
+ from .templates import (
28
+ HTMLTheme,
29
+ RawDataTemplate,
30
+ BarChartTemplate,
31
+ NormalizationNoticeTemplate,
32
+ )
33
+
34
+
35
+ def display_data(
36
+ value: Union[np.ndarray, object],
37
+ label: Optional[str] = None,
38
+ theme: Optional[HTMLTheme] = None
39
+ ) -> None:
40
+ """
41
+ Display raw quantum data with LaTeX mathematical representation.
42
+
43
+ Shows both:
44
+ 1. Raw numpy array (with HTML formatting)
45
+ 2. Mathematical notation:
46
+ - For vectors: Dirac notation + matrix form
47
+ - For matrices: Matrix form only
48
+
49
+ Automatically detects variable name if label not provided.
50
+
51
+ Args:
52
+ value: Statevector (1D) or operator (2D) array, or Qiskit object
53
+ label: Display label (auto-detected from variable name if None)
54
+ theme: HTMLTheme for styling (uses default if None)
55
+
56
+ Raises:
57
+ ValueError: If not a 1D vector or 2D square matrix
58
+
59
+ Example:
60
+ >>> psi = np.array([1/np.sqrt(2), 0, 0, 1/np.sqrt(2)])
61
+ >>> display_data(psi) # Shows as "psi"
62
+ >>> display_data(psi, label="Bell state")
63
+ """
64
+ if theme is None:
65
+ theme = HTMLTheme()
66
+
67
+ name = label or get_variable_name(value, fallback="Result")
68
+ array = as_complex_array(value)
69
+
70
+ # Escape and format raw array
71
+ raw_str = np.array2string(array).replace('\n', '<br>')
72
+ raw_html = RawDataTemplate.render(name, raw_str, theme)
73
+ display(HTML(raw_html))
74
+
75
+ # Display mathematical representation
76
+ if array.ndim == 1:
77
+ # Validate statevector
78
+ validate_qubit_count(len(array))
79
+
80
+ # Generate Dirac expression and matrix form
81
+ dirac, matrix = LatexMatrixFormatter.format_full_statevector(array)
82
+ latex_expr = rf"|\psi\rangle = {dirac} = {matrix}"
83
+
84
+ elif array.ndim == 2:
85
+ # Format matrix
86
+ matrix = LatexMatrixFormatter.format_matrix(array)
87
+ latex_expr = matrix
88
+
89
+ else:
90
+ raise ValueError(
91
+ f"Only 1D vectors or 2D matrices supported, got {array.ndim}D"
92
+ )
93
+
94
+ display(Math(latex_expr))
95
+
96
+
97
+ def display_probabilities(
98
+ statevector: Union[np.ndarray, object],
99
+ label: str = "Theoretical probabilities",
100
+ theme: Optional[HTMLTheme] = None
101
+ ) -> np.ndarray:
102
+ """
103
+ Display computational-basis probabilities of a normalized pure state.
104
+
105
+ Computes |amplitude|² for each basis state and displays as bar chart.
106
+ Validates normalization (⟨ψ|ψ⟩ = 1.0).
107
+
108
+ Args:
109
+ statevector: Input statevector (must be normalized)
110
+ label: Chart title
111
+ theme: HTMLTheme for styling
112
+
113
+ Returns:
114
+ Probability array (same as |amplitude|²)
115
+
116
+ Raises:
117
+ ValueError: If not a 1D vector or not normalized
118
+
119
+ Example:
120
+ >>> psi = np.array([0.6, 0.8])
121
+ >>> probs = display_probabilities(psi)
122
+ >>> print(probs) # [0.36, 0.64]
123
+ """
124
+ if theme is None:
125
+ theme = HTMLTheme()
126
+
127
+ vector = as_complex_array(statevector)
128
+
129
+ if vector.ndim != 1:
130
+ raise ValueError(
131
+ f"Probabilities require 1D statevector, got {vector.ndim}D"
132
+ )
133
+
134
+ n_qubits = validate_qubit_count(len(vector))
135
+
136
+ # Validate normalization
137
+ norm_sq = float(np.vdot(vector, vector).real)
138
+ if not np.isclose(norm_sq, 1.0):
139
+ raise ValueError(
140
+ f"Statevector is not normalized: ⟨ψ|ψ⟩ = {norm_sq:.6f}"
141
+ )
142
+
143
+ # Compute probabilities
144
+ probabilities = np.abs(vector) ** 2
145
+
146
+ # Prepare data for bar chart
147
+ data = {
148
+ f'{i:0{n_qubits}b}': float(prob * 100)
149
+ for i, prob in enumerate(probabilities)
150
+ }
151
+
152
+ # Render bar chart
153
+ html_chart = BarChartTemplate.render_chart(
154
+ data, label, suffix="%", unit="Probability", theme=theme
155
+ )
156
+ display(HTML(html_chart))
157
+
158
+ # Show normalization status
159
+ norm_notice = NormalizationNoticeTemplate.render(norm_sq)
160
+ display(HTML(norm_notice))
161
+
162
+ return probabilities
163
+
164
+
165
+ def display_counts(
166
+ counts: Dict[str, int],
167
+ label: str = "Quantum simulator",
168
+ theme: Optional[HTMLTheme] = None
169
+ ) -> Dict[str, float]:
170
+ """
171
+ Display shot counts from quantum simulator or real hardware.
172
+
173
+ Converts raw counts (number of measurement outcomes) into percentages
174
+ and displays as bar chart.
175
+
176
+ Args:
177
+ counts: Dictionary mapping bitstrings to shot counts
178
+ label: Chart title (e.g., "Aer simulator", "IBM Fez")
179
+ theme: HTMLTheme for styling
180
+
181
+ Returns:
182
+ Normalized probability distribution (counts / total_shots)
183
+
184
+ Raises:
185
+ ValueError: If counts dict is empty or has non-positive total
186
+
187
+ Example:
188
+ >>> counts = {'00': 512, '11': 512}
189
+ >>> probs = display_counts(counts, label="Aer simulator")
190
+ >>> print(probs) # {'00': 0.5, '11': 0.5}
191
+ """
192
+ if theme is None:
193
+ theme = HTMLTheme()
194
+
195
+ if not counts:
196
+ raise ValueError("Counts dictionary cannot be empty")
197
+
198
+ total_shots = sum(counts.values())
199
+ if total_shots <= 0:
200
+ raise ValueError(f"Total shots must be positive, got {total_shots}")
201
+
202
+ # Prepare data: convert counts to percentages
203
+ data = {
204
+ bitstring: (count / total_shots) * 100
205
+ for bitstring, count in counts.items()
206
+ }
207
+
208
+ # Build custom unit string with shot count
209
+ unit_str = f"{total_shots} shots"
210
+
211
+ # Render bar chart with custom formatting for shot counts
212
+ html_chart = BarChartTemplate.render_chart(
213
+ data, label, suffix="", unit=unit_str, theme=theme
214
+ )
215
+
216
+ # Post-process to add actual counts next to percentages
217
+ # This is a workaround to show both count and percentage
218
+ html_chart_lines = []
219
+ for bitstring in sorted(counts.keys()):
220
+ count = counts[bitstring]
221
+ percentage = (count / total_shots) * 100
222
+
223
+ bar_width = min(100.0, percentage)
224
+ bar_line = BarChartTemplate.render_bar(
225
+ bitstring, count, bar_width, theme
226
+ )
227
+
228
+ # Modify to show "count (percentage%)" format
229
+ bar_line = bar_line.replace(
230
+ f">{count:.2f}<",
231
+ f">{count} ({percentage:.1f}%)<"
232
+ )
233
+ html_chart_lines.append(bar_line)
234
+
235
+ # Reconstruct chart with modified bars
236
+ chart_html_base = BarChartTemplate.render_chart(
237
+ data, label, unit=unit_str, theme=theme
238
+ )
239
+
240
+ # Simpler approach: render one by one
241
+ from .templates import HTMLBuilder
242
+
243
+ container_style = (
244
+ f"max-width:550px;"
245
+ f"padding:{theme.container_padding};"
246
+ f"border:1px solid {theme.container_border_color};"
247
+ f"border-radius:{theme.container_border_radius};"
248
+ )
249
+
250
+ label_html = f"<b>{HTMLBuilder.safe_text(label)}</b>"
251
+ unit_html = (
252
+ f'<div style="font-size:{theme.unit_font_size};'
253
+ f'color:{theme.unit_color};">{unit_str}</div>'
254
+ )
255
+
256
+ bars = []
257
+ for bitstring in sorted(counts.keys()):
258
+ count = counts[bitstring]
259
+ percentage = (count / total_shots) * 100
260
+ bar_width = min(100.0, percentage)
261
+
262
+ bar_style = (
263
+ f"width:{bar_width:.4f}%;"
264
+ f"height:{theme.bar_height};"
265
+ f"background:{theme.bar_color};"
266
+ )
267
+
268
+ container_style_bar = (
269
+ f"flex:1;"
270
+ f"background:{theme.bar_bg_color};"
271
+ f"border-radius:{theme.bar_border_radius};"
272
+ f"overflow:hidden;"
273
+ )
274
+
275
+ row_style = (
276
+ "display:flex;"
277
+ "align-items:center;"
278
+ "gap:10px;"
279
+ "margin:7px 0;"
280
+ )
281
+
282
+ bar_html = f'<div style="{bar_style}"></div>'
283
+ container_bar_html = f'<div style="{container_style_bar}">{bar_html}</div>'
284
+ value_html = f'<div style="min-width:130px;text-align:right;">{count} ({percentage:.1f}%)</div>'
285
+ state_html = f'<div style="width:55px;"><code>|{HTMLBuilder.safe_text(bitstring)}⟩</code></div>'
286
+
287
+ row_html = f'<div style="{row_style}">{state_html}{container_bar_html}{value_html}</div>'
288
+ bars.append(row_html)
289
+
290
+ full_html = (
291
+ f'<div style="{container_style}">'
292
+ f'{label_html}{unit_html}{"".join(bars)}'
293
+ f'</div>'
294
+ )
295
+
296
+ display(HTML(full_html))
297
+
298
+ # Return normalized distribution
299
+ return {bitstring: count / total_shots for bitstring, count in counts.items()}
300
+
301
+
302
+ def display_operator_heatmap(
303
+ operator: Union[np.ndarray, object],
304
+ label: str = "Quantum operator",
305
+ theme: Optional[HTMLTheme] = None
306
+ ):
307
+ """
308
+ Display quantum operator as heatmap with annotated elements.
309
+
310
+ Creates a matplotlib heatmap showing:
311
+ - Color intensity proportional to element magnitude |⟨i|U|j⟩|
312
+ - Annotations showing decimal approximations
313
+ - Basis state labels on axes
314
+
315
+ Args:
316
+ operator: Square matrix representing quantum operator
317
+ label: Plot title
318
+ theme: HTMLTheme configuration (for sizing hints)
319
+
320
+ Returns:
321
+ matplotlib figure object
322
+
323
+ Raises:
324
+ ValueError: If not a square 2D matrix
325
+ ImportError: If matplotlib or seaborn not installed
326
+
327
+ Example:
328
+ >>> pauli_x = np.array([[0, 1], [1, 0]])
329
+ >>> fig = display_operator_heatmap(pauli_x, label="Pauli X")
330
+ """
331
+ try:
332
+ import matplotlib.pyplot as plt
333
+ import seaborn as sns
334
+ except ImportError as e:
335
+ raise ImportError(
336
+ "display_operator_heatmap requires matplotlib and seaborn"
337
+ ) from e
338
+
339
+ if theme is None:
340
+ theme = HTMLTheme()
341
+
342
+ matrix = as_complex_array(operator)
343
+
344
+ if matrix.ndim != 2 or matrix.shape[0] != matrix.shape[1]:
345
+ raise ValueError(
346
+ f"Operator must be square 2D matrix, "
347
+ f"got shape {matrix.shape}"
348
+ )
349
+
350
+ n_qubits = validate_qubit_count(matrix.shape[0])
351
+
352
+ # Create basis state labels
353
+ states = [f'|{i:0{n_qubits}b}⟩' for i in range(matrix.shape[0])]
354
+
355
+ # Prepare annotations: show magnitude in decimal form, omit near-zero elements
356
+ annotations = np.array([
357
+ ["" if np.isclose(x, 0) else f"{x:.2g}" for x in row]
358
+ for row in matrix
359
+ ])
360
+
361
+ # Compute figure size (at least theme minimum)
362
+ size = max(theme.heatmap_min_size, matrix.shape[0] * 1.1)
363
+
364
+ fig, ax = plt.subplots(figsize=(size, size))
365
+
366
+ # Plot heatmap
367
+ sns.heatmap(
368
+ np.abs(matrix),
369
+ annot=annotations,
370
+ fmt="",
371
+ cmap="Blues",
372
+ cbar=False,
373
+ square=True,
374
+ xticklabels=states,
375
+ yticklabels=states,
376
+ linewidths=1,
377
+ linecolor="white",
378
+ ax=ax,
379
+ )
380
+
381
+ # Move x-axis labels to top
382
+ ax.xaxis.tick_top()
383
+ ax.set_title(label, pad=22)
384
+
385
+ plt.tight_layout()
386
+ plt.show()
387
+
388
+ return fig