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,268 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
Quantum measurement utilities.
|
|
4
|
+
|
|
5
|
+
Provides functions for calculating probabilities,
|
|
6
|
+
analyzing measurement counts, and comparing results
|
|
7
|
+
from quantum simulators and real quantum hardware.
|
|
8
|
+
|
|
9
|
+
Compatible with standard Python, NumPy and Qiskit.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from collections.abc import Mapping, Sequence
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def calculate_probabilities(
|
|
18
|
+
counts: Mapping[str, int],
|
|
19
|
+
) -> dict[str, float]:
|
|
20
|
+
"""
|
|
21
|
+
Calculate measurement probabilities from shot counts.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
counts: Dictionary mapping bitstrings to shot counts.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Dictionary mapping bitstrings to probabilities.
|
|
28
|
+
"""
|
|
29
|
+
_validate_counts(counts)
|
|
30
|
+
|
|
31
|
+
total = sum(counts.values())
|
|
32
|
+
|
|
33
|
+
if total == 0:
|
|
34
|
+
return {}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
state: count / total
|
|
38
|
+
for state, count in sorted(counts.items())
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def calculate_state_probabilities(
|
|
43
|
+
statevector: Sequence[complex],
|
|
44
|
+
) -> dict[str, float]:
|
|
45
|
+
"""
|
|
46
|
+
Calculate theoretical probabilities from a statevector.
|
|
47
|
+
|
|
48
|
+
Uses the Born rule: P(i) = |amplitude_i|^2.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
statevector: Quantum statevector amplitudes.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Dictionary mapping computational basis states
|
|
55
|
+
to their theoretical probabilities.
|
|
56
|
+
"""
|
|
57
|
+
if hasattr(statevector, "data"):
|
|
58
|
+
statevector = statevector.data
|
|
59
|
+
|
|
60
|
+
amplitudes = np.asarray(statevector, dtype=complex)
|
|
61
|
+
|
|
62
|
+
if amplitudes.ndim != 1:
|
|
63
|
+
raise ValueError("Statevector must be one-dimensional.")
|
|
64
|
+
|
|
65
|
+
dimension = len(amplitudes)
|
|
66
|
+
|
|
67
|
+
if dimension == 0 or dimension & (dimension - 1):
|
|
68
|
+
raise ValueError(
|
|
69
|
+
"Statevector length must be a power of two."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
norm = np.sum(np.abs(amplitudes) ** 2)
|
|
73
|
+
|
|
74
|
+
if not np.isclose(norm, 1.0):
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Statevector is not normalized: {norm:.6f}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
num_qubits = dimension.bit_length() - 1
|
|
80
|
+
|
|
81
|
+
probabilities = np.abs(amplitudes) ** 2
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
format(i, f"0{max(1, num_qubits)}b"): float(p)
|
|
85
|
+
for i, p in enumerate(probabilities)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def calculate_percentages(
|
|
90
|
+
counts: Mapping[str, int],
|
|
91
|
+
) -> dict[str, float]:
|
|
92
|
+
"""
|
|
93
|
+
Convert measurement counts into percentages.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Dictionary mapping bitstrings to percentages.
|
|
97
|
+
"""
|
|
98
|
+
probabilities = calculate_probabilities(counts)
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
state: probability * 100
|
|
102
|
+
for state, probability in probabilities.items()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def calculate_expectation_z(
|
|
107
|
+
counts: Mapping[str, int],
|
|
108
|
+
qubit: int = 0,
|
|
109
|
+
) -> float:
|
|
110
|
+
"""
|
|
111
|
+
Calculate the expectation value of Pauli-Z
|
|
112
|
+
for a measured qubit.
|
|
113
|
+
|
|
114
|
+
For Qiskit bitstrings, qubit 0 is the rightmost bit.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
counts: Measurement counts.
|
|
118
|
+
qubit: Qubit index.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Expectation value between -1 and +1.
|
|
122
|
+
"""
|
|
123
|
+
probabilities = calculate_probabilities(counts)
|
|
124
|
+
|
|
125
|
+
if not probabilities:
|
|
126
|
+
raise ValueError("No measurement shots available.")
|
|
127
|
+
|
|
128
|
+
if qubit < 0:
|
|
129
|
+
raise ValueError("Qubit index must be non-negative.")
|
|
130
|
+
|
|
131
|
+
expectation = 0.0
|
|
132
|
+
|
|
133
|
+
for bitstring, probability in probabilities.items():
|
|
134
|
+
|
|
135
|
+
bits = bitstring.replace(" ", "")
|
|
136
|
+
|
|
137
|
+
if qubit >= len(bits):
|
|
138
|
+
raise ValueError("Qubit index out of range.")
|
|
139
|
+
|
|
140
|
+
bit = bits[-(qubit + 1)]
|
|
141
|
+
|
|
142
|
+
if bit not in ("0", "1"):
|
|
143
|
+
raise ValueError("Invalid measurement bitstring.")
|
|
144
|
+
|
|
145
|
+
eigenvalue = 1 if bit == "0" else -1
|
|
146
|
+
|
|
147
|
+
expectation += eigenvalue * probability
|
|
148
|
+
|
|
149
|
+
return expectation
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def compare_measurements(
|
|
153
|
+
simulator_counts: Mapping[str, int],
|
|
154
|
+
hardware_counts: Mapping[str, int],
|
|
155
|
+
) -> dict[str, dict[str, float]]:
|
|
156
|
+
"""
|
|
157
|
+
Compare simulator and real hardware measurements.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
simulator_counts: Counts obtained from simulation.
|
|
161
|
+
hardware_counts: Counts obtained from hardware.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Dictionary containing simulator probabilities,
|
|
165
|
+
hardware probabilities and absolute differences.
|
|
166
|
+
"""
|
|
167
|
+
simulator = calculate_probabilities(simulator_counts)
|
|
168
|
+
hardware = calculate_probabilities(hardware_counts)
|
|
169
|
+
|
|
170
|
+
if not simulator or not hardware:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
"Both datasets must contain measurement shots."
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
states = sorted(set(simulator) | set(hardware))
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
state: {
|
|
179
|
+
"simulator": simulator.get(state, 0.0),
|
|
180
|
+
"hardware": hardware.get(state, 0.0),
|
|
181
|
+
"difference": abs(
|
|
182
|
+
simulator.get(state, 0.0)
|
|
183
|
+
- hardware.get(state, 0.0)
|
|
184
|
+
),
|
|
185
|
+
}
|
|
186
|
+
for state in states
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def calculate_total_variation_distance(
|
|
191
|
+
counts_a: Mapping[str, int],
|
|
192
|
+
counts_b: Mapping[str, int],
|
|
193
|
+
) -> float:
|
|
194
|
+
"""
|
|
195
|
+
Calculate total variation distance between
|
|
196
|
+
two measurement distributions.
|
|
197
|
+
|
|
198
|
+
TVD = 1/2 * sum(|P(i) - Q(i)|)
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Value between 0 and 1.
|
|
202
|
+
"""
|
|
203
|
+
comparison = compare_measurements(counts_a, counts_b)
|
|
204
|
+
|
|
205
|
+
return 0.5 * sum(
|
|
206
|
+
values["difference"]
|
|
207
|
+
for values in comparison.values()
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def measurement_summary(
|
|
212
|
+
counts: Mapping[str, int],
|
|
213
|
+
) -> dict:
|
|
214
|
+
"""
|
|
215
|
+
Generate a statistical summary of measurement results.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Dictionary with shot counts, probabilities,
|
|
219
|
+
percentages and most frequent state.
|
|
220
|
+
"""
|
|
221
|
+
probabilities = calculate_probabilities(counts)
|
|
222
|
+
|
|
223
|
+
total_shots = sum(counts.values())
|
|
224
|
+
|
|
225
|
+
if total_shots == 0:
|
|
226
|
+
return {
|
|
227
|
+
"total_shots": 0,
|
|
228
|
+
"unique_states": 0,
|
|
229
|
+
"most_frequent_state": None,
|
|
230
|
+
"probabilities": {},
|
|
231
|
+
"percentages": {},
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
"total_shots": total_shots,
|
|
236
|
+
"unique_states": sum(
|
|
237
|
+
count > 0 for count in counts.values()
|
|
238
|
+
),
|
|
239
|
+
"most_frequent_state": max(
|
|
240
|
+
counts, key=counts.get
|
|
241
|
+
),
|
|
242
|
+
"probabilities": probabilities,
|
|
243
|
+
"percentages": calculate_percentages(counts),
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _validate_counts(
|
|
248
|
+
counts: Mapping[str, int],
|
|
249
|
+
) -> None:
|
|
250
|
+
"""
|
|
251
|
+
Validate measurement count data.
|
|
252
|
+
"""
|
|
253
|
+
for state, count in counts.items():
|
|
254
|
+
|
|
255
|
+
if not isinstance(state, str):
|
|
256
|
+
raise TypeError(
|
|
257
|
+
"Measurement states must be strings."
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if not isinstance(count, (int, np.integer)):
|
|
261
|
+
raise TypeError(
|
|
262
|
+
"Measurement counts must be integers."
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if count < 0:
|
|
266
|
+
raise ValueError(
|
|
267
|
+
"Measurement counts cannot be negative."
|
|
268
|
+
)
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
Visualization utilities for quantum computing.
|
|
4
|
+
|
|
5
|
+
Visualize quantum states, probabilities, measurement
|
|
6
|
+
counts, operators, and simulator/hardware comparisons.
|
|
7
|
+
|
|
8
|
+
Compatible with Jupyter and standard Python.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from collections.abc import Mapping, Sequence
|
|
12
|
+
|
|
13
|
+
import matplotlib.pyplot as plt
|
|
14
|
+
import numpy as np
|
|
15
|
+
from matplotlib.figure import Figure
|
|
16
|
+
|
|
17
|
+
from .measurements import (
|
|
18
|
+
calculate_probabilities,
|
|
19
|
+
calculate_state_probabilities,
|
|
20
|
+
compare_measurements,
|
|
21
|
+
calculate_total_variation_distance,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_statevector(statevector) -> np.ndarray:
|
|
26
|
+
"""Convert a statevector into a validated NumPy array."""
|
|
27
|
+
if hasattr(statevector, "data"):
|
|
28
|
+
statevector = statevector.data
|
|
29
|
+
|
|
30
|
+
array = np.asarray(statevector, dtype=complex)
|
|
31
|
+
|
|
32
|
+
if array.ndim != 1:
|
|
33
|
+
raise ValueError("Statevector must be one-dimensional.")
|
|
34
|
+
|
|
35
|
+
size = len(array)
|
|
36
|
+
if size == 0 or size & (size - 1):
|
|
37
|
+
raise ValueError("Statevector length must be a power of two.")
|
|
38
|
+
|
|
39
|
+
return array
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _basis_labels(size: int) -> list[str]:
|
|
43
|
+
"""Create computational basis labels."""
|
|
44
|
+
if size <= 0 or size & (size - 1):
|
|
45
|
+
raise ValueError("Dimension must be a power of two.")
|
|
46
|
+
|
|
47
|
+
num_qubits = max(1, size.bit_length() - 1)
|
|
48
|
+
|
|
49
|
+
return [
|
|
50
|
+
format(i, f"0{num_qubits}b")
|
|
51
|
+
for i in range(size)
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _finish(
|
|
56
|
+
fig: Figure,
|
|
57
|
+
show: bool,
|
|
58
|
+
) -> Figure:
|
|
59
|
+
"""Apply layout and optionally display a figure."""
|
|
60
|
+
fig.tight_layout()
|
|
61
|
+
|
|
62
|
+
if show:
|
|
63
|
+
plt.show()
|
|
64
|
+
|
|
65
|
+
return fig
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def display_data(
|
|
69
|
+
data,
|
|
70
|
+
label: str = "Quantum Data",
|
|
71
|
+
show: bool = True,
|
|
72
|
+
) -> Figure:
|
|
73
|
+
"""
|
|
74
|
+
Display a quantum vector or matrix with numeric values.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
data: One-dimensional vector or two-dimensional matrix.
|
|
78
|
+
label: Figure title.
|
|
79
|
+
show: Whether to display the figure.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Matplotlib Figure.
|
|
83
|
+
"""
|
|
84
|
+
array = np.asarray(data, dtype=complex)
|
|
85
|
+
|
|
86
|
+
if array.ndim not in (1, 2):
|
|
87
|
+
raise ValueError("Data must be a vector or matrix.")
|
|
88
|
+
|
|
89
|
+
if array.size == 0:
|
|
90
|
+
raise ValueError("Data cannot be empty.")
|
|
91
|
+
|
|
92
|
+
matrix = array.reshape(-1, 1) if array.ndim == 1 else array
|
|
93
|
+
|
|
94
|
+
rows, cols = matrix.shape
|
|
95
|
+
|
|
96
|
+
fig, ax = plt.subplots(
|
|
97
|
+
figsize=(
|
|
98
|
+
max(5, min(cols * 1.2, 16)),
|
|
99
|
+
max(2.5, min(rows * 0.55 + 1, 18)),
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
ax.axis("off")
|
|
104
|
+
|
|
105
|
+
formatted = [
|
|
106
|
+
[
|
|
107
|
+
f"{value.real:.3f}"
|
|
108
|
+
if np.isclose(value.imag, 0)
|
|
109
|
+
else f"{value.real:.3f}{value.imag:+.3f}j"
|
|
110
|
+
for value in row
|
|
111
|
+
]
|
|
112
|
+
for row in matrix
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
table = ax.table(
|
|
116
|
+
cellText=formatted,
|
|
117
|
+
loc="center",
|
|
118
|
+
cellLoc="center",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
table.auto_set_font_size(False)
|
|
122
|
+
table.set_fontsize(10)
|
|
123
|
+
table.scale(1, 1.5)
|
|
124
|
+
|
|
125
|
+
ax.set_title(label, pad=18)
|
|
126
|
+
|
|
127
|
+
return _finish(fig, show)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def display_probabilities(
|
|
131
|
+
statevector: Sequence[complex],
|
|
132
|
+
label: str = "Theoretical Probabilities",
|
|
133
|
+
show: bool = True,
|
|
134
|
+
) -> Figure:
|
|
135
|
+
"""
|
|
136
|
+
Display theoretical probabilities using Born's rule.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
statevector: Normalized quantum statevector.
|
|
140
|
+
label: Figure title.
|
|
141
|
+
show: Whether to display.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Matplotlib Figure.
|
|
145
|
+
"""
|
|
146
|
+
probabilities = calculate_state_probabilities(statevector)
|
|
147
|
+
|
|
148
|
+
states = list(probabilities)
|
|
149
|
+
values = [100 * p for p in probabilities.values()]
|
|
150
|
+
|
|
151
|
+
fig, ax = plt.subplots(figsize=(max(6, len(states) * 0.75), 4))
|
|
152
|
+
|
|
153
|
+
bars = ax.bar(states, values)
|
|
154
|
+
|
|
155
|
+
ax.set_title(label)
|
|
156
|
+
ax.set_xlabel("Computational basis state")
|
|
157
|
+
ax.set_ylabel("Probability (%)")
|
|
158
|
+
ax.set_ylim(0, max(5, max(values) * 1.2))
|
|
159
|
+
|
|
160
|
+
for bar, value in zip(bars, values):
|
|
161
|
+
ax.text(
|
|
162
|
+
bar.get_x() + bar.get_width() / 2,
|
|
163
|
+
bar.get_height(),
|
|
164
|
+
f"{value:.2f}%",
|
|
165
|
+
ha="center",
|
|
166
|
+
va="bottom",
|
|
167
|
+
fontsize=9,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return _finish(fig, show)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def display_counts(
|
|
174
|
+
counts: Mapping[str, int],
|
|
175
|
+
label: str = "Measurement Results",
|
|
176
|
+
show: bool = True,
|
|
177
|
+
) -> Figure:
|
|
178
|
+
"""
|
|
179
|
+
Display quantum measurement counts.
|
|
180
|
+
|
|
181
|
+
Works with counts from both simulators and real hardware.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
counts: Dictionary mapping bitstrings to shot counts.
|
|
185
|
+
label: Name of the simulator or quantum computer.
|
|
186
|
+
show: Whether to display.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Matplotlib Figure.
|
|
190
|
+
"""
|
|
191
|
+
probabilities = calculate_probabilities(counts)
|
|
192
|
+
|
|
193
|
+
if not probabilities:
|
|
194
|
+
raise ValueError("No measurement shots available.")
|
|
195
|
+
|
|
196
|
+
states = list(probabilities)
|
|
197
|
+
values = [100 * p for p in probabilities.values()]
|
|
198
|
+
total_shots = sum(counts.values())
|
|
199
|
+
|
|
200
|
+
fig, ax = plt.subplots(figsize=(max(6, len(states) * 0.75), 4))
|
|
201
|
+
|
|
202
|
+
bars = ax.bar(states, values)
|
|
203
|
+
|
|
204
|
+
ax.set_title(f"{label}\n{total_shots} shots")
|
|
205
|
+
ax.set_xlabel("Measured state")
|
|
206
|
+
ax.set_ylabel("Frequency (%)")
|
|
207
|
+
ax.set_ylim(0, max(5, max(values) * 1.2))
|
|
208
|
+
|
|
209
|
+
for bar, state, value in zip(bars, states, values):
|
|
210
|
+
ax.text(
|
|
211
|
+
bar.get_x() + bar.get_width() / 2,
|
|
212
|
+
bar.get_height(),
|
|
213
|
+
f"{counts[state]} ({value:.1f}%)",
|
|
214
|
+
ha="center",
|
|
215
|
+
va="bottom",
|
|
216
|
+
fontsize=9,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return _finish(fig, show)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def compare_counts(
|
|
223
|
+
simulator_counts: Mapping[str, int],
|
|
224
|
+
hardware_counts: Mapping[str, int],
|
|
225
|
+
labels: tuple[str, str] = ("Simulator", "Hardware"),
|
|
226
|
+
show: bool = True,
|
|
227
|
+
) -> Figure:
|
|
228
|
+
"""
|
|
229
|
+
Compare simulator and hardware measurement distributions.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
simulator_counts: Simulation results.
|
|
233
|
+
hardware_counts: Real hardware results.
|
|
234
|
+
labels: Names displayed in the legend.
|
|
235
|
+
show: Whether to display.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Matplotlib Figure.
|
|
239
|
+
"""
|
|
240
|
+
if len(labels) != 2:
|
|
241
|
+
raise ValueError("Exactly two labels are required.")
|
|
242
|
+
|
|
243
|
+
comparison = compare_measurements(
|
|
244
|
+
simulator_counts,
|
|
245
|
+
hardware_counts,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
states = list(comparison)
|
|
249
|
+
simulator = [
|
|
250
|
+
comparison[s]["simulator"] * 100 for s in states
|
|
251
|
+
]
|
|
252
|
+
hardware = [
|
|
253
|
+
comparison[s]["hardware"] * 100 for s in states
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
x = np.arange(len(states))
|
|
257
|
+
width = 0.36
|
|
258
|
+
|
|
259
|
+
fig, ax = plt.subplots(
|
|
260
|
+
figsize=(max(7, len(states) * 0.9), 4.5)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
ax.bar(
|
|
264
|
+
x - width / 2,
|
|
265
|
+
simulator,
|
|
266
|
+
width,
|
|
267
|
+
label=labels[0],
|
|
268
|
+
)
|
|
269
|
+
ax.bar(
|
|
270
|
+
x + width / 2,
|
|
271
|
+
hardware,
|
|
272
|
+
width,
|
|
273
|
+
label=labels[1],
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
tvd = calculate_total_variation_distance(
|
|
277
|
+
simulator_counts,
|
|
278
|
+
hardware_counts,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
ax.set_title(
|
|
282
|
+
f"Quantum Measurement Comparison\n"
|
|
283
|
+
f"Total Variation Distance: {tvd:.4f}"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
ax.set_xlabel("Measured state")
|
|
287
|
+
ax.set_ylabel("Frequency (%)")
|
|
288
|
+
ax.set_xticks(x, states)
|
|
289
|
+
ax.set_ylim(
|
|
290
|
+
0,
|
|
291
|
+
max(5, max(simulator + hardware) * 1.2),
|
|
292
|
+
)
|
|
293
|
+
ax.legend()
|
|
294
|
+
|
|
295
|
+
return _finish(fig, show)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def display_operator_heatmap(
|
|
299
|
+
operator,
|
|
300
|
+
label: str = "Quantum Operator",
|
|
301
|
+
show: bool = True,
|
|
302
|
+
) -> Figure:
|
|
303
|
+
"""
|
|
304
|
+
Display the magnitude of a quantum operator matrix.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
operator: Square matrix or compatible Qiskit Operator.
|
|
308
|
+
label: Figure title.
|
|
309
|
+
show: Whether to display.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Matplotlib Figure.
|
|
313
|
+
"""
|
|
314
|
+
if hasattr(operator, "data"):
|
|
315
|
+
operator = operator.data
|
|
316
|
+
|
|
317
|
+
matrix = np.asarray(operator, dtype=complex)
|
|
318
|
+
|
|
319
|
+
if matrix.ndim != 2 or matrix.shape[0] != matrix.shape[1]:
|
|
320
|
+
raise ValueError("Operator must be a square matrix.")
|
|
321
|
+
|
|
322
|
+
size = matrix.shape[0]
|
|
323
|
+
states = _basis_labels(size)
|
|
324
|
+
|
|
325
|
+
magnitude = np.abs(matrix)
|
|
326
|
+
|
|
327
|
+
fig, ax = plt.subplots(
|
|
328
|
+
figsize=(max(5, size * 0.9), max(4, size * 0.8))
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
image = ax.imshow(
|
|
332
|
+
magnitude,
|
|
333
|
+
cmap="Blues",
|
|
334
|
+
vmin=0,
|
|
335
|
+
vmax=max(1, float(magnitude.max())),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
ax.set_xticks(range(size), [f"|{s}⟩" for s in states])
|
|
339
|
+
ax.set_yticks(range(size), [f"|{s}⟩" for s in states])
|
|
340
|
+
ax.set_xlabel("Input state")
|
|
341
|
+
ax.set_ylabel("Output state")
|
|
342
|
+
ax.set_title(label)
|
|
343
|
+
|
|
344
|
+
for row in range(size):
|
|
345
|
+
for col in range(size):
|
|
346
|
+
value = matrix[row, col]
|
|
347
|
+
|
|
348
|
+
if not np.isclose(value, 0):
|
|
349
|
+
text = (
|
|
350
|
+
f"{value.real:.2f}"
|
|
351
|
+
if np.isclose(value.imag, 0)
|
|
352
|
+
else f"{value.real:.2f}{value.imag:+.2f}j"
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
ax.text(
|
|
356
|
+
col,
|
|
357
|
+
row,
|
|
358
|
+
text,
|
|
359
|
+
ha="center",
|
|
360
|
+
va="center",
|
|
361
|
+
fontsize=9,
|
|
362
|
+
color=(
|
|
363
|
+
"white"
|
|
364
|
+
if magnitude[row, col] > 0.65
|
|
365
|
+
else "black"
|
|
366
|
+
),
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
fig.colorbar(image, ax=ax, label="Magnitude")
|
|
370
|
+
|
|
371
|
+
return _finish(fig, show)
|