qtomos 0.1.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.
- qtomos/__init__.py +6 -0
- qtomos/acquisition.py +186 -0
- qtomos/circuits_catalog.py +67 -0
- qtomos/cli.py +65 -0
- qtomos/utils.py +73 -0
- qtomos-0.1.0.dist-info/METADATA +225 -0
- qtomos-0.1.0.dist-info/RECORD +10 -0
- qtomos-0.1.0.dist-info/WHEEL +5 -0
- qtomos-0.1.0.dist-info/entry_points.txt +2 -0
- qtomos-0.1.0.dist-info/top_level.txt +1 -0
qtomos/__init__.py
ADDED
qtomos/acquisition.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# lib/acquisition.py
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import datetime
|
|
5
|
+
import os
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
|
|
8
|
+
from spinqit import NMRConfig, get_basic_simulator, get_compiler, BasicSimulatorConfig, get_nmr, draw as sq_draw, Circuit
|
|
9
|
+
from spinqit import H, Sd, QasmBackend
|
|
10
|
+
from spinqit.backend.nmr_backend import NMRBackend
|
|
11
|
+
|
|
12
|
+
load_dotenv()
|
|
13
|
+
|
|
14
|
+
TWO_QUBIT_OBSERVABLES = [
|
|
15
|
+
"XX", "XY", "XZ",
|
|
16
|
+
"YX", "YY", "YZ",
|
|
17
|
+
"ZX", "ZY", "ZZ",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
THREE_QUBIT_OBSERVABLES = [
|
|
21
|
+
"XXX", "XXY", "XXZ",
|
|
22
|
+
"XYX", "XYY", "XYZ",
|
|
23
|
+
"XZX", "XZY", "XZZ",
|
|
24
|
+
|
|
25
|
+
"YXX", "YXY", "YXZ",
|
|
26
|
+
"YYX", "YYY", "YYZ",
|
|
27
|
+
"YZX", "YZY", "YZZ",
|
|
28
|
+
|
|
29
|
+
"ZXX", "ZXY", "ZXZ",
|
|
30
|
+
"ZYX", "ZYY", "ZYZ",
|
|
31
|
+
"ZZX", "ZZY", "ZZZ",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
def append_observation_basis(circuit: Circuit, observable: str):
|
|
35
|
+
"""
|
|
36
|
+
Appends the necessary gates to change the measurement basis
|
|
37
|
+
to match the Pauli observable.
|
|
38
|
+
|
|
39
|
+
This function applies the required gates (H for X, Sd+H for Y) sequentially
|
|
40
|
+
to the first N qubits of the circuit, where N is the length of the observable string.
|
|
41
|
+
|
|
42
|
+
Examples of `observable` strings: "XX", "XY", "XYZ", "ZZZ"
|
|
43
|
+
"""
|
|
44
|
+
for qubit_index, pauli in enumerate(observable):
|
|
45
|
+
if pauli == "X":
|
|
46
|
+
circuit << (H, qubit_index)
|
|
47
|
+
elif pauli == "Y":
|
|
48
|
+
circuit << (Sd, qubit_index)
|
|
49
|
+
circuit << (H, qubit_index)
|
|
50
|
+
elif pauli == "Z":
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def simulate(c: Circuit, shots: int = 1024):
|
|
54
|
+
comp = get_compiler("native")
|
|
55
|
+
engine = get_basic_simulator()
|
|
56
|
+
# Compile
|
|
57
|
+
optimization_level = 0
|
|
58
|
+
exe = comp.compile(c, optimization_level)
|
|
59
|
+
# Run
|
|
60
|
+
config = BasicSimulatorConfig()
|
|
61
|
+
config.configure_shots(shots)
|
|
62
|
+
result = engine.execute(exe, config)
|
|
63
|
+
return result.counts
|
|
64
|
+
|
|
65
|
+
def run(c: Circuit, shots: int = 1024):
|
|
66
|
+
IP = os.environ.get("IP")
|
|
67
|
+
PORT = int(os.environ.get("PORT"))
|
|
68
|
+
USERNAME = os.environ.get("USERNAME")
|
|
69
|
+
PASSWORD = os.environ.get("PASSWORD")
|
|
70
|
+
|
|
71
|
+
comp = get_compiler("native")
|
|
72
|
+
optimization_level = 0
|
|
73
|
+
exe = comp.compile(c, optimization_level)
|
|
74
|
+
engine = get_nmr()
|
|
75
|
+
config = NMRConfig()
|
|
76
|
+
config.configure_ip(IP)
|
|
77
|
+
config.configure_port(PORT)
|
|
78
|
+
config.configure_account(USERNAME, PASSWORD)
|
|
79
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
80
|
+
task_name = getattr(c, 'name', "circuit")
|
|
81
|
+
task_desc = f"Execution of {task_name} at {timestamp}"
|
|
82
|
+
config.configure_task(task_name, task_desc)
|
|
83
|
+
config.configure_shots(shots)
|
|
84
|
+
resultado = engine.execute(exe, config)
|
|
85
|
+
return resultado.counts
|
|
86
|
+
|
|
87
|
+
def draw(c: Circuit):
|
|
88
|
+
compiler = get_compiler('native')
|
|
89
|
+
ir = compiler.compile(c, level=0)
|
|
90
|
+
name = getattr(c, 'name', "circuit")
|
|
91
|
+
filename = f"{name.replace(' ', '_')}.png"
|
|
92
|
+
sq_draw(ir, filename=filename)
|
|
93
|
+
print(f"Circuit drawing saved to {filename}")
|
|
94
|
+
|
|
95
|
+
def normalize_counts(counts, endian="big"):
|
|
96
|
+
return {
|
|
97
|
+
str(bitstring) if endian == "big" else str(bitstring)[::-1]: int(count)
|
|
98
|
+
for bitstring, count in counts.items()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
def measure_observable(circuit: Circuit, observable: str, mode: str, endian="big", shots: int = 1024):
|
|
102
|
+
circuit_name = getattr(circuit, 'name', 'circuit')
|
|
103
|
+
print(f"Starting measurement task for circuit '{circuit_name}', observable '{observable}'...")
|
|
104
|
+
|
|
105
|
+
c = copy.deepcopy(circuit)
|
|
106
|
+
append_observation_basis(c, observable)
|
|
107
|
+
|
|
108
|
+
start_time = datetime.datetime.now().astimezone().isoformat()
|
|
109
|
+
|
|
110
|
+
# Compile to get QASM representations
|
|
111
|
+
compiler = get_compiler('native')
|
|
112
|
+
ir = compiler.compile(c, level=0)
|
|
113
|
+
qasm_str = QasmBackend.convert_ir_to_qasm(ir)
|
|
114
|
+
|
|
115
|
+
# Assemble to Native hardware IR and get Native QASM
|
|
116
|
+
ir_native = copy.deepcopy(ir)
|
|
117
|
+
try:
|
|
118
|
+
NMRBackend().assemble(ir_native)
|
|
119
|
+
native_qasm_str = QasmBackend.convert_ir_to_qasm(ir_native)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
native_qasm_str = f"Error generating native QASM: {e}"
|
|
122
|
+
|
|
123
|
+
if mode == "draw":
|
|
124
|
+
draw(c)
|
|
125
|
+
counts = {}
|
|
126
|
+
elif mode == "qpu":
|
|
127
|
+
counts = run(c, shots)
|
|
128
|
+
else:
|
|
129
|
+
counts = simulate(c, shots)
|
|
130
|
+
|
|
131
|
+
end_time = datetime.datetime.now().astimezone().isoformat()
|
|
132
|
+
|
|
133
|
+
print(f"Finished measurement task for circuit '{circuit_name}', observable '{observable}'.")
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
observable: {
|
|
137
|
+
"circuit_name": circuit_name,
|
|
138
|
+
"mode": mode,
|
|
139
|
+
"shots": shots,
|
|
140
|
+
"endian": endian,
|
|
141
|
+
"timestamps": {
|
|
142
|
+
"start": start_time,
|
|
143
|
+
"end": end_time
|
|
144
|
+
},
|
|
145
|
+
"counts": normalize_counts(counts, endian),
|
|
146
|
+
"qasm": qasm_str,
|
|
147
|
+
"native": native_qasm_str
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
def measure_all_observables(circuit: Circuit, mode: str, endian="big", shots: int = 1024):
|
|
152
|
+
start_time = datetime.datetime.now().astimezone().isoformat()
|
|
153
|
+
results = {}
|
|
154
|
+
qubits = circuit.qubits_num
|
|
155
|
+
observables = TWO_QUBIT_OBSERVABLES if qubits == 2 else THREE_QUBIT_OBSERVABLES
|
|
156
|
+
|
|
157
|
+
for observable in observables:
|
|
158
|
+
obs_data = measure_observable(circuit, observable, mode, endian, shots)
|
|
159
|
+
res = obs_data[observable]
|
|
160
|
+
|
|
161
|
+
# Remove common metadata properties to avoid duplication
|
|
162
|
+
res.pop("circuit_name", None)
|
|
163
|
+
res.pop("mode", None)
|
|
164
|
+
res.pop("shots", None)
|
|
165
|
+
res.pop("endian", None)
|
|
166
|
+
|
|
167
|
+
results[observable] = res
|
|
168
|
+
|
|
169
|
+
end_time = datetime.datetime.now().astimezone().isoformat()
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
"metadata": {
|
|
173
|
+
"circuit_name": getattr(circuit, 'name', "circuit"),
|
|
174
|
+
"qubits": qubits,
|
|
175
|
+
"mode": mode,
|
|
176
|
+
"shots": shots,
|
|
177
|
+
"endian": endian,
|
|
178
|
+
"timestamps": {
|
|
179
|
+
"start": start_time,
|
|
180
|
+
"end": end_time
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
"measurements": results
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# lib/circuits_catalog.py
|
|
2
|
+
|
|
3
|
+
from spinqit import Circuit, H, CX, Ry, Rx, X
|
|
4
|
+
import math
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
def create_ghz(qubits: int) -> Circuit:
|
|
8
|
+
"""Generates a GHZ state preparation circuit."""
|
|
9
|
+
c = Circuit()
|
|
10
|
+
c.name = "ghz"
|
|
11
|
+
c.allocateQubits(qubits)
|
|
12
|
+
|
|
13
|
+
c << (H, 0)
|
|
14
|
+
for i in range(1, qubits):
|
|
15
|
+
c.append(CX, [0, i])
|
|
16
|
+
|
|
17
|
+
return c
|
|
18
|
+
|
|
19
|
+
def create_phi_plus(qubits: int) -> Circuit:
|
|
20
|
+
"""Creates a 2-qubit Phi+ Bell state. Ignores qubits > 2."""
|
|
21
|
+
c = Circuit()
|
|
22
|
+
c.name = "phi_plus"
|
|
23
|
+
c.allocateQubits(2)
|
|
24
|
+
c << (H, 0)
|
|
25
|
+
c << (CX, [0, 1])
|
|
26
|
+
return c
|
|
27
|
+
|
|
28
|
+
def create_w(qubits: int) -> Circuit:
|
|
29
|
+
"""Creates a 3-qubit W state. Ignores qubits parameter."""
|
|
30
|
+
c = Circuit()
|
|
31
|
+
c.name = "w"
|
|
32
|
+
c.allocateQubits(3)
|
|
33
|
+
|
|
34
|
+
# 1. Ry to create 1/sqrt(3)|0> + sqrt(2/3)|1> on q0
|
|
35
|
+
theta = 2 * math.acos(1 / math.sqrt(3))
|
|
36
|
+
c.append(Ry, [0], [], theta)
|
|
37
|
+
|
|
38
|
+
# 2. Controlled-H equivalent (acts as H on |0> when controlled)
|
|
39
|
+
c.append(Ry, [1], [], math.pi/4)
|
|
40
|
+
c.append(CX, [0, 1])
|
|
41
|
+
c.append(Ry, [1], [], -math.pi/4)
|
|
42
|
+
|
|
43
|
+
# 3. Entangle q2
|
|
44
|
+
c.append(CX, [1, 2])
|
|
45
|
+
|
|
46
|
+
# 4. Flip remaining states
|
|
47
|
+
c.append(CX, [0, 1])
|
|
48
|
+
c.append(X, [0])
|
|
49
|
+
|
|
50
|
+
return c
|
|
51
|
+
|
|
52
|
+
def create_random(qubits: int) -> Circuit:
|
|
53
|
+
"""Creates a random parameterized circuit for the given number of qubits."""
|
|
54
|
+
c = Circuit()
|
|
55
|
+
c.name = "random"
|
|
56
|
+
c.allocateQubits(qubits)
|
|
57
|
+
|
|
58
|
+
# Apply random rotations
|
|
59
|
+
for i in range(qubits):
|
|
60
|
+
c.append(Rx, [i], [], random.uniform(0, 2*math.pi))
|
|
61
|
+
c.append(Ry, [i], [], random.uniform(0, 2*math.pi))
|
|
62
|
+
|
|
63
|
+
# Apply entangling chain
|
|
64
|
+
for i in range(qubits - 1):
|
|
65
|
+
c.append(CX, [i, i+1])
|
|
66
|
+
|
|
67
|
+
return c
|
qtomos/cli.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import argparse
|
|
3
|
+
import inspect
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
from . import circuits_catalog
|
|
6
|
+
from .acquisition import measure_observable, measure_all_observables
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
load_dotenv()
|
|
10
|
+
|
|
11
|
+
parser = argparse.ArgumentParser(description="Acquire SpinQ Tomographic Data")
|
|
12
|
+
|
|
13
|
+
# Dynamically discover circuits in the catalog
|
|
14
|
+
circuit_funcs = {
|
|
15
|
+
name.replace("create_", ""): func
|
|
16
|
+
for name, func in inspect.getmembers(circuits_catalog, inspect.isfunction)
|
|
17
|
+
if name.startswith("create_")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
parser.add_argument("-m", "--mode", choices=["sim", "qpu", "draw"], required=True, help="Execution mode: sim (simulator), qpu (real computer), or draw (print circuit)")
|
|
21
|
+
parser.add_argument("-c", "--circuit", choices=list(circuit_funcs.keys()), required=True, help="Circuit to prepare")
|
|
22
|
+
parser.add_argument("-q", "--qubits", type=int, help="Number of qubits (inferred from observable if omitted, defaults to 3)")
|
|
23
|
+
parser.add_argument("-e", "--endian", choices=["big", "little"], default="big", help="Endianness for output bitstrings: big (q[0] is leftmost) or little (q[0] is rightmost)")
|
|
24
|
+
parser.add_argument("--shots", type=int, default=1024, help="Number of shots for execution")
|
|
25
|
+
|
|
26
|
+
parser.add_argument("-f", "--file", type=str, required=True, help="Output JSON file path")
|
|
27
|
+
parser.add_argument("-o", "--observable", type=str, help="Measure a single observable (e.g., XX, XYZ)")
|
|
28
|
+
|
|
29
|
+
args = parser.parse_args()
|
|
30
|
+
|
|
31
|
+
# Determine number of qubits
|
|
32
|
+
if args.qubits is not None:
|
|
33
|
+
num_qubits = args.qubits
|
|
34
|
+
else:
|
|
35
|
+
num_qubits = len(args.observable) if args.observable else 3
|
|
36
|
+
|
|
37
|
+
# Retrieve the dynamically selected circuit creation function
|
|
38
|
+
create_func = circuit_funcs[args.circuit]
|
|
39
|
+
c = create_func(num_qubits)
|
|
40
|
+
|
|
41
|
+
# Check for inconsistencies
|
|
42
|
+
if args.observable and len(args.observable) != c.qubits_num:
|
|
43
|
+
parser.error(f"Length of observable '{args.observable}' ({len(args.observable)}) does not match the actual circuit size ({c.qubits_num} qubits).")
|
|
44
|
+
|
|
45
|
+
if args.observable:
|
|
46
|
+
output = measure_observable(
|
|
47
|
+
circuit=c,
|
|
48
|
+
observable=args.observable,
|
|
49
|
+
mode=args.mode,
|
|
50
|
+
endian=args.endian,
|
|
51
|
+
shots=args.shots
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
output = measure_all_observables(
|
|
55
|
+
circuit=c,
|
|
56
|
+
mode=args.mode,
|
|
57
|
+
endian=args.endian,
|
|
58
|
+
shots=args.shots
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
with open(args.file, "w") as f:
|
|
62
|
+
json.dump(output, f, indent=2)
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
main()
|
qtomos/utils.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# lib/utils.py
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
def get_pauli(label):
|
|
6
|
+
"""Return the Pauli matrix corresponding to the label."""
|
|
7
|
+
paulis = {
|
|
8
|
+
'I': np.array([[1, 0], [0, 1]], dtype=complex),
|
|
9
|
+
'X': np.array([[0, 1], [1, 0]], dtype=complex),
|
|
10
|
+
'Y': np.array([[0, -1j], [1j, 0]], dtype=complex),
|
|
11
|
+
'Z': np.array([[1, 0], [0, -1]], dtype=complex)
|
|
12
|
+
}
|
|
13
|
+
return paulis[label]
|
|
14
|
+
|
|
15
|
+
def construct_pauli_string(p_str):
|
|
16
|
+
"""Construct an N-qubit Pauli matrix from a string (e.g. 'XX')."""
|
|
17
|
+
result = np.array([[1]])
|
|
18
|
+
for p in p_str:
|
|
19
|
+
result = np.kron(result, get_pauli(p))
|
|
20
|
+
return result
|
|
21
|
+
|
|
22
|
+
def expectation_value(counts, endian="big"):
|
|
23
|
+
"""
|
|
24
|
+
Calculate the expectation value from counts.
|
|
25
|
+
If the bitstring has an even number of 1s, it corresponds to eigenvalue +1.
|
|
26
|
+
If it has an odd number of 1s, it corresponds to eigenvalue -1.
|
|
27
|
+
"""
|
|
28
|
+
total = sum(counts.values())
|
|
29
|
+
if total == 0:
|
|
30
|
+
return 0.0
|
|
31
|
+
|
|
32
|
+
exp_val = 0.0
|
|
33
|
+
for bitstring, count in counts.items():
|
|
34
|
+
# count number of 1s
|
|
35
|
+
ones = bitstring.count('1')
|
|
36
|
+
eigenvalue = 1 if ones % 2 == 0 else -1
|
|
37
|
+
exp_val += eigenvalue * (count / total)
|
|
38
|
+
|
|
39
|
+
return exp_val
|
|
40
|
+
|
|
41
|
+
def marginal_expectation_value(counts, p_str, m_str):
|
|
42
|
+
"""
|
|
43
|
+
Calculate the expectation value of a Pauli string p_str (which may contain 'I')
|
|
44
|
+
from the counts of a measured Pauli string m_str (which has no 'I' and matches
|
|
45
|
+
p_str on all non-'I' positions).
|
|
46
|
+
"""
|
|
47
|
+
total = sum(counts.values())
|
|
48
|
+
if total == 0:
|
|
49
|
+
return 0.0
|
|
50
|
+
|
|
51
|
+
active_indices = [i for i, char in enumerate(p_str) if char != 'I']
|
|
52
|
+
if not active_indices:
|
|
53
|
+
return 1.0
|
|
54
|
+
|
|
55
|
+
exp_val = 0.0
|
|
56
|
+
for bitstring, count in counts.items():
|
|
57
|
+
sub_bits = [bitstring[i] for i in active_indices]
|
|
58
|
+
ones = sub_bits.count('1')
|
|
59
|
+
eigenvalue = 1 if ones % 2 == 0 else -1
|
|
60
|
+
exp_val += eigenvalue * (count / total)
|
|
61
|
+
|
|
62
|
+
return exp_val
|
|
63
|
+
|
|
64
|
+
def matches(p_str, m_str):
|
|
65
|
+
"""Check if the measured Pauli string m_str matches the target p_str."""
|
|
66
|
+
for p_char, m_char in zip(p_str, m_str):
|
|
67
|
+
if p_char != 'I' and p_char != m_char:
|
|
68
|
+
return False
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
def generate_all_pauli_strings(n_qubits):
|
|
72
|
+
import itertools
|
|
73
|
+
return [''.join(p) for p in itertools.product(['I', 'X', 'Y', 'Z'], repeat=n_qubits)]
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: qtomos
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A SpinQ quantum state tomography data acquisition toolset
|
|
5
|
+
Author-email: Tomas <tomas@example.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: spinqit
|
|
11
|
+
Requires-Dist: python-dotenv
|
|
12
|
+
|
|
13
|
+
# Quantum State Tomography
|
|
14
|
+
|
|
15
|
+
Quantum State Tomography is the process of completely characterizing the quantum state of a system by performing a series of measurements on identical copies of the state. It allows us to mathematically reconstruct the density matrix, which fully describes the system.
|
|
16
|
+
|
|
17
|
+
Quantum State Tomography is generally a two-phase process:
|
|
18
|
+
1. **Data Acquisition**: Run quantum circuits on a simulator or real hardware to gather measurement statistics for a complete set of observables. In this toolset, we specifically use the complete set of tensor products of the non-identity Pauli matrices ($X, Y, Z$).
|
|
19
|
+
2. **State Reconstruction**: Use the acquired measurement data to mathematically reconstruct the density matrix of the quantum state.
|
|
20
|
+
|
|
21
|
+
> [!NOTE]
|
|
22
|
+
> This toolset is solely focused on **Data Acquisition (`qtomos`)**. State reconstruction is not handled by this repository.
|
|
23
|
+
|
|
24
|
+
Currently, data acquisition supports multiple predefined quantum states (GHZ, Phi+, W, and random circuits) defined in the `circuits_catalog`. You can dynamically select which circuit to prepare during acquisition.
|
|
25
|
+
|
|
26
|
+
This is now you basically use this tool:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# acquire data for the complete set of tensor products of the non-identity Pauli matrices (X, Y, Z), on the noiseless simulator, on a three qubit GHZ, using 500 shots for each measurement, saving the results to output.json
|
|
30
|
+
qtomos --circuit ghz --mode sim --shots 500 --file output.json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Read on to learn how to install and use this tool.
|
|
34
|
+
|
|
35
|
+
**IMPORTANT**: to connect to a real SpinQ QPU you need to provide your connection credentials. Read section "Acquire Data from the QPU" below. Do not put your access credentials in a file that is commited to the repository.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
# Install
|
|
40
|
+
|
|
41
|
+
SpinQit currently works only on Python 3.8.
|
|
42
|
+
|
|
43
|
+
The file .python-version will most likely take care of setting up your environment with the correct Python version (if 3.8 is installed on your machine; if not, use pyenv, Conda or whatever manager you prefer to install it).
|
|
44
|
+
|
|
45
|
+
We suggest installing everything in a virtual environment.
|
|
46
|
+
|
|
47
|
+
To set up your environment, run:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
python -m venv .venv
|
|
51
|
+
source .venv/bin/activate
|
|
52
|
+
pip install -e .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
On Arm based Macs, you'll have issues with the default location of SPinQit libraries. Use the ```fix-spinqit-macos-arm.sh```script to fix it (changes will only affect that venv)
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Data Acquisition
|
|
60
|
+
|
|
61
|
+
The first time you run `qtomos`, it may take longer (the SpinQ SDK might be downloading required assets).
|
|
62
|
+
|
|
63
|
+
### Simulate Acquisition
|
|
64
|
+
|
|
65
|
+
To run a simulation for a specific observable (e.g., `XX`) on a GHZ state:
|
|
66
|
+
```bash
|
|
67
|
+
qtomos --circuit ghz --mode sim --observable XX --file output.json
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Selecting a Circuit
|
|
71
|
+
|
|
72
|
+
You can select the quantum state to prepare using the `-c` or `--circuit` argument. The CLI dynamically exposes all circuits defined in `qtomos/circuits_catalog.py`. Current available states include `ghz` (default), `phi_plus`, `w`, and `random`.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Acquire the ZZZ observable for a W state
|
|
76
|
+
qtomos --circuit w --mode sim --observable ZZZ --file output.json
|
|
77
|
+
|
|
78
|
+
# Acquire the full set for a random circuit
|
|
79
|
+
qtomos --circuit random --mode sim --file output.json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
By default, the measurement bitstrings use Big-Endian format (qubit 0 is the leftmost bit). If you prefer Little-Endian (qubit 0 is the rightmost bit, similar to Qiskit), use the `--endian little` flag:
|
|
83
|
+
```bash
|
|
84
|
+
qtomos --circuit ghz --mode sim --observable XX --endian little --file output.json
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Acquire Data from the QPU
|
|
88
|
+
|
|
89
|
+
Before running on the real hardware (QPU), you need to configure your environment variables.
|
|
90
|
+
Copy the `.env.example` file to `.env` and fill in your connection details:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
cp .env.example .env
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Edit `.env` to match your credentials:
|
|
97
|
+
```env
|
|
98
|
+
IP=192.168.172.233
|
|
99
|
+
PORT=50177
|
|
100
|
+
USERNAME=your_username
|
|
101
|
+
PASSWORD=your_password
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Then, to acquire data for the same specific observable on the real hardware:
|
|
105
|
+
```bash
|
|
106
|
+
qtomos --circuit ghz --mode qpu --observable XX --file output.json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Drawing Circuits
|
|
110
|
+
|
|
111
|
+
To generate a visual representation of the quantum circuit instead of simulating it or running it on the QPU, use the `draw` mode. This will save a `.png` image of the circuit in your current directory (e.g., `XX_of_a_Ghz.png`):
|
|
112
|
+
```bash
|
|
113
|
+
qtomos --circuit ghz --mode draw --observable XX --file output.json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Full Tomographic Acquisition
|
|
117
|
+
|
|
118
|
+
To perform a full tomographic acquisition (all observables), omit the `--observable` argument. This defaults to 3 qubits.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# 3-qubit full tomographic acquisition on simulator
|
|
122
|
+
qtomos --circuit ghz --mode sim --file output.json
|
|
123
|
+
|
|
124
|
+
# 3-qubit full tomographic acquisition on QPU
|
|
125
|
+
qtomos --circuit ghz --mode qpu --file output.json
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Parametrizing Shots
|
|
129
|
+
|
|
130
|
+
By default, execution uses `1024` shots. You can customize the number of shots using the `--shots` flag:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
qtomos --circuit ghz --mode sim --file output.json --shots 500
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Saving Output and Format
|
|
137
|
+
|
|
138
|
+
The output JSON file has the following structure:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"metadata": {
|
|
143
|
+
"circuit_name": "ghz",
|
|
144
|
+
"qubits": 2,
|
|
145
|
+
"mode": "sim",
|
|
146
|
+
"shots": 500,
|
|
147
|
+
"endian": "big",
|
|
148
|
+
"timestamps": {
|
|
149
|
+
"start": "2026-06-25T20:51:33.528965-03:00",
|
|
150
|
+
"end": "2026-06-25T20:51:33.530752-03:00"
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"measurements": {
|
|
154
|
+
"XX": {
|
|
155
|
+
"timestamps": {
|
|
156
|
+
"start": "2026-06-25T20:51:33.529028-03:00",
|
|
157
|
+
"end": "2026-06-25T20:51:33.530740-03:00"
|
|
158
|
+
},
|
|
159
|
+
"counts": {
|
|
160
|
+
"00": 250,
|
|
161
|
+
"11": 250
|
|
162
|
+
},
|
|
163
|
+
"qasm": "...",
|
|
164
|
+
"native": "..."
|
|
165
|
+
},
|
|
166
|
+
...
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Help (qtomos)
|
|
172
|
+
|
|
173
|
+
For a complete list of options, use the `--help` flag:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
$ qtomos --help
|
|
177
|
+
usage: qtomos [-h] [-m {sim,qpu,draw}] [-c {ghz,phi_plus,random,w}]
|
|
178
|
+
[-q QUBITS] [-e {big,little}] [--shots SHOTS] -f FILE
|
|
179
|
+
[-o OBSERVABLE]
|
|
180
|
+
|
|
181
|
+
Acquire SpinQ Tomographic Data
|
|
182
|
+
|
|
183
|
+
optional arguments:
|
|
184
|
+
-h, --help show this help message and exit
|
|
185
|
+
-m {sim,qpu,draw}, --mode {sim,qpu,draw}
|
|
186
|
+
Execution mode: sim (simulator), qpu (real computer),
|
|
187
|
+
or draw (print circuit)
|
|
188
|
+
-c {ghz,phi_plus,random,w}, --circuit {ghz,phi_plus,random,w}
|
|
189
|
+
Circuit to prepare
|
|
190
|
+
-q QUBITS, --qubits QUBITS
|
|
191
|
+
Number of qubits (inferred from observable if omitted,
|
|
192
|
+
defaults to 3)
|
|
193
|
+
-e {big,little}, --endian {big,little}
|
|
194
|
+
Endianness for output bitstrings: big (q[0] is
|
|
195
|
+
leftmost) or little (q[0] is rightmost)
|
|
196
|
+
--shots SHOTS Number of shots for execution
|
|
197
|
+
-f FILE, --file FILE Output JSON file path
|
|
198
|
+
-o OBSERVABLE, --observable OBSERVABLE
|
|
199
|
+
Measure a single observable (e.g., XX, XYZ)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
## Project Structure
|
|
204
|
+
|
|
205
|
+
The codebase is structured as follows:
|
|
206
|
+
|
|
207
|
+
* **`pyproject.toml`**: The package configuration file defining dependencies and the CLI entry point.
|
|
208
|
+
* **`qtomos/`**: Directory containing the project's internal modules and CLI:
|
|
209
|
+
* **`qtomos/__init__.py`**: Initializer that exposes `qtomos` as a Python package.
|
|
210
|
+
* **`qtomos/cli.py`**: CLI entry-point script for data acquisition. It handles argument parsing (simulator, real QPU, or circuit drawing, shots, endianness) and prints the structured JSON output with metadata.
|
|
211
|
+
* **`qtomos/acquisition.py`**: Contains the core logic and programmatic API (`measure_observable` and `measure_all_observables`) for executing the quantum circuits and gathering measurement statistics.
|
|
212
|
+
* **`qtomos/circuits_catalog.py`**: A catalog of pre-defined quantum circuits. The CLI dynamically discovers states defined here (e.g., `ghz`, `phi_plus`, `w`, `random`).
|
|
213
|
+
* **`qtomos/utils.py`**: Contains all shared mathematical utilities, including Pauli basis generation, match filtering, and average expectation calculation for marginal operators.
|
|
214
|
+
* **`tests/`**: Directory for automated tests:
|
|
215
|
+
* **`tests/test_tomography.py`**: Unit test suite to validate supporting mathematical operations.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Running Unit Tests
|
|
220
|
+
|
|
221
|
+
To run the automated unit tests and verify the consistency of the project, execute the following command from the repository root:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
python tests/test_tomography.py
|
|
225
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
qtomos/__init__.py,sha256=gA0HOy7-XNvFIsiecv17qFY8HJSfMLBEbaGKpTnt3Fc,170
|
|
2
|
+
qtomos/acquisition.py,sha256=mA2B-r2dieIWAQkUkNpPQcrmKkscBi_ovO8lC0K33dQ,5759
|
|
3
|
+
qtomos/circuits_catalog.py,sha256=BN_kMzr1JK6EnrDo5FlDE0RlFqQHYJUPgKWUPsrt3EU,1712
|
|
4
|
+
qtomos/cli.py,sha256=9GmTqqqirFZfuXFQ6wtaF4UST8RTjinW7S4XPrQxskY,2583
|
|
5
|
+
qtomos/utils.py,sha256=zh1KsvzxsW_I2UQ0pgvL1JtCRXm-weAcw9n7pbv09Ko,2349
|
|
6
|
+
qtomos-0.1.0.dist-info/METADATA,sha256=GnIXnDL_qsm91pC-Y9A8QKa3Dqo-Pr8gnArQW01ZpXc,8589
|
|
7
|
+
qtomos-0.1.0.dist-info/WHEEL,sha256=BNRMDyzLkkcmlv0J8ppDQkk2VED33SesJDynr9ED1gc,91
|
|
8
|
+
qtomos-0.1.0.dist-info/entry_points.txt,sha256=cpFSVjh9Ac7xw7c1jtLMkgtD0-eLlej6ljtBRpyrIlQ,43
|
|
9
|
+
qtomos-0.1.0.dist-info/top_level.txt,sha256=mOU_AweQC25ZmDdwueoM8kHDGI4NqqlMmRz0d7PIOHs,7
|
|
10
|
+
qtomos-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
qtomos
|