qiskit-state-evolution-recorder 0.1.0__tar.gz
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.
- qiskit_state_evolution_recorder-0.1.0/LICENSE +28 -0
- qiskit_state_evolution_recorder-0.1.0/PKG-INFO +39 -0
- qiskit_state_evolution_recorder-0.1.0/README.md +20 -0
- qiskit_state_evolution_recorder-0.1.0/pyproject.toml +21 -0
- qiskit_state_evolution_recorder-0.1.0/qiskit_state_evolution_recorder/__init__.py +497 -0
- qiskit_state_evolution_recorder-0.1.0/qiskit_state_evolution_recorder.egg-info/PKG-INFO +39 -0
- qiskit_state_evolution_recorder-0.1.0/qiskit_state_evolution_recorder.egg-info/SOURCES.txt +10 -0
- qiskit_state_evolution_recorder-0.1.0/qiskit_state_evolution_recorder.egg-info/dependency_links.txt +1 -0
- qiskit_state_evolution_recorder-0.1.0/qiskit_state_evolution_recorder.egg-info/requires.txt +6 -0
- qiskit_state_evolution_recorder-0.1.0/qiskit_state_evolution_recorder.egg-info/top_level.txt +1 -0
- qiskit_state_evolution_recorder-0.1.0/setup.cfg +4 -0
- qiskit_state_evolution_recorder-0.1.0/setup.py +34 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Dawid Ciepiela
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: qiskit_state_evolution_recorder
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Simple module allowing to record animations to trace changes in qubit states for arbitrary quantum circuits.
|
|
5
|
+
Home-page: https://github.com/sarumaj/qiskit-state-evolution-recorder
|
|
6
|
+
Author: Dawid Ciepiela
|
|
7
|
+
Author-email: Dawid Ciepiela <71898979+sarumaj@users.noreply.github.com>
|
|
8
|
+
License: BSD-3-Clause
|
|
9
|
+
Project-URL: Homepage, https://github.com/sarumaj/qiskit-state-evolution-recorder
|
|
10
|
+
Requires-Python: >=3.6, <3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: matplotlib==3.9.2
|
|
14
|
+
Requires-Dist: matplotlib-inline==0.1.7
|
|
15
|
+
Requires-Dist: numpy==2.1.1
|
|
16
|
+
Requires-Dist: qiskit==1.2.4
|
|
17
|
+
Requires-Dist: qiskit-aer==0.15.1
|
|
18
|
+
Requires-Dist: pillow==11.0.0
|
|
19
|
+
|
|
20
|
+
# qiskit-state-evolution-recorder
|
|
21
|
+
|
|
22
|
+
Simple module allowing to record animations to trace changes in qubit states for arbitrary quantum circuits.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from qiskit.circuit import QuantumCircuit
|
|
28
|
+
from qiskit_state_evolution_recorder import StateEvolutionRecorder
|
|
29
|
+
|
|
30
|
+
qc = QuantumCircuit(4)
|
|
31
|
+
qc.h(range(4))
|
|
32
|
+
qc.cx(range(3), 3)
|
|
33
|
+
qc.h(range(4))
|
|
34
|
+
qc.measure_all()
|
|
35
|
+
|
|
36
|
+
recorder = StateEvolutionRecorder(qc, figsize=(12, 8), num_cols=4, style={'name': 'bw'})
|
|
37
|
+
recorder.evolve(120)
|
|
38
|
+
recorder.record("quantum_circuit.mp4", fps=30)
|
|
39
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# qiskit-state-evolution-recorder
|
|
2
|
+
|
|
3
|
+
Simple module allowing to record animations to trace changes in qubit states for arbitrary quantum circuits.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from qiskit.circuit import QuantumCircuit
|
|
9
|
+
from qiskit_state_evolution_recorder import StateEvolutionRecorder
|
|
10
|
+
|
|
11
|
+
qc = QuantumCircuit(4)
|
|
12
|
+
qc.h(range(4))
|
|
13
|
+
qc.cx(range(3), 3)
|
|
14
|
+
qc.h(range(4))
|
|
15
|
+
qc.measure_all()
|
|
16
|
+
|
|
17
|
+
recorder = StateEvolutionRecorder(qc, figsize=(12, 8), num_cols=4, style={'name': 'bw'})
|
|
18
|
+
recorder.evolve(120)
|
|
19
|
+
recorder.record("quantum_circuit.mp4", fps=30)
|
|
20
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "qiskit_state_evolution_recorder"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Simple module allowing to record animations to trace changes in qubit states for arbitrary quantum circuits."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = {text = "BSD-3-Clause"}
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Dawid Ciepiela", email = "71898979+sarumaj@users.noreply.github.com" }
|
|
9
|
+
]
|
|
10
|
+
dependencies = [
|
|
11
|
+
"matplotlib==3.9.2",
|
|
12
|
+
"matplotlib-inline==0.1.7",
|
|
13
|
+
"numpy==2.1.1",
|
|
14
|
+
"qiskit==1.2.4",
|
|
15
|
+
"qiskit-aer==0.15.1",
|
|
16
|
+
"pillow==11.0.0"
|
|
17
|
+
]
|
|
18
|
+
requires-python = ">=3.6, <3.12"
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
"Homepage" = "https://github.com/sarumaj/qiskit-state-evolution-recorder"
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
from qiskit.circuit import InstructionSet, QuantumCircuit
|
|
2
|
+
from qiskit.quantum_info import Statevector
|
|
3
|
+
from qiskit.visualization import plot_bloch_vector
|
|
4
|
+
from qiskit.visualization.state_visualization import _bloch_multivector_data
|
|
5
|
+
|
|
6
|
+
from numpy import linspace, frombuffer, uint8, ndarray, ceil
|
|
7
|
+
from numpy.linalg import norm
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
import matplotlib.gridspec as gridspec
|
|
10
|
+
from matplotlib.animation import FuncAnimation
|
|
11
|
+
|
|
12
|
+
from typing import Iterable, Union, Generator
|
|
13
|
+
from multiprocessing import Process, Queue, cpu_count
|
|
14
|
+
from tempfile import gettempdir
|
|
15
|
+
from os import remove, path as os_path
|
|
16
|
+
from PIL.Image import open as open_image
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
class StateEvolutionRecorder:
|
|
20
|
+
"""
|
|
21
|
+
A class to record the evolution of a quantum state through a quantum circuit
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
-----------
|
|
25
|
+
qc: QuantumCircuit
|
|
26
|
+
The quantum circuit to record
|
|
27
|
+
|
|
28
|
+
initial_state: Statevector
|
|
29
|
+
The initial state of the quantum circuit
|
|
30
|
+
|
|
31
|
+
Methods:
|
|
32
|
+
--------
|
|
33
|
+
evolve(intermediate_steps: int = 0)
|
|
34
|
+
Evolve the initial state through the circuit
|
|
35
|
+
|
|
36
|
+
record(filename: str, *, fps: int = 60, interval: int = 200, disk: bool = False)
|
|
37
|
+
Record the frames into a video file
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
qc: QuantumCircuit,
|
|
43
|
+
initial_state: Statevector = None,
|
|
44
|
+
figsize: tuple[float, float] = None,
|
|
45
|
+
dpi: float = None,
|
|
46
|
+
num_cols: int = 5,
|
|
47
|
+
select: list[int] = None,
|
|
48
|
+
style: dict = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Initialize the StateEvolutionRecorder
|
|
52
|
+
|
|
53
|
+
Parameters:
|
|
54
|
+
-----------
|
|
55
|
+
qc: QuantumCircuit
|
|
56
|
+
The quantum circuit to record
|
|
57
|
+
|
|
58
|
+
initial_state: Statevector
|
|
59
|
+
The initial state of the quantum circuit, default is |0>^n
|
|
60
|
+
|
|
61
|
+
figsize: tuple[float, float]
|
|
62
|
+
The size of the figure, default is (6, 6)
|
|
63
|
+
|
|
64
|
+
dpi: float
|
|
65
|
+
The resolution of the figure, default is 100
|
|
66
|
+
|
|
67
|
+
select: list[int]
|
|
68
|
+
The qubits to select for the bloch vectors to display, default is all qubits
|
|
69
|
+
|
|
70
|
+
style: dict
|
|
71
|
+
The style of the quantum circuit diagram, default is {'name':'textbook'}
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
self._qc = qc
|
|
75
|
+
self._states = None
|
|
76
|
+
self._size = 1
|
|
77
|
+
self._initial_state = initial_state or Statevector.from_label('0'*self._qc.num_qubits)
|
|
78
|
+
self._fig = plt.figure(figsize=figsize, dpi=dpi)
|
|
79
|
+
self._selected_qubits = list(range(self._qc.num_qubits)) if not select else sorted(select)
|
|
80
|
+
num_cols = num_cols if 0 < num_cols <= len(self._selected_qubits) else 3
|
|
81
|
+
num_rows = 1 + int(ceil(len(self._selected_qubits) / num_cols))
|
|
82
|
+
self._gs = gridspec.GridSpec(num_rows, num_cols)
|
|
83
|
+
self._gs.set_height_ratios([3] + [2] * (num_rows - 1))
|
|
84
|
+
self._ax = [[self._fig.add_subplot(self._gs[0, :])]]
|
|
85
|
+
self._qubit_partitions = [
|
|
86
|
+
self._selected_qubits[i:i + num_cols]
|
|
87
|
+
for i in range(
|
|
88
|
+
0,
|
|
89
|
+
min(
|
|
90
|
+
len(self._selected_qubits),
|
|
91
|
+
num_cols * num_rows
|
|
92
|
+
),
|
|
93
|
+
num_cols
|
|
94
|
+
)
|
|
95
|
+
]
|
|
96
|
+
self._ax.extend([
|
|
97
|
+
[
|
|
98
|
+
self._fig.add_subplot(self._gs[i + 1, j], projection="3d")
|
|
99
|
+
for j in range(len(partition))
|
|
100
|
+
]
|
|
101
|
+
for i, partition in enumerate(self._qubit_partitions)
|
|
102
|
+
])
|
|
103
|
+
for axes in self._ax[1:]:
|
|
104
|
+
for ax in axes:
|
|
105
|
+
ax.axis('off')
|
|
106
|
+
self._text = None
|
|
107
|
+
qc.draw(output='mpl', fold=-1, style=style if style else {'name':'textbook'} , ax=self._ax[0][0])
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def evolve(self, intermediate_steps: int = 0):
|
|
112
|
+
"""
|
|
113
|
+
Evolve the initial state through the circuit
|
|
114
|
+
|
|
115
|
+
Parameters:
|
|
116
|
+
-----------
|
|
117
|
+
intermediate_steps: int
|
|
118
|
+
The number of intermediate steps to interpolate between two adjacent states, default is 0
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def group_instructions() -> list[list[InstructionSet]]:
|
|
122
|
+
"""
|
|
123
|
+
Group the instructions taking place in parallel based on the qubits involved or barriers
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
--------
|
|
127
|
+
list[list[InstructionSet]]: A list of grouped instructions taking place in parallel
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
current_group = []
|
|
131
|
+
active_qubits = set()
|
|
132
|
+
instructions = []
|
|
133
|
+
|
|
134
|
+
for instruction in self._qc.data:
|
|
135
|
+
if instruction.operation.name == 'barrier':
|
|
136
|
+
if current_group:
|
|
137
|
+
instructions.append(current_group)
|
|
138
|
+
current_group = []
|
|
139
|
+
active_qubits.clear()
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
if instruction.operation.name == 'measure':
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
# Get the qubits involved in the instruction
|
|
146
|
+
qubits_involved = set(f"{q._register.prefix}{q._index}" for q in instruction.qubits)
|
|
147
|
+
|
|
148
|
+
# If the qubits involved in the current instruction are not the same as the active qubits
|
|
149
|
+
if active_qubits & qubits_involved: # If there is an intersection
|
|
150
|
+
instructions.append(current_group)
|
|
151
|
+
current_group = []
|
|
152
|
+
active_qubits.clear()
|
|
153
|
+
|
|
154
|
+
current_group.append(instruction)
|
|
155
|
+
active_qubits.update(qubits_involved)
|
|
156
|
+
|
|
157
|
+
if current_group:
|
|
158
|
+
instructions.append(current_group)
|
|
159
|
+
|
|
160
|
+
return instructions
|
|
161
|
+
|
|
162
|
+
# Group the instructions taking place in parallel
|
|
163
|
+
grouped_instructions = group_instructions()
|
|
164
|
+
|
|
165
|
+
# The number of frames to render
|
|
166
|
+
self._size = len(grouped_instructions)+1
|
|
167
|
+
|
|
168
|
+
def generate_states() -> Generator[tuple[Statevector, Iterable[InstructionSet]], None, None]:
|
|
169
|
+
"""
|
|
170
|
+
Generate the states of the quantum circuit at each step
|
|
171
|
+
|
|
172
|
+
Yields:
|
|
173
|
+
-------
|
|
174
|
+
tuple[Statevector, Iterable[InstructionSet]]:
|
|
175
|
+
A tuple containing the state of the quantum circuit and
|
|
176
|
+
the instructions responsible for the state transition
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
state = self._initial_state
|
|
180
|
+
for instructionSet in grouped_instructions:
|
|
181
|
+
yield (state, instructionSet)
|
|
182
|
+
|
|
183
|
+
sub_circuit = QuantumCircuit(*self._qc.qregs, *self._qc.cregs)
|
|
184
|
+
for instruction in instructionSet:
|
|
185
|
+
sub_circuit.append(instruction.operation, instruction.qubits, instruction.clbits)
|
|
186
|
+
|
|
187
|
+
state = state.evolve(sub_circuit)
|
|
188
|
+
|
|
189
|
+
yield (state, [])
|
|
190
|
+
|
|
191
|
+
# If there are no intermediate steps
|
|
192
|
+
if intermediate_steps <= 1:
|
|
193
|
+
self._states = generate_states()
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
def interpolate_states() -> Generator[tuple[Statevector, Iterable[InstructionSet]], None, None]:
|
|
197
|
+
"""
|
|
198
|
+
Interpolate between two adjacent states to smooth the transition
|
|
199
|
+
|
|
200
|
+
Yields:
|
|
201
|
+
-------
|
|
202
|
+
tuple[Statevector, Iterable[InstructionSet]]:
|
|
203
|
+
A tuple containing the state of the quantum circuit and
|
|
204
|
+
the instructions responsible for the state transition
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
states = list(generate_states())
|
|
208
|
+
for lhs, rhs in zip(states[:-1], states[1:]):
|
|
209
|
+
for i in linspace(0, 1, intermediate_steps, endpoint=False):
|
|
210
|
+
intermediate_state = (1 - i) * lhs[0].data + i * rhs[0].data
|
|
211
|
+
state_norm = norm(intermediate_state)
|
|
212
|
+
if state_norm:
|
|
213
|
+
intermediate_state = intermediate_state / state_norm
|
|
214
|
+
yield (Statevector(intermediate_state), lhs[1])
|
|
215
|
+
|
|
216
|
+
yield states[-1]
|
|
217
|
+
|
|
218
|
+
# Interpolate between each pair of two adjacent states to smooth the transition
|
|
219
|
+
self._states = interpolate_states()
|
|
220
|
+
# Update the size to reflect the number of intermediate steps
|
|
221
|
+
self._size += len(grouped_instructions) * (intermediate_steps-1)
|
|
222
|
+
|
|
223
|
+
def _render_frame(
|
|
224
|
+
self,
|
|
225
|
+
index: int,
|
|
226
|
+
frame_data: tuple[Statevector, Iterable[InstructionSet]],
|
|
227
|
+
disk: bool
|
|
228
|
+
) -> tuple[int, Union[str, ndarray[uint8]]]:
|
|
229
|
+
"""
|
|
230
|
+
Render a frame. If disk is True, save the frames to disk at the OS-specific temporary directory.
|
|
231
|
+
|
|
232
|
+
Parameters:
|
|
233
|
+
-----------
|
|
234
|
+
index: int
|
|
235
|
+
The index of the frame
|
|
236
|
+
|
|
237
|
+
frame_data: tuple[Statevector, Iterable[InstructionSet]]
|
|
238
|
+
A tuple containing the state of the quantum circuit and
|
|
239
|
+
the instructions responsible for the state transition
|
|
240
|
+
|
|
241
|
+
disk: bool
|
|
242
|
+
Whether to save the frame to hard disk or not
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
--------
|
|
246
|
+
tuple[int, Union[str, ndarray[uint8]]]:
|
|
247
|
+
A tuple containing the index of the frame and the frame data
|
|
248
|
+
either as a filename or as an image array
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
# Remove the text describing the operations
|
|
252
|
+
if self._text:
|
|
253
|
+
self._text.remove()
|
|
254
|
+
|
|
255
|
+
# Clear the axes for the bloch vectors
|
|
256
|
+
for axes in self._ax[1:]:
|
|
257
|
+
for ax in axes:
|
|
258
|
+
ax.clear()
|
|
259
|
+
|
|
260
|
+
state, operations = frame_data
|
|
261
|
+
|
|
262
|
+
# Allocate the axes for the bloch vectors
|
|
263
|
+
if len(self._ax) < 2:
|
|
264
|
+
self._ax.extend([
|
|
265
|
+
[
|
|
266
|
+
self._fig.add_subplot(self._gs[i + 1, j], projection="3d")
|
|
267
|
+
for j in range(len(partition))
|
|
268
|
+
]
|
|
269
|
+
for i, partition in enumerate(self._qubit_partitions)
|
|
270
|
+
])
|
|
271
|
+
|
|
272
|
+
# Plot the bloch vectors
|
|
273
|
+
bloch_data = _bloch_multivector_data(state)
|
|
274
|
+
for i, partition in enumerate(self._qubit_partitions):
|
|
275
|
+
for j in range(len(partition)):
|
|
276
|
+
plot_bloch_vector(bloch_data[partition[j]], f"q{partition[j]}", ax=self._ax[i + 1][j])
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# Describe the operations taking place
|
|
280
|
+
if operations:
|
|
281
|
+
fragments = []
|
|
282
|
+
for gate in operations:
|
|
283
|
+
fragments.append("{0} -> {1}".format(gate.operation.name, [f"{q._register._name}{q._index}" for q in gate.qubits]))
|
|
284
|
+
|
|
285
|
+
self._text = self._fig.text(
|
|
286
|
+
0.5, 0.95,
|
|
287
|
+
" | ".join(fragments),
|
|
288
|
+
ha='center', va='bottom',
|
|
289
|
+
fontsize=15,
|
|
290
|
+
transform=self._fig.transFigure
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Update the figure
|
|
294
|
+
self._fig.canvas.draw()
|
|
295
|
+
|
|
296
|
+
# Save the frame to disk
|
|
297
|
+
if disk:
|
|
298
|
+
filename = os_path.join(gettempdir(), f"{index}.png")
|
|
299
|
+
self._fig.savefig(filename, format="png")
|
|
300
|
+
return index, filename
|
|
301
|
+
|
|
302
|
+
# Return the frame as an image array
|
|
303
|
+
buf = self._fig.canvas.tostring_rgb()
|
|
304
|
+
dim = self._fig.canvas.get_width_height()[::-1] + (3,)
|
|
305
|
+
img = frombuffer(buf, dtype=uint8).reshape(dim)
|
|
306
|
+
return index, img
|
|
307
|
+
|
|
308
|
+
def _render_frames(self, disk: bool = False) -> Generator[Union[str, ndarray[uint8]], None, None]:
|
|
309
|
+
"""
|
|
310
|
+
Render the frames
|
|
311
|
+
|
|
312
|
+
Parameters:
|
|
313
|
+
-----------
|
|
314
|
+
disk: bool
|
|
315
|
+
Whether to save the frames to disk or not
|
|
316
|
+
|
|
317
|
+
Yields:
|
|
318
|
+
-------
|
|
319
|
+
Union[str, ndarray[uint8]]:
|
|
320
|
+
The frame data either as a filename or as an image array
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
def generate_frames() -> Generator[tuple[int, Union[str, ndarray[uint8]]], None, None]:
|
|
324
|
+
"""
|
|
325
|
+
Generate the frames using multiple processes
|
|
326
|
+
|
|
327
|
+
Yields:
|
|
328
|
+
-------
|
|
329
|
+
tuple[int, Union[str, ndarray[uint8]]]:
|
|
330
|
+
A tuple containing the index of the frame and the frame data
|
|
331
|
+
either as a filename or as an image array
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
task_queue = Queue()
|
|
335
|
+
result_queue = Queue()
|
|
336
|
+
|
|
337
|
+
def task():
|
|
338
|
+
"""
|
|
339
|
+
The task to be executed by each process
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
while True:
|
|
343
|
+
args = task_queue.get()
|
|
344
|
+
|
|
345
|
+
# If there are no arguments left, terminate the process
|
|
346
|
+
if args is None:
|
|
347
|
+
break
|
|
348
|
+
|
|
349
|
+
index, frame_data, disk = args
|
|
350
|
+
result_queue.put(self._render_frame(index, frame_data, disk))
|
|
351
|
+
|
|
352
|
+
# Create a process for each CPU core
|
|
353
|
+
processes = [Process(target=task) for _ in range(cpu_count())]
|
|
354
|
+
for process in processes:
|
|
355
|
+
process.start()
|
|
356
|
+
|
|
357
|
+
# Distribute the tasks among the processes to render a frame for each state
|
|
358
|
+
for index, frame_data in enumerate(self._states):
|
|
359
|
+
task_queue.put((index, frame_data, disk))
|
|
360
|
+
|
|
361
|
+
# Terminate the processes
|
|
362
|
+
for _ in processes:
|
|
363
|
+
task_queue.put(None)
|
|
364
|
+
|
|
365
|
+
# Yield the frames in the order they were rendered
|
|
366
|
+
for i in range(self._size):
|
|
367
|
+
yield result_queue.get()
|
|
368
|
+
self._progress_callback(i, self._size, msg="Rendering frames")
|
|
369
|
+
|
|
370
|
+
# Wait for all the processes to terminate
|
|
371
|
+
for _ in processes:
|
|
372
|
+
process.join()
|
|
373
|
+
|
|
374
|
+
# Sort the frames in the order they were scheduled
|
|
375
|
+
return (frame for _, frame in sorted(generate_frames(), key=lambda x: x[0]))
|
|
376
|
+
|
|
377
|
+
def _update(self, image: Union[str, ndarray[uint8]], disk: bool):
|
|
378
|
+
"""
|
|
379
|
+
Update the figure with the new frame
|
|
380
|
+
|
|
381
|
+
Parameters:
|
|
382
|
+
-----------
|
|
383
|
+
image: Union[str, ndarray[uint8]]
|
|
384
|
+
The frame data either as a filename or as an image array
|
|
385
|
+
|
|
386
|
+
disk: bool
|
|
387
|
+
Whether the frame data is a filename or an image array
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
# Remove the text describing the operations
|
|
391
|
+
if self._text:
|
|
392
|
+
self._text.remove()
|
|
393
|
+
|
|
394
|
+
# Remove the axes from the figure
|
|
395
|
+
if len(self._ax) > 1:
|
|
396
|
+
for axes in self._ax:
|
|
397
|
+
for ax in axes:
|
|
398
|
+
ax.clear()
|
|
399
|
+
ax.remove()
|
|
400
|
+
# Create a new axes for the image
|
|
401
|
+
self._ax = [[self._fig.add_axes([0, 0, 1, 1])]]
|
|
402
|
+
|
|
403
|
+
# If the frame data is a filename, open the image
|
|
404
|
+
if disk:
|
|
405
|
+
image = open_image(image)
|
|
406
|
+
|
|
407
|
+
# Display the image
|
|
408
|
+
self._ax[0][0].clear()
|
|
409
|
+
self._ax[0][0].imshow(image, aspect='auto')
|
|
410
|
+
self._ax[0][0].axis('off')
|
|
411
|
+
|
|
412
|
+
# Update the figure
|
|
413
|
+
self._fig.canvas.draw()
|
|
414
|
+
|
|
415
|
+
def _progress_callback(self, frame: int, total_frames: int, bar_length: int = 40, msg: str = "Progress:"):
|
|
416
|
+
"""
|
|
417
|
+
Display a progress bar
|
|
418
|
+
|
|
419
|
+
Parameters:
|
|
420
|
+
-----------
|
|
421
|
+
frame: int
|
|
422
|
+
The current frame number
|
|
423
|
+
|
|
424
|
+
total_frames: int
|
|
425
|
+
The total number of frames to render
|
|
426
|
+
|
|
427
|
+
bar_length: int
|
|
428
|
+
The length of the progress bar
|
|
429
|
+
|
|
430
|
+
msg: str
|
|
431
|
+
The message to display along with the progress bar
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
progress = (frame + 1) / total_frames
|
|
435
|
+
percent = int(progress * 100)
|
|
436
|
+
filled_length = int(bar_length * progress)
|
|
437
|
+
bar = '█' * filled_length + '-' * (bar_length - filled_length)
|
|
438
|
+
template = f"\r{msg}: |{bar}| {percent:>3}% ({{frame:>{len(str(total_frames))}}}/{total_frames})"
|
|
439
|
+
print(template.format(frame=frame + 1), end=" "*10)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def record(self, filename: str, *, fps: int = 60, interval: int = 200, disk: bool = False):
|
|
443
|
+
"""
|
|
444
|
+
Record the frames into a video file
|
|
445
|
+
|
|
446
|
+
Parameters:
|
|
447
|
+
-----------
|
|
448
|
+
filename: str
|
|
449
|
+
The name of the video file to create
|
|
450
|
+
|
|
451
|
+
fps: int
|
|
452
|
+
The number of frames per second
|
|
453
|
+
|
|
454
|
+
interval: int
|
|
455
|
+
The interval between each frame in milliseconds
|
|
456
|
+
|
|
457
|
+
disk: bool
|
|
458
|
+
Whether to save the frames to disk or not
|
|
459
|
+
"""
|
|
460
|
+
|
|
461
|
+
try:
|
|
462
|
+
start_time = time.time()
|
|
463
|
+
# Create an animation object
|
|
464
|
+
anim = FuncAnimation(self._fig,
|
|
465
|
+
self._update,
|
|
466
|
+
frames=self._render_frames(disk),
|
|
467
|
+
fargs=(disk,),
|
|
468
|
+
save_count=self._size,
|
|
469
|
+
cache_frame_data=False,
|
|
470
|
+
interval=interval)
|
|
471
|
+
# Save the animation to a video file
|
|
472
|
+
anim.save(filename,
|
|
473
|
+
writer="ffmpeg",
|
|
474
|
+
fps=fps,
|
|
475
|
+
progress_callback=lambda frame, total_frames: self._progress_callback(frame, total_frames, msg="Encoding video"))
|
|
476
|
+
|
|
477
|
+
except KeyboardInterrupt:
|
|
478
|
+
# If the recording was interrupted, remove the video file
|
|
479
|
+
remove(filename)
|
|
480
|
+
|
|
481
|
+
else:
|
|
482
|
+
elapsed_time = time.time() - start_time
|
|
483
|
+
print(f"\nRecording finished. Elapsed time: {elapsed_time:.2f} seconds")
|
|
484
|
+
|
|
485
|
+
finally:
|
|
486
|
+
# Close the figure
|
|
487
|
+
plt.close(self._fig)
|
|
488
|
+
|
|
489
|
+
# Remove the frames from disk if any
|
|
490
|
+
if disk:
|
|
491
|
+
for index in range(self._size):
|
|
492
|
+
filename = os_path.join(gettempdir(), f"{index}.png")
|
|
493
|
+
try:
|
|
494
|
+
remove(filename)
|
|
495
|
+
except FileNotFoundError:
|
|
496
|
+
pass
|
|
497
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: qiskit_state_evolution_recorder
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Simple module allowing to record animations to trace changes in qubit states for arbitrary quantum circuits.
|
|
5
|
+
Home-page: https://github.com/sarumaj/qiskit-state-evolution-recorder
|
|
6
|
+
Author: Dawid Ciepiela
|
|
7
|
+
Author-email: Dawid Ciepiela <71898979+sarumaj@users.noreply.github.com>
|
|
8
|
+
License: BSD-3-Clause
|
|
9
|
+
Project-URL: Homepage, https://github.com/sarumaj/qiskit-state-evolution-recorder
|
|
10
|
+
Requires-Python: >=3.6, <3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: matplotlib==3.9.2
|
|
14
|
+
Requires-Dist: matplotlib-inline==0.1.7
|
|
15
|
+
Requires-Dist: numpy==2.1.1
|
|
16
|
+
Requires-Dist: qiskit==1.2.4
|
|
17
|
+
Requires-Dist: qiskit-aer==0.15.1
|
|
18
|
+
Requires-Dist: pillow==11.0.0
|
|
19
|
+
|
|
20
|
+
# qiskit-state-evolution-recorder
|
|
21
|
+
|
|
22
|
+
Simple module allowing to record animations to trace changes in qubit states for arbitrary quantum circuits.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from qiskit.circuit import QuantumCircuit
|
|
28
|
+
from qiskit_state_evolution_recorder import StateEvolutionRecorder
|
|
29
|
+
|
|
30
|
+
qc = QuantumCircuit(4)
|
|
31
|
+
qc.h(range(4))
|
|
32
|
+
qc.cx(range(3), 3)
|
|
33
|
+
qc.h(range(4))
|
|
34
|
+
qc.measure_all()
|
|
35
|
+
|
|
36
|
+
recorder = StateEvolutionRecorder(qc, figsize=(12, 8), num_cols=4, style={'name': 'bw'})
|
|
37
|
+
recorder.evolve(120)
|
|
38
|
+
recorder.record("quantum_circuit.mp4", fps=30)
|
|
39
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
qiskit_state_evolution_recorder/__init__.py
|
|
6
|
+
qiskit_state_evolution_recorder.egg-info/PKG-INFO
|
|
7
|
+
qiskit_state_evolution_recorder.egg-info/SOURCES.txt
|
|
8
|
+
qiskit_state_evolution_recorder.egg-info/dependency_links.txt
|
|
9
|
+
qiskit_state_evolution_recorder.egg-info/requires.txt
|
|
10
|
+
qiskit_state_evolution_recorder.egg-info/top_level.txt
|
qiskit_state_evolution_recorder-0.1.0/qiskit_state_evolution_recorder.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
qiskit_state_evolution_recorder
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="qiskit_state_evolution_recorder",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
description="Simple module allowing to record animations to trace changes in qubit states for arbitrary quantum circuits.",
|
|
7
|
+
long_description=open("README.md").read(),
|
|
8
|
+
long_description_content_type="text/markdown",
|
|
9
|
+
author="Dawid Ciepiela",
|
|
10
|
+
author_email="71898979+sarumaj@users.noreply.github.com",
|
|
11
|
+
url="https://github.com/sarumaj/qiskit-state-evolution-recorder",
|
|
12
|
+
packages=find_packages(),
|
|
13
|
+
install_requires=[
|
|
14
|
+
"matplotlib==3.9.2",
|
|
15
|
+
"matplotlib-inline==0.1.7",
|
|
16
|
+
"numpy==2.1.1",
|
|
17
|
+
"qiskit==1.2.4",
|
|
18
|
+
"qiskit-aer==0.15.1",
|
|
19
|
+
"pillow==11.0.0"
|
|
20
|
+
],
|
|
21
|
+
classifiers=[
|
|
22
|
+
"License :: OSI Approved :: BSD License",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.6",
|
|
25
|
+
"Programming Language :: Python :: 3.7",
|
|
26
|
+
"Programming Language :: Python :: 3.8",
|
|
27
|
+
"Programming Language :: Python :: 3.9",
|
|
28
|
+
"Programming Language :: Python :: 3.10",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Operating System :: OS Independent",
|
|
31
|
+
],
|
|
32
|
+
license="BSD-3-Clause",
|
|
33
|
+
python_requires=">=3.6, <3.12",
|
|
34
|
+
)
|