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.
- quantum_study_utils-0.2.0.dist-info/METADATA +513 -0
- quantum_study_utils-0.2.0.dist-info/RECORD +14 -0
- quantum_study_utils-0.2.0.dist-info/WHEEL +5 -0
- quantum_study_utils-0.2.0.dist-info/licenses/LICENSE +21 -0
- quantum_study_utils-0.2.0.dist-info/top_level.txt +1 -0
- quantum_utils/__init__.py +42 -0
- quantum_utils/display/__init__.py +48 -0
- quantum_utils/display/_utils.py +137 -0
- quantum_utils/display/display.py +388 -0
- quantum_utils/display/formatters.py +197 -0
- quantum_utils/display/templates.py +225 -0
- quantum_utils/formatting.py +136 -0
- quantum_utils/measurements.py +268 -0
- quantum_utils/visualization.py +371 -0
|
@@ -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
|