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 ADDED
@@ -0,0 +1,6 @@
1
+ # qtomos/__init__.py
2
+ # Marks qtomos/ as a Python package.
3
+
4
+ from .circuits_catalog import create_ghz
5
+ from .acquisition import measure_observable, measure_all_observables
6
+
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.3.4)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ qtomos = qtomos.cli:main
@@ -0,0 +1 @@
1
+ qtomos