├── .gitignore ├── README.md ├── examples └── SimplestQKD.py ├── qoord ├── __init__.py ├── __support__.py ├── core_gates.py ├── core_operators.py ├── devices.py ├── gates.py ├── qubits.py └── states.py ├── requirements.txt └── tests ├── __init__.py └── test_qoord.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Pycharm files 132 | .idea 133 | 134 | 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qoord 2 | Tiny, rough-and-ready quantum circuit emulator for exploring quantum 3 | networking and computing. 4 | 5 | Should only require [Numpy](https://numpy.org) to get started. Tests in 6 | `test__init__.py` can be run using the 7 | [`pytest`](https://docs.pytest.org/en/8.0.x/) framework from PyPy. Python 3.10 or higher recommended. 8 | 9 | ## Overview 10 | Qoord is a quantum circuit emulator, written to teach myself about quantum 11 | computing, and (secondarily) to prototype quantum algorithms. 12 | 13 | In ordinary computers, the deepest level of programming (directly on the 14 | chip) uses logic gates (AND, OR, NOT) to manipulate binary bits. Most 15 | quantum computing efforts are also focused on this kind of "gate-based" 16 | computing - devices are built from quantum bits or _qubits_. Quantum 17 | computers also use logical operations, but because qubits have a more 18 | complex behavior than binary logic, the quantum logic gates are very 19 | different. 20 | 21 | Each quantum program is written as a sequence of quantum gates, which are 22 | applied to the qubits. Because quantum programs are still low-level and 23 | operate directly on the hardware, they are often called _quantum 24 | circuits_. Most of us do not have an ion trap or a near-absolute-zero 25 | refrigerator hanging around to build quantum systems with, but we can mimic 26 | quantum computers in software: a quantum circuit emulator is a program to 27 | mimic the behavior of an idealized gate-based quantum computer, as a 28 | substitute for having actual quantum hardware. That's what Qoord does. 29 | 30 | ### Background Material 31 | Getting comfortable with the bra-ket (<| and |>) notation makes a big 32 | difference; they're two types of vectors. The kets |> are basically 33 | vector-valued data, and the bras <| are the coefficients of multivariable 34 | linear functions. The expression is then just a dot product, 35 | e.g. applying the linear model to the data to get a scalar answer. 36 | In quantum circuits, the ket vectors are always the current state of 37 | the quantum register, and the various gate operations are multiplying 38 | the state by a matrix. 39 | 40 | There are a few stumbling blocks related to what kind of data a qubit value 41 | provides - a single qubit stores a 2-dimensional complex-valued vector of 42 | length 1. That's way more data than a classical bit, but you can only 43 | extract one classical bit per measurement and all the rest of the qubit 44 | information is destroyed in the process. 45 | 46 | I strongly recommend https://www.scottaaronson.com/qclec.pdf - Scott 47 | Aaronson's lecture notes for the first semester of quantum information 48 | theory. The beginning parts are good for understanding what a qubit is 49 | and what a gate is doing. Later he goes into algorithms and nonlocal 50 | games, which are incredibly cool. 51 | 52 | There's also https://quantum.country, which 53 | is introductory material in circuit-model quantum computing, by Michael Nielsen 54 | and Andy Matuschak. Nielsen also co-wrote "Mike and Ike", the standard intro to 55 | quantum computing / quantum information textbook. The Quantum Country 56 | site is part of his work on finding alternative ways to teach deep topics. 57 | 58 | Generally, it helped me a lot to use multiple sets of lecture notes online 59 | to get several perspectives on the same material. One point that gets 60 | glossed over: when considering a gate acting on a qubit, you often 61 | need to know the matrix that operates on the entire state vector, not just 62 | the one qubit that the gate is acting on. When this happens, you are 63 | taking tensor products of matrices; the 2x2 identity matrix is used on the 64 | other qubits. This probably isn't at all obvious if you're not used to 65 | working with tensor products and linear maps, especially coming from a 66 | software background. 67 | 68 | ### Notes and caveats 69 | Qoord is a very simple emulator, designed to be easy to understand and 70 | hack on. It's not designed for speed or scale, which are challenging 71 | problems for a quantum emulator because of the exponential growth in 72 | the size of the quantum state vector as the number of qubits 73 | increases. Since quantum computation in Qoord involves repeated matrix 74 | multiplications, the accuracy will be limited by the standard floating point 75 | precision of Python and numpy - we currently don't take any measures to 76 | correct for this. 77 | 78 | Many other circuit emulators are available, but I wrote my own to 79 | really lock in the basics of the theory. I've also used Cirq from Google, 80 | IBM's Qiskit, and PennyLane from Xanadu. They're all great, and they have 81 | different strengths. When I started this project in early 2023, one missing 82 | feature from a lot of the packages out there was an ability to perform mid-circuit 83 | measurements and then continue the circuit - because most of these were written to 84 | control actual hardware, and most of the time you just measure at the end. 85 | I made sure Qoord could do mid-circuit measurements because I needed it for something 86 | else. Good news - in the last year, most of the packages I look at have added this feature. 87 | 88 | #### Pronunciation 89 | Qoord is pronounced like "coordinate". If you say it like "cord" or 90 | "qword", I probably won't notice. My whole family are writers, 91 | but I've mostly fought off the temptation to spell it "qoörd" with a 92 | [diaeresis](https://www.newyorker.com/culture/culture-desk/the-curse-of-the-diaeresis). Although... 93 | the two dots do look a bit entangled, so maybe I'll rethink it. 94 | 95 | 96 | ## Design 97 | ### Core: State Vectors and Matrix Operators 98 | The base layer represents and manipulates program states using vectors of 99 | complex numbers, in the `StateVector` class. Each state is a complex-valued 100 | vector of length $2^n$, where $n$ is the number of qubits in the 101 | system. This layer is primarily implementing mathematical operations. It 102 | doesn't know anything about the physical interpretation as quantum states 103 | and gates. 104 | 105 | The `StateVector` class is immutable, and all operations on it return a 106 | new `StateVector` instance. States can also be represented as a 107 | `DensityMatrix`, an array of complex numbers that captures a broader set 108 | of possibilities where the quantum state is only partially determined. All 109 | `StateVector` instances can be converted to valid `DensityMatrix` 110 | instances, but not vice-versa. 111 | 112 | To change a program's state, we multiply the `StateVector` by a matrix 113 | operator to get a new `StateVector`. These matrix operators are always 114 | either _unitary_ (representing quantum gates) or _projections_ (representing 115 | measurements). You use unitary matrices to change the program state during 116 | a calculation; you use projection matrices to extract data from the program 117 | by reading the value of a qubit. Operators are represented by the 118 | `MatrixOperator` class. 119 | 120 | ### Mantle: Quantum States and Gates 121 | 122 | The `QuantumState` class represents the joint state of a set of 123 | qubits. `QuantumState` contains a collection of qubit identifiers and 124 | either a `StateVector` or a `DensityMatrix` instance to represent the 125 | numeric values of the state. 126 | 127 | When working with multiple qubits, the global state of the system can't be 128 | broken down into a simple combination of the individual qubit states. If 129 | Alice and Bob's qubits are _entangled_, when Alice manipulates her qubit, 130 | the global state of the system changes in a way that matters for Bob's qubit, 131 | even if they are separated by a large distance and can't otherwise 132 | interact. This means that multiple distinct objects need to keep references 133 | to the global `QuantumState`; this violates the normal object/state 134 | encapsulation you want in software, but is a critical part of the quantum 135 | behavior. We handle this by making all _references_ to the global 136 | `QuantumState` immutable, but the `QuantumState` itself is a mutable object 137 | whose value is maintained by either a `StateVector` or a `DensityMatrix`. All 138 | changes in the system involve updating the internal values of the shared 139 | `QuantumState` object. 140 | 141 | When constructing a quantum system, we first fix the number of qubits $n$ 142 | and initialize a `StateVector` to the ${\left|0\right\rangle}^n$ state. The 143 | `StateVector` is used to set up a `QuantumState` instance. Then we 144 | create $n$ `Qubit` instances, passing the `QuantumState` to each constructor 145 | so the state is shared by all the qubits. This reference is immutable, so 146 | qubits cannot lose their connection to the global state object. However, 147 | because the `QuantumState` class has mutable internal state, gates and 148 | measurements on a `Qubit` can change the global state of the system, 149 | and all the qubits still share access to the changed state. 150 | 151 | | Object | Description | 152 | |---|---| 153 | | StateVector, DensityMatrix | immutable wrapper around a numpy array or numeric list. Fundamental operations are just math. | 154 | | QuantumState | mutable container for a state vector or density matrix, with a list of associated qubit identifiers. | 155 | |Qubit, QubitSet | immutable identifier for one or more of the qubits in a quantum system, with an immutable reference to the QuantumState object. | 156 | 157 | 158 | 159 | ### Crust: Devices and Circuits 160 | Users typically will initialize a `Device` instance with some number of 161 | quantum bits. The Device is a container and initializer for the shared 162 | `QuantumState`. 163 | 164 | ```python 165 | device = Device(qubits=2) 166 | device.initialize(StateVector((0, 0, 1, 0))) # |10> 167 | 168 | qubits = device.get_qubits([0, 1]) 169 | CNOT(qubits) 170 | 171 | expected = StateVector((0, 0, 0, 1)) # |11> 172 | actual = device.get_state() 173 | 174 | print(expected) 175 | print(actual) 176 | 177 | # (0, 0, 0, 1) 178 | # (0, 0, 0, 1) 179 | ``` 180 | 181 | ## Usage 182 | 183 | ### A quantum state is a vector of complex numbers. 184 | You can create a quantum state by passing a list of complex numbers 185 | to the `StateVector` class. Here's a state vector representing a 186 | single qubit in the |0> state, and another in the |+> state. 187 | ```python 188 | from qoord import StateVector 189 | sv0 = StateVector([1, 0]) # |0> 190 | sv_plus = StateVector([1, 1]) # |+> 191 | ``` 192 | State vectors are always normalised to have unit length. 193 | If you print out a state vector, you'll see the normalised version. 194 | ```python 195 | print(sv_plus) 196 | # StateVector([0.70710678+0.j, 0.70710678+0.j]) 197 | ``` 198 | 199 | 200 | ### Create a Bell pair on two qubits 201 | ```python 202 | device = Device(qubits=2) 203 | device.make_bell_pair(qubits=[0, 1]) 204 | 205 | qubit = device.get_qubit(0) 206 | state = qubit.get_state(force_density_matrix=True) 207 | 208 | # Use partial trace to reduce to look at just the first qubit 209 | qb0_state = state.partial_trace(keep_qubits=[0]) 210 | print(qb0_state) 211 | # this is a density matrix, not a state vector,because of the 212 | # partial trace operation 213 | 214 | ``` 215 | -------------------------------------------------------------------------------- /examples/SimplestQKD.py: -------------------------------------------------------------------------------- 1 | from qoord import Device, StateVector 2 | from qoord.core_operators import pauli_z 3 | from qoord.states import close_enough 4 | 5 | """ 6 | If Alice and Bob share only a source of entangled qubits, with no 7 | other communication mechanism, they can use the qubits to generate 8 | matching random data. They will each see a random sequence of 1s and -1s, 9 | but their sequences will be exactly the same. 10 | 11 | Quantum Key Distribution uses this property to generate a shared secret 12 | key that can be used without having to transmit the key over any channel. 13 | They share qubits ahead of time, but the classical bit for the shared key 14 | does not exist until either Alice or Bob performs a local measurement. 15 | """ 16 | 17 | 18 | device = Device(qubits=2) 19 | alice_results = [] 20 | bob_results = [] 21 | 22 | n_runs = 1000 23 | 24 | standard_bell_state = StateVector((1, 0, 0, 1)) 25 | standard_check_result = [] 26 | for idx in range(n_runs): 27 | # re-set the device to the |00> state 28 | device.initialize(StateVector((1, 0, 0, 0))) 29 | 30 | # Alice and Bob share a Bell pair, a basic type of quantum entanglement 31 | pair = device.make_bell_pair(qubits=[0, 1]) 32 | 33 | """ 34 | We could be cheating, by generating identical random bits and 35 | sending them to Alice and Bob. But we're not doing that! 36 | """ 37 | 38 | # Check that the underlying state is the standard Bell state 39 | quantum_state = device.get_state() 40 | print(quantum_state) 41 | print(standard_bell_state) 42 | using_standard = close_enough(quantum_state, standard_bell_state) 43 | # default tolerance on this comparison is 1e-15 44 | standard_check_result.append(using_standard) 45 | 46 | """ 47 | Since the generated state is the *same joint state* every time 48 | before we give the qubits to Alice and Bob, we are not just 49 | generating two identical random bits ahead of time and sending 50 | them off. The final bits are not determined until Alice and Bob 51 | perform their measurements. 52 | """ 53 | alice_qubit = pair[0] # Alice receives the first qubit 54 | bob_qubit = pair[1] # Bob receives the second qubit 55 | 56 | # Alice measures her qubit in the standard basis 57 | result = alice_qubit.measure(pauli_z) 58 | alice_results.append(result) # result is either -1 or 1 59 | 60 | # Bob measures his qubit in the standard basis 61 | result = bob_qubit.measure(pauli_z) 62 | bob_results.append(result) # result is either -1 or 1 63 | 64 | # All the trials should have used the standard Bell state 65 | standard_check_pct = 100*sum(standard_check_result)/n_runs 66 | print(f"Standard Bell state used in {standard_check_pct}% of trials.") 67 | 68 | # Alice and Bob should have roughly 50% 1s and -1s 69 | alice_pos = sum([1 for r in alice_results if r == 1]) 70 | bob_pos = sum([1 for r in bob_results if r == 1]) 71 | print(f"Alice: {100*alice_pos/n_runs}% value 1; {100*(1-alice_pos/n_runs)}% value -1") 72 | print(f"Bob: {100*bob_pos/n_runs}% value 1; {100*(1-bob_pos/n_runs)}% value -1") 73 | 74 | # The entanglement in the Bell pair ensures that Alice and Bob's 75 | # results are perfectly correlated and they have the same sequence. 76 | match = sum([1 for a, b in zip(alice_results, bob_results) if a == b]) 77 | print(f"Alice and Bob match on {100*match/n_runs}% of trials") 78 | 79 | 80 | -------------------------------------------------------------------------------- /qoord/__init__.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | import numpy as np 4 | 5 | from qoord.states import * 6 | from qoord.core_gates import * 7 | from qoord.core_operators import * 8 | from qoord.gates import * 9 | from qoord.devices import Device 10 | from qoord.qubits import * 11 | -------------------------------------------------------------------------------- /qoord/__support__.py: -------------------------------------------------------------------------------- 1 | import cmath 2 | import math 3 | 4 | from copy import deepcopy 5 | from numbers import Number 6 | 7 | import numpy as np 8 | 9 | """ 10 | This is a lowest-level module that can safely be 11 | imported by anything else in the package without 12 | creating circular dependencies. 13 | 14 | DO NOT import any other part of qoord into this module. 15 | 16 | """ 17 | 18 | 19 | def tupleize(array): 20 | new_array = [tuple(r) for r in array] 21 | return tuple(new_array) 22 | 23 | 24 | def eipn(n: int) -> complex: 25 | """ 26 | Shorthand for e^(i*pi/n) 27 | @param n: an integer 28 | @return: the math expression above 29 | """ 30 | return cmath.exp(1j*math.pi/n) 31 | 32 | 33 | def close_enough(array1, array2, rel_tol=1e-15): 34 | if isinstance(array1, np.ndarray): 35 | array1 = [x for x in array1.flat] 36 | if isinstance(array2, np.ndarray): 37 | array2 = [x for x in array2.flat] 38 | compare = zip(array1, array2) 39 | are_close = [cmath.isclose(x, y, rel_tol=rel_tol) for x, y in compare] 40 | result = all(are_close) 41 | return result 42 | 43 | 44 | def int_to_binary_list(x: int, size: int) -> list[chr]: 45 | return list(f"{x:0{size}b}") 46 | 47 | 48 | def binary_list_to_int(x: list[chr]) -> int: 49 | return int(''.join(x), base=2) 50 | 51 | 52 | def update_index(x: int, permutation: dict, size: int) -> int: 53 | x = int_to_binary_list(x, size) 54 | new_x = deepcopy(x) 55 | for old_idx, new_idx in permutation.items(): 56 | # Instead of breaking here, do you actually want to assert or raise? 57 | if old_idx >= len(x): 58 | raise ValueError(f"Permutation is wrong length? {len(x)}, {old_idx}") 59 | new_x[new_idx] = x[old_idx] 60 | 61 | new_x = binary_list_to_int(new_x) 62 | return new_x 63 | 64 | 65 | def ndim_zero_ket(n_qubits: int) -> tuple[Number]: 66 | ket = 2**n_qubits * [0] 67 | ket[0] = 1 68 | return ket 69 | 70 | 71 | def is_square(array): 72 | for row in array: 73 | if len(row) != len(array): 74 | return False 75 | 76 | return len(array) > 0 77 | 78 | 79 | def closest(val, items, tolerance=1e-15): 80 | """ 81 | Sometimes floating-point issues cause a measured value to 82 | be slightly different from the expected value. If we have a 83 | set of known target values, this picks the closest one, 84 | within some tolerance 85 | @param val: the candidate value that should be in the list of items 86 | @param items: allowed set of values 87 | @param tolerance: maximum allowed discrepancy 88 | @return: the entry from items that is closest, within the tolerance 89 | """ 90 | gap = np.inf 91 | best_val = None 92 | for item in items: 93 | delta = item - val 94 | d2 = delta * np.conj(delta) # could be complex-valued 95 | if d2 > tolerance: 96 | continue 97 | if d2 < gap: 98 | gap = d2 99 | best_val = item 100 | 101 | if best_val is None: 102 | msg = f"Could not find match for {val} within tolerance {tolerance}" 103 | raise ValueError(msg) 104 | return best_val 105 | -------------------------------------------------------------------------------- /qoord/core_gates.py: -------------------------------------------------------------------------------- 1 | from qoord.states import identity_op 2 | from qoord.gates import UnitaryGate 3 | from qoord.core_operators import * 4 | 5 | Identity = UnitaryGate(identity_op, 'I') 6 | PauliX = UnitaryGate(pauli_x, 'X') 7 | PauliY = UnitaryGate(pauli_y, 'Y') 8 | PauliZ = UnitaryGate(pauli_z, 'Z') 9 | Hadamard = UnitaryGate(hadamard_op, 'H') 10 | S = UnitaryGate(phase_op, 'S') 11 | T = UnitaryGate(pi_over_8_gate_op, 'T') 12 | CNOT = UnitaryGate(cnot_op, 'CNOT') 13 | CCNOT = UnitaryGate(ccnot_op, 'CCNOT') -------------------------------------------------------------------------------- /qoord/core_operators.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from qoord.__support__ import eipn 4 | from qoord.states import MatrixOperator 5 | 6 | pauli_x = MatrixOperator(((0, 1), 7 | (1, 0))) 8 | 9 | pauli_y = MatrixOperator(((0, -1j), 10 | (1j, 0))) 11 | 12 | pauli_z = MatrixOperator(((1, 0), 13 | (0, -1))) 14 | 15 | c = math.sqrt(1.0/2.0) 16 | hadamard_op = MatrixOperator(((c, c), 17 | (c, -c))) 18 | 19 | phase_op = MatrixOperator(((1, 0), 20 | (0, 1j))) 21 | 22 | pi_over_8_gate_op = MatrixOperator(((1, 0), 23 | (0, eipn(4)))) 24 | 25 | cnot_op = MatrixOperator(((1, 0, 0, 0), 26 | (0, 1, 0, 0), 27 | (0, 0, 0, 1), 28 | (0, 0, 1, 0))) 29 | 30 | ccnot_op = MatrixOperator(((1, 0, 0, 0, 0, 0, 0, 0), 31 | (0, 1, 0, 0, 0, 0, 0, 0), 32 | (0, 0, 1, 0, 0, 0, 0, 0), 33 | (0, 0, 0, 1, 0, 0, 0, 0), 34 | (0, 0, 0, 0, 1, 0, 0, 0), 35 | (0, 0, 0, 0, 0, 1, 0, 0), 36 | (0, 0, 0, 0, 0, 0, 0, 1), 37 | (0, 0, 0, 0, 0, 0, 1, 0))) 38 | -------------------------------------------------------------------------------- /qoord/devices.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | from typing import Hashable, Optional 4 | 5 | from qoord.__support__ import ndim_zero_ket 6 | from qoord.qubits import Qubit, QubitSet 7 | from qoord.core_gates import Hadamard, CNOT 8 | from qoord.states import DensityMatrix, StateVector, QuantumState 9 | 10 | 11 | class Device(object): 12 | """ 13 | A device enables *local* manipulation of a subset 14 | of the available qubits in the global system. 15 | 16 | An n-qubit device defaults to an initial state of |0>^n. 17 | """ 18 | def __init__(self, qubits: int | list[str]): 19 | """ 20 | Initialize a device with a number of qubits. 21 | @param qubits: number of qubits, or list of qubit labels 22 | """ 23 | # assign unique identifiers to the qubits 24 | if isinstance(qubits, int): 25 | self._num_qubits = qubits 26 | self._qubit_ids = [_ for _ in range(qubits)] 27 | else: 28 | self._num_qubits = len(qubits) 29 | unique_ids = set(qubits) 30 | if len(unique_ids) != len(qubits): 31 | raise ValueError('qubit ids are not unique! {}'.format(unique_ids)) 32 | self._qubit_ids = qubits 33 | 34 | default_init_state = ndim_zero_ket(self._num_qubits) 35 | state = QuantumState(self._qubit_ids) 36 | state.set_value(StateVector(default_init_state)) 37 | self._quantum_state = state 38 | self._original_qubit_ids = copy.deepcopy(self._qubit_ids) 39 | 40 | def initialize(self, state: StateVector | DensityMatrix): 41 | """ 42 | This DOES NOT preserve the input state vector, it copies 43 | the contents into the shared internal state of the device, 44 | which are also held by the qubits, OVERWRITING any previous 45 | state of the device and its qubits. 46 | """ 47 | state = copy.deepcopy(state) 48 | self._quantum_state.set_value(state) 49 | 50 | def get_state(self): 51 | inner_state = self._quantum_state.get_value() 52 | return copy.deepcopy(inner_state) 53 | 54 | def get_qubit(self, key: Hashable) -> Qubit: 55 | """ 56 | Get a qubit from the device using its label. 57 | :param key: the qubit label 58 | :return: Qubit instance, an immutable handle to (part of) the quantum state 59 | """ 60 | state = self._quantum_state 61 | result = Qubit(state, label=key) 62 | return result 63 | 64 | def get_qubits(self, keys: Optional[list[Hashable]] = None) -> QubitSet: 65 | """ 66 | Get one or more qubits from the device using their labels. 67 | :param keys: one or more qubit labels 68 | :return: QubitSet, even if only one qubit is requested 69 | """ 70 | if keys is None: 71 | keys = self._qubit_ids 72 | elif isinstance(keys, Hashable): 73 | keys = [keys] # because it's a singleton 74 | 75 | state = self._quantum_state 76 | 77 | qubits = [Qubit(state, label=k) for k in keys] 78 | results = QubitSet(qubits) 79 | return results 80 | 81 | def make_bell_pair(self, qubits): 82 | assert len(qubits) == 2 83 | q1, q2 = self.get_qubits(qubits) 84 | Hadamard(q1) # create superposition 85 | CNOT(q1, q2) # entangle first shared pair 86 | return QubitSet([q1, q2]) 87 | 88 | -------------------------------------------------------------------------------- /qoord/gates.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from qoord.states import MatrixArray, MatrixOperator 4 | from qoord.qubits import Qubit, QubitSet 5 | 6 | TmpUG = TypeVar("TmpUG", bound="UnitaryGate") 7 | 8 | 9 | class UnitaryGate: 10 | def __init__(self, matrix: MatrixArray | MatrixOperator, name=None): 11 | 12 | if isinstance(matrix, MatrixOperator): 13 | self._matrix = matrix 14 | else: 15 | self._matrix = MatrixOperator(matrix) 16 | 17 | if not self._matrix.is_unitary(): 18 | raise ValueError(f"Input operator {matrix} is not unitary.") 19 | 20 | self._name = name 21 | 22 | def __repr__(self): 23 | return f'{self._name}' 24 | 25 | def __call__(self, qubit: Qubit, *qubits: Qubit): 26 | """ 27 | The gate call with the qubit argument is implemented using the 28 | qubit.apply(matrix operator) method. 29 | 30 | @param qubit: 31 | @param qubits: 32 | @return: 33 | """ 34 | if qubits: 35 | qubits = [qubit] + [x for x in qubits] 36 | qubits = QubitSet(qubits) 37 | else: 38 | qubits = qubit 39 | qubits.apply(self._matrix) 40 | 41 | def __pow__(self, exponent): 42 | new_matrix = self._matrix.__pow__(exponent) 43 | return UnitaryGate(new_matrix, f'{self}^{exponent}') 44 | 45 | def __neg__(self): 46 | new_matrix = -self._matrix 47 | return UnitaryGate(new_matrix, f'-{self}') 48 | 49 | def to_operator(self): 50 | return self._matrix 51 | 52 | def tensor(self, other: TmpUG) -> TmpUG: 53 | op1 = self.to_operator() 54 | op2 = other.to_operator() 55 | new_op = op1.tensor(op2) 56 | return UnitaryGate(matrix=new_op, name=f'{self}x{other}') 57 | 58 | def dim(self): 59 | return self._matrix.dim() -------------------------------------------------------------------------------- /qoord/qubits.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from qoord.__support__ import closest 4 | from qoord.states import MatrixOperator, QuantumState 5 | 6 | 7 | class Qubit(object): 8 | """ 9 | This is the basic resource that can be used for computing 10 | and coordination. The global state is shared among several 11 | qubits, and the label tracks which tensor component corresponds 12 | to this qubit. 13 | 14 | A Qubit acts like a handle to a particular part of the state, with 15 | a label for that part. There is no other internal state besides 16 | the global quantum_state. If two qubits have the same label and 17 | share the same quantum state, there's no problem with that. 18 | 19 | FIXME: currently unsolved: what happens if you create two Device objects, 20 | then use a binary gate operation like CNOT to entangle two qubits from 21 | different devices? This needs to create a joint quantum state object 22 | but the devices would only have access to some of the state. 23 | 24 | """ 25 | def __init__(self, state: QuantumState, label: int | str = 0): 26 | self._quantum_state = state 27 | self._state_index = label 28 | 29 | def get_state(self, force_density_matrix=False): 30 | inner_state = self._quantum_state.get_value(force_density_matrix) 31 | return deepcopy(inner_state) 32 | 33 | def apply(self, operator: MatrixOperator) -> None: 34 | state = self._quantum_state 35 | state.apply(operator, [self._state_index]) 36 | 37 | def measure(self, observable: MatrixOperator): 38 | eigenvalue = self._quantum_state.measure(observable, self._state_index) 39 | return eigenvalue 40 | 41 | 42 | class QubitSet(object): 43 | 44 | def __init__(self, qubits: list[Qubit], state: QuantumState = None): 45 | if len(qubits) == 0: 46 | self._global_state = state # weird, but okay? 47 | else: 48 | q1 = qubits[0] 49 | self._global_state = q1._quantum_state 50 | self._state_indexes = [q._state_index for q in qubits] 51 | 52 | def __getitem__(self, item): 53 | if isinstance(item, slice): 54 | qb_list = [Qubit(self._global_state, idx) for idx in self._state_indexes[item]] 55 | return QubitSet(qb_list, self._global_state) 56 | else: 57 | return Qubit(self._global_state, self._state_indexes[item]) 58 | 59 | def apply(self, operator: MatrixOperator) -> None: 60 | state = self._global_state 61 | state.apply(operator, self._state_indexes) 62 | 63 | def measure(self, observable: MatrixOperator): 64 | eigenvalue = self._global_state.measure(observable, on_qubits=self._state_indexes) 65 | return eigenvalue 66 | 67 | def __iter__(self): 68 | for idx in self._state_indexes: 69 | yield Qubit(self._global_state, idx) 70 | 71 | def __len__(self): 72 | return len(self._state_indexes) 73 | 74 | 75 | def binary_from_measurement(observable, qubit): 76 | """ 77 | Map the two eigenvalues of the observable to binary values 0, 1, 78 | then measure the provided qubit to get a binary result. This is 79 | helpful when interpreting measurements as choices or actions. 80 | 81 | @param observable: essentially, what basis we choose to measure in 82 | @param qubit: which qubit we are measuring 83 | @return: 0 if we measure the first eigenvalue, 1 if the second 84 | """ 85 | op = observable.to_operator() 86 | values, vectors = op.eig() 87 | actions = {v: a for v, a in zip(values, (0, 1))} 88 | measurement = qubit.measure(op) 89 | measurement_corrected = closest(measurement, values) 90 | chosen_action = actions[measurement_corrected] 91 | return chosen_action 92 | -------------------------------------------------------------------------------- /qoord/states.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import math 3 | import random 4 | import typing 5 | 6 | import numpy as np 7 | 8 | from numbers import Number 9 | from typing import TypeAlias, TypeVar 10 | 11 | from qoord.__support__ import ndim_zero_ket, update_index, tupleize 12 | from qoord.__support__ import close_enough as support_close 13 | 14 | # row and column vectors are both supported 15 | RowVector: TypeAlias = tuple[Number, ...] 16 | ColumnVector: TypeAlias = tuple[tuple[Number], ...] 17 | VectorArray: TypeAlias = RowVector | ColumnVector 18 | MatrixArray: TypeAlias = tuple[RowVector, ...] 19 | 20 | # TmpMO exists because "Self"-typing isn't available until Python 3.11 21 | TmpSV = TypeVar("TmpSV", bound="StateVector") 22 | TmpMO = TypeVar("TmpMO", bound="MatrixOperator") 23 | TmpDM = TypeVar("TmpDM", bound="DensityMatrix") 24 | 25 | 26 | # data like [1, 2, 3] 27 | def _flat(data): 28 | return all([isinstance(x, Number) for x in data]) 29 | 30 | 31 | # data like [[1, 2, 3]] 32 | def _np_row(data): 33 | one_row = len(data) == 1 34 | tuple_or_list = isinstance(data[0], tuple) or isinstance(data[0], list) 35 | numeric = all([isinstance(x, Number) for x in data[0]]) 36 | return one_row and tuple_or_list and numeric 37 | 38 | 39 | # data like [[1], [2], [3]] 40 | def _np_column(data): 41 | return all([len(x) == 1 and isinstance(x[0], Number) for x in data]) 42 | 43 | 44 | class StateVector(object): 45 | 46 | @classmethod 47 | def _normalize(cls, array): 48 | # need to make sure array norms to 1 49 | #norm = math.sqrt(sum([x * np.conj(x) for x in array])) 50 | norm = math.sqrt(sum([np.abs(x)**2 for x in array])) 51 | if norm != 1: 52 | normed_array = [x / norm for x in array] 53 | else: 54 | normed_array = array 55 | 56 | return tuple(normed_array) 57 | 58 | def __init__(self, array: VectorArray, as_ket=True): 59 | """ 60 | Contract: self._array is always a list, 61 | and we only use numpy arrays internally to assist 62 | with calculations or as a return value when that's 63 | clearly expected from the API. 64 | 65 | The StateVector is immutable and shareable, but the 66 | input array shouldn't be. It is a numeric array, so 67 | we don't need to deepcopy. 68 | 69 | @param array: the elements of the vector, as tuple or list 70 | @param as_ket: flag for column (ket) vector or row (bra) vector; default True 71 | """ 72 | if array is None: # does type hinting help prevent this? 73 | raise ValueError("Cannot make StateVector with no array input.") 74 | 75 | if not isinstance(array, list) and not isinstance(array, tuple): 76 | raise ValueError("Only support list and tuple as input.") 77 | 78 | if _flat(array): 79 | self._array = self._normalize(array) 80 | self._as_ket = as_ket 81 | else: 82 | vtype, array = self._vector_type(array) 83 | self._as_ket = vtype == 'column' 84 | self._array = self._normalize(array) 85 | 86 | @classmethod 87 | def _vector_type(cls, data): 88 | # assume the data is already a list or tuple 89 | if _flat(data): 90 | vtype = 'row' 91 | flat_data = data 92 | elif _np_row(data): 93 | vtype = 'row' 94 | flat_data = data[0] 95 | elif _np_column(data): 96 | vtype = 'column' 97 | flat_data = [x[0] for x in data] 98 | else: 99 | raise ValueError("Data is the wrong shape!") 100 | 101 | return vtype, flat_data 102 | 103 | def to_array(self): 104 | return [x for x in self._array] 105 | 106 | def to_numpy_array(self): 107 | if self._as_ket: 108 | vtype = 'c' # kets are column vectors 109 | else: 110 | vtype = 'r' # bras are row vectors 111 | return np.r_[vtype, self._array].A 112 | 113 | def __repr__(self): 114 | return str(self._array) # we just care about the contents 115 | 116 | def __eq__(self, other: TmpSV): 117 | if not isinstance(other, StateVector): 118 | return False 119 | return np.array_equal(self._array, other._array) 120 | 121 | def __hash__(self): 122 | return hash(self._array) 123 | 124 | def __len__(self): 125 | return len(self._array) 126 | 127 | def __matmul__(self, other): 128 | if isinstance(other, StateVector): 129 | return self.dot(other) 130 | elif isinstance(other, MatrixOperator): 131 | self_array = self.to_numpy_array() 132 | other_array = other.to_array(as_numpy=True) 133 | new_value = self_array @ other_array 134 | return StateVector(new_value.tolist()) 135 | elif isinstance(other, np.ndarray): 136 | self_array = self.to_numpy_array() 137 | new_value = self_array @ other 138 | return StateVector(new_value.tolist()) 139 | 140 | def qubit_count(self): 141 | return int(math.log(len(self), 2)) 142 | 143 | def tensor(self, other: TmpSV | TmpMO) -> TmpSV | TmpMO: 144 | self_array = self.to_numpy_array() 145 | other_array = other.to_numpy_array() 146 | array_out = np.kron(self_array, other_array) 147 | if 1 in array_out.shape: 148 | result = StateVector(array_out.tolist()) 149 | else: 150 | result = MatrixOperator(array_out) 151 | return result 152 | 153 | def outer(self, other: TmpSV) -> TmpMO: 154 | content = np.outer(self.to_numpy_array(), other.to_numpy_array()) 155 | return MatrixOperator(content) 156 | 157 | def adjoint(self): 158 | new_array = self.to_numpy_array().conj().T 159 | return StateVector(new_array.tolist()) 160 | 161 | def dot(self, other: TmpSV) -> Number: 162 | other_parts = other.to_array() 163 | self_parts = self.to_array() 164 | parts = zip(self_parts, other_parts) 165 | parts = [x * y for x, y in parts] 166 | result = sum(parts) 167 | return result 168 | 169 | def to_density_matrix(self) -> TmpDM: 170 | content = self.outer(self) 171 | return DensityMatrix(content.to_array()) 172 | 173 | def rearrange(self, tensor_permutation): 174 | new_inner = rearrange_vector(tensor_permutation, self._array) 175 | return StateVector(new_inner) 176 | 177 | 178 | ZERO = StateVector([1, 0]) # the zero ket |0> gives the 0 bit-value 179 | ONE = StateVector([0, 1]) # the one ket |0> gives the 1 bit-value 180 | 181 | 182 | def close_enough(s1: StateVector, s2: StateVector, rel_tol=1e-15): 183 | a1 = s1.to_numpy_array() 184 | a2 = s2.to_numpy_array() 185 | return support_close(a1, a2, rel_tol) 186 | 187 | 188 | class MatrixOperator(object): 189 | @classmethod 190 | def lift(cls, function: typing.Callable) -> typing.Callable: 191 | """ 192 | Convert any one-variable function on complex numbers into 193 | a compatible function on normal operators. Do this by 194 | diagonalizing using the eigenvector structure, applying 195 | the function to the eigenvalues, then building the new 196 | operator using a basis of outer products of the eigenvectors. 197 | @param function: 1-variable complex to complex (not checked!) 198 | @return: new MatrixOperator 199 | """ 200 | def new_func(mo: MatrixOperator) -> MatrixOperator: 201 | values, vectors = mo.eig() 202 | basis = [np.outer(v, v) for v in vectors] 203 | new_vals = [function(val) for val in values] 204 | matrix_new = sum([val * vec for val, vec in zip(new_vals, basis)]) 205 | op_new = MatrixOperator(matrix_new) 206 | return op_new 207 | return new_func 208 | 209 | def __init__(self, components: MatrixArray): 210 | # FIXME: should we do the same thing as SV and DM 211 | # and coerce this to a list of lists? 212 | self._array = np.array(components) 213 | self._projectors = None 214 | 215 | def __eq__(self, other: TmpMO): 216 | self_array = self.to_array() 217 | other_array = other.to_array() 218 | return np.array_equal(self_array, other_array) 219 | 220 | def __neg__(self): 221 | new_array = -self.to_array(True) 222 | return MatrixOperator(new_array) 223 | 224 | def __add__(self, other: TmpMO): 225 | shape1 = self.shape() 226 | shape2 = other.shape() 227 | if shape1 != shape2: 228 | msg = f"Wrong shape for '+'! Self: {shape1}; other: {shape2}" 229 | raise ValueError(msg) 230 | new_array = self.to_array(True) + other.to_array(True) 231 | return MatrixOperator(new_array) 232 | 233 | def __matmul__(self, other: TmpMO | StateVector | np.array): 234 | if isinstance(other, MatrixOperator) or isinstance(other, DensityMatrix): 235 | self_array = self.to_array(as_numpy=True) 236 | other_array = other.to_array(as_numpy=True) 237 | new_array = self_array @ other_array 238 | return MatrixOperator(components=new_array) 239 | elif isinstance(other, StateVector): # StateVector 240 | return self.apply_to_vector(other) 241 | elif isinstance(other, np.ndarray): 242 | self_array = self.to_array(as_numpy=True) 243 | new_array = self_array @ other 244 | if 1 not in other.shape: # not a vector 245 | return MatrixOperator(new_array) # this choice seems dodgy as fuck 246 | else: 247 | return new_array 248 | 249 | def apply_to_vector(self, vector: StateVector): 250 | vector_array = vector.to_numpy_array() 251 | new_vector = self.to_array(as_numpy=True) @ vector_array 252 | return StateVector(array=new_vector.tolist()) 253 | 254 | def __pow__(self, exponent): 255 | def cpower(a, b): 256 | a = complex(a) 257 | b = complex(b) 258 | return a**b 259 | power_func = self.lift(lambda x: cpower(x, exponent)) # does this get slow? 260 | return power_func(self) 261 | 262 | def tensor(self, other: TmpMO | TmpSV): 263 | if isinstance(other, StateVector): 264 | self_array = self.to_array(as_numpy=True) 265 | other_array = other.to_numpy_array() 266 | array_out = np.kron(self_array, other_array) 267 | else: 268 | array_out = np.kron(self._array, other._array) 269 | return MatrixOperator(components=array_out) 270 | 271 | def tensor_power(self, num: int) -> TmpMO: 272 | result = self 273 | for _ in range(num - 1): 274 | result = result.tensor(self) 275 | return result 276 | 277 | def adjoint(self): 278 | new_array = self.to_array(as_numpy=True).conj().T 279 | return MatrixOperator(new_array) 280 | 281 | def to_array(self, as_numpy=False): 282 | if as_numpy: 283 | return self._array.copy() 284 | else: 285 | return tupleize(self._array.tolist()) 286 | 287 | def to_numpy_array(self): 288 | return self.to_array(True) 289 | 290 | def is_unitary(self): 291 | m = self._array 292 | return np.allclose(np.eye(len(m)), m.dot(m.T.conj())) 293 | 294 | def dim(self): 295 | """ 296 | The size of the operator as a square array 297 | @return: 298 | """ 299 | return self._array.shape[0] 300 | 301 | def shape(self): 302 | return self._array.shape 303 | 304 | def qubit_count(self): 305 | return int(math.log(self.dim(), 2)) 306 | 307 | def eig(self) -> tuple[list[Number], list[VectorArray]]: 308 | """ 309 | Get the eigenvalue, eigenvector structure of the operator. 310 | Differs from np.linalg.eig by returning eigenvectors as 311 | a list, rather than a numpy array, for clarity (numpy "eig" 312 | chooses a surprising order for presenting the vectors). 313 | 314 | @return: values, vectors 315 | - values: List[numeric] 316 | - vectors: List[numpy vectors] 317 | """ 318 | values, vectors = np.linalg.eig(self._array) # maybe cache this? 319 | n_dims = len(values) 320 | vectors = [vectors[:, i] for i in range(n_dims)] 321 | """ 322 | WARNING: "vectors.tolist()" does not work in the above line 323 | because the matrix of eigenvectors from .eig() has an unexpected 324 | orientation: you will get a meaningless set of vectors! 325 | """ 326 | 327 | return values, vectors 328 | 329 | def eigenvalues(self): 330 | values, _ = self.eig() 331 | return values 332 | 333 | def eigenvectors(self): 334 | _, vectors = self.eig() 335 | return vectors 336 | 337 | def get_eigenvector(self, eigenvalue): 338 | values, vectors = self.eig() 339 | values = values.tolist() 340 | idx = values.index(eigenvalue) 341 | return vectors[idx] 342 | 343 | def projectors(self): 344 | """ 345 | Get the projection operators for the eigenvalues 346 | of this operator. 347 | 348 | @return: dictionary mapping eigenvalue to subspace projector 349 | """ 350 | if not self._projectors: 351 | values, states = self.eig() 352 | projector_map = {} 353 | for v, s in zip(values, states): 354 | if v not in projector_map: 355 | projector_map[v] = 0 356 | projector_map[v] += np.outer(s, s) 357 | 358 | self._projectors = {v: MatrixOperator(p) for v, p in projector_map.items()} 359 | 360 | return self._projectors 361 | 362 | def distribution(self, state: StateVector | TmpMO): 363 | """ 364 | Compute the distribution of the eigenvalues of this 365 | operator when measuring on a given state. 366 | @param state: quantum state to measure 367 | @return: 368 | options - the eigenvalue-projector map (dict) 369 | probabilities - eigenvalue-probability map (dict) 370 | """ 371 | saj = state.adjoint().to_numpy_array() 372 | state = state.to_numpy_array() 373 | options = self.projectors() 374 | probabilities = {} 375 | for val, proj in options.items(): 376 | proj = proj.to_numpy_array() 377 | prob = (saj @ proj @ state).item() 378 | probabilities[val] = prob.real 379 | 380 | return options, probabilities 381 | 382 | def measure(self, state: StateVector | TmpMO, extra_data=False) \ 383 | -> float | tuple[float, TmpSV, float]: 384 | options, probabilities = self.distribution(state) 385 | eigenvalues = list(options.keys()) 386 | sampler = [probabilities[v] for v in eigenvalues] 387 | n_choices = len(options) 388 | select = random.choices(range(n_choices), sampler, k=1) 389 | result_idx = select[0] 390 | 391 | value = eigenvalues[result_idx] 392 | projector = options[value].to_numpy_array() 393 | sample_probability = sampler[result_idx] 394 | post_measurement_vector = (projector @ state.to_numpy_array())/math.sqrt(sample_probability) 395 | post_measurement_vector = StateVector(post_measurement_vector.tolist()) 396 | p = sampler[result_idx] 397 | 398 | if extra_data: 399 | return value, post_measurement_vector, p 400 | else: 401 | return value 402 | 403 | def __repr__(self): 404 | return str(self._array) 405 | 406 | def expand_with_identities(self, num_dims: int): 407 | """ 408 | The point of this function is to elevate this operator to a 409 | higher-dimensional space by adding dummy dimensions. This 410 | helps us operate on one part of the total quantum state of 411 | the system while leaving the rest unchanged. 412 | 413 | Return a new operator on k+n dimensions, where k is the number 414 | of added dimensions, and n is dim(self). The new operator 415 | is a tensor product of k Identity operators followed by the 416 | current operator, so the active dimensions are always at the end. 417 | 418 | @param num_dims: number of new "identity" dimensions to add. 419 | @return: a new operator on num_dims + self.dim() dimensions 420 | """ 421 | op_parts = [] 422 | for _ in range(num_dims): 423 | op_parts.append(identity_op) 424 | op_parts.append(self) # current operator is last 425 | new_op = op_parts[0] 426 | for op in op_parts[1:]: 427 | new_op = new_op.tensor(op) 428 | return new_op 429 | 430 | 431 | identity_op = MatrixOperator([[1, 0], 432 | [0, 1]]) 433 | 434 | 435 | class DensityMatrix(MatrixOperator): 436 | def __init__(self, array: MatrixArray | VectorArray | MatrixOperator): 437 | """ 438 | Contract: self._array is always a (nested?) list, 439 | and we only use np.arrays to assist with calculations 440 | or as a return value when that's clearly expected 441 | from the API. 442 | """ 443 | if isinstance(array, typing.Sequence) and \ 444 | not isinstance(array[0], typing.Sequence): 445 | # this is a vector input, so we want to cast it to a square array 446 | array = np.outer(array, array).tolist() 447 | elif isinstance(array, MatrixOperator): 448 | array = array.to_numpy_array() 449 | super().__init__(array) 450 | 451 | def __eq__(self, other: TmpDM): 452 | return np.array_equal(self._array, other._array) 453 | 454 | def __add__(self, other: TmpDM): 455 | content = self.to_array(True) + other.to_array(True) 456 | return DensityMatrix(content) 457 | 458 | def scale(self, scale_factor): 459 | content = self.to_array(True) * scale_factor 460 | return DensityMatrix(content) 461 | 462 | def partial_trace(self, keep_qubits: list) -> TmpDM: 463 | """ 464 | Compute a density operator that applies to a subset 465 | of the qubits. The partial_trace operation effectively 466 | removes the influence of the other qubits by swapping 467 | in the expectation value of measuring them (I think). 468 | The result is the density operator for the qubits 469 | you want to keep. 470 | 471 | @param keep_qubits: 472 | @return: 473 | """ 474 | 475 | qubit_ids = {x for x in range(self.qubit_count())} 476 | keep_qubits = set(keep_qubits) 477 | drop_qubits = qubit_ids.difference(keep_qubits) 478 | 479 | # we need to change to a basis of the form a_i * b_j where 480 | # the a_i represent qubits to trace out and b_j represent things to keep 481 | perm = permute_to_end(move_these=keep_qubits, total_set=qubit_ids) 482 | self_array = rearrange_matrix(perm, self.to_array(False)) 483 | self_array = DensityMatrix(self_array).to_array(True) 484 | 485 | # To keep subsystem B (keep_qubits), and trace out 486 | # subsystem A (drop_qubits), we need a basis for the A-set, 487 | # and the identity on the B-set. 488 | keep_size, drop_size = 2**len(keep_qubits), 2**len(drop_qubits) 489 | drop_basis = np.identity(drop_size) 490 | drop_basis = [drop_basis[:, i] for i in range(drop_size)] 491 | keep_identity = identity_op.tensor_power(len(keep_qubits)) 492 | 493 | result = None 494 | 495 | for j in drop_basis: 496 | j = StateVector(j.tolist()) 497 | jT = j.adjoint() 498 | left_side = jT.tensor(keep_identity) 499 | right_side = j.tensor(keep_identity) 500 | step_result = left_side @ self_array # here is where we use the permuted form 501 | step_result = step_result @ right_side 502 | if result is None: 503 | result = step_result 504 | else: 505 | result += step_result 506 | 507 | little_perm = numeric_list_to_permutation(keep_qubits) 508 | undo_little_perm = invert_permutation(little_perm) 509 | 510 | result = rearrange_matrix(undo_little_perm, result.to_array(False)) 511 | result = DensityMatrix(result) 512 | return result 513 | 514 | 515 | def rearrange_vector(tensor_permutation: dict, state_vector: list | tuple, size=None): 516 | if not size: 517 | size = len(state_vector).bit_length() - 1 518 | # this will probably be wrong if the vector is not 2^n length, 519 | # but it doesn't matter here because we're working on quantum states 520 | 521 | new_state = list(state_vector) 522 | 523 | for idx in range(len(new_state)): 524 | new_idx = update_index(idx, tensor_permutation, size) 525 | new_state[new_idx] = state_vector[idx] 526 | 527 | return new_state 528 | 529 | 530 | def rearrange_matrix(tensor_permutation: dict, matrix: MatrixArray): 531 | new_matrix = [[x for x in row] for row in matrix] 532 | size = len(new_matrix).bit_length() - 1 533 | n = len(new_matrix) 534 | for row in range(n): 535 | new_row = update_index(row, tensor_permutation, size) 536 | for col in range(n): 537 | new_col = update_index(col, tensor_permutation, size) 538 | new_matrix[new_row][new_col] = matrix[row][col] 539 | 540 | return new_matrix 541 | 542 | 543 | def permute_to_end(move_these: list, total_set: list) -> dict: 544 | """ 545 | Given a sublist of qubit labels, pop and move to the end of the list. 546 | These are values, not indices, so we match on the value to find 547 | which index to pop. That lets us work with qubits accessed by 548 | addresses or names more complex than a list-position. 549 | 550 | @param move_these: qubit labels to move to end 551 | @param total_set: all the qubit labels as a unique list 552 | @return: a permutation dictionary 553 | """ 554 | if not isinstance(total_set, list): 555 | total_set = list(total_set) 556 | for q_val in move_these: 557 | qi = total_set.index(q_val) 558 | total_set.pop(qi) # take q_val out 559 | total_set.append(q_val) # put q_val back 560 | 561 | perm = {v: idx for idx, v in enumerate(total_set)} 562 | return perm 563 | 564 | 565 | def invert_permutation(permutation: dict) -> dict: 566 | """ 567 | Reverse a permutation dictionary. The permutation dictionary 568 | acts on a list by manipulating its indices. 569 | @param permutation: a dictionary of from-index: to-index 570 | @return: 571 | """ 572 | new_perm = {v: k for k, v in permutation.items()} 573 | return new_perm 574 | 575 | 576 | def numeric_list_to_permutation(a_list: list) -> dict: 577 | """ 578 | Take a numeric list and convert it to a permutation dictionary. 579 | For each value, map its current index to the index it would have 580 | in a sorted version of the list. 581 | 582 | @param a_list: unique list of numeric (or o/w sortable) values 583 | @return: a permutation dictionary that would sort the list 584 | """ 585 | sorted_list = sorted(a_list) 586 | reverse_map = {} 587 | perm = {} 588 | for idx, v, in enumerate(sorted_list): 589 | reverse_map[v] = idx 590 | 591 | for idx, v in enumerate(a_list): 592 | perm[idx] = reverse_map[v] 593 | 594 | return perm 595 | 596 | 597 | class QuantumState(object): 598 | 599 | def __init__(self, qubit_ids): 600 | self.qubit_ids = qubit_ids 601 | z = ndim_zero_ket(len(qubit_ids)) 602 | self._state_vector = z 603 | self._density_matrix = None 604 | 605 | def __repr__(self): 606 | if self._density_matrix is None: 607 | return str(self._state_vector) 608 | else: 609 | return str(self._density_matrix) 610 | 611 | def size(self) -> int: 612 | return 2**self.qubit_count() 613 | 614 | def qubit_count(self): 615 | return len(self.qubit_ids) 616 | 617 | def get_value(self, force_density_matrix=False) -> TmpDM | TmpSV: 618 | if self._density_matrix is None: 619 | value = self._state_vector 620 | if force_density_matrix: 621 | value = value.to_density_matrix() 622 | else: 623 | value = self._density_matrix 624 | return value 625 | 626 | def set_value(self, state: TmpSV | TmpDM) -> None: 627 | if len(state) != self.size(): 628 | raise ValueError(f"Input state must be of size {self.size()}") 629 | 630 | if isinstance(state, StateVector): 631 | self._state_vector = state 632 | self._density_matrix = None 633 | elif isinstance(state, DensityMatrix): 634 | self._state_vector = None 635 | self._density_matrix = state 636 | 637 | @contextlib.contextmanager 638 | def _align_to_end(self, operator: TmpMO, on_qubits: list): 639 | """ 640 | tensor an operator with a bunch of identities so it operates 641 | on the tail of the internal qubit list; permute the global state 642 | to put the key qubits at the end, in the order listed; then 643 | yield to whatever needs to be done; finally permute the state back 644 | 645 | NOTE: nothing inside this function uses the original labels 646 | for the on_qubits entries - those would have to be re-indexed 647 | to account for the permutation before they could be used in any 648 | matrix calculations. See DensityMatrix.partial_trace for an 649 | example that needs to do this, therefore can't use this context 650 | manager. 651 | """ 652 | # 1) tensor the operator with a bunch of identities 653 | gate_qubits = operator.dim().bit_length() - 1 654 | n_qubits = self.qubit_count() 655 | n_identities = n_qubits - gate_qubits 656 | new_op = operator.expand_with_identities(n_identities) 657 | 658 | # permute the global state to put the key qubits at the end 659 | perm = permute_to_end(move_these=on_qubits, total_set=self.qubit_ids) 660 | 661 | v = self.get_value() # is the copy needed? 662 | v = v.rearrange(tensor_permutation=perm) 663 | self.set_value(v) 664 | 665 | yield new_op, perm 666 | 667 | # have to re-fetch the state because the external context can modify it 668 | # maybe this is a case for using yield-from? 669 | 670 | v = self.get_value() 671 | 672 | # permute the state back 673 | inv_perm = invert_permutation(perm) 674 | v = v.rearrange(inv_perm) 675 | self.set_value(v) 676 | 677 | def apply(self, operator: TmpMO, on_qubits: list) -> None: 678 | with self._align_to_end(operator, on_qubits) as context: 679 | # apply the full state operator 680 | new_op, perm = context 681 | v = self.get_value() 682 | v = new_op @ v 683 | self.set_value(v) 684 | 685 | def measure(self, observable: MatrixOperator, on_qubits: tuple | list | None = None): 686 | if on_qubits is None: 687 | if observable.qubit_count() == self.qubit_count(): 688 | on_qubits = self.qubit_ids 689 | else: 690 | raise ValueError("Specify the qubits to measure!") 691 | elif not isinstance(on_qubits, list) and not isinstance(on_qubits, set): 692 | on_qubits = [on_qubits] # single qubit label of some kind? 693 | 694 | with self._align_to_end(observable, on_qubits) as context: 695 | 696 | # Perform the measurement 697 | new_op, perm = context 698 | state = self.get_value() 699 | evalue, evector, probability = new_op.measure(state, extra_data=True) 700 | 701 | # update the global quantum state to account for the measurement 702 | # and do this in-context because the shuffle needs to be applied 703 | projector = evector.to_density_matrix() # because this uses the outer product 704 | updated_state = projector @ state.to_numpy_array() # project to the chosen eigenvector 705 | updated_state = [s / math.sqrt(probability) for s in updated_state] 706 | updated_state = StateVector(updated_state) 707 | 708 | self.set_value(updated_state) 709 | 710 | # some things we have to do manually 711 | inv_perm = invert_permutation(perm) 712 | evector = evector.rearrange(inv_perm) # return the eigenvector to the original arrangement 713 | 714 | """ 715 | # reduce the measurement result to the user-visible qubits 716 | current_state = self.get_value(force_density_matrix=True) 717 | if not set(on_qubits) == set(self.qubit_ids): 718 | current_state = current_state.partial_trace(keep_qubits=on_qubits) 719 | # FIXME: what comes next here? 720 | 721 | values, vectors = observable.eig() 722 | for vidx, v in enumerate(vectors): 723 | if all(v == vector): 724 | break 725 | result = values[vidx] 726 | """ 727 | return evalue 728 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | pytest 3 | 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottmckuen/qoord/4da20484e39cef5b9da9f6dc97146da803bf58dd/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_qoord.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import numpy as np 4 | 5 | from qoord.__support__ import close_enough, update_index, tupleize 6 | from qoord.states import StateVector, ZERO, ONE, DensityMatrix, MatrixOperator, QuantumState, \ 7 | identity_op, permute_to_end, numeric_list_to_permutation 8 | from qoord.core_operators import hadamard_op, cnot_op, pauli_z, pauli_x, phase_op 9 | from qoord.gates import UnitaryGate 10 | from qoord.core_gates import Hadamard, CNOT, PauliX, PauliZ as Z, Identity as I 11 | from qoord.qubits import Qubit, QubitSet 12 | from qoord.devices import Device 13 | 14 | 15 | def test_shuffle_binary_indices(): 16 | shuffle = {3: 5, 5: 3} 17 | original = 1000 18 | expected = 952 19 | actual = update_index(original, shuffle, 9) 20 | assert actual == expected 21 | 22 | shuffle = {0: 9, 9: 0} 23 | original = 1000 24 | expected = 489 25 | actual = update_index(original, shuffle, 2) 26 | assert actual == expected 27 | 28 | 29 | def test_setup(): 30 | device = Device(qubits=1) 31 | qbs = device.get_qubits([0]) 32 | assert qbs is not None 33 | assert isinstance(qbs, QubitSet) 34 | qubit = device.get_qubit(0) 35 | assert qubit is not None 36 | assert isinstance(qubit, Qubit) 37 | assert isinstance(device.get_state(), StateVector) 38 | 39 | device.initialize(StateVector((1, 0))) 40 | qubit_state_value = qubit.get_state() 41 | assert isinstance(qubit_state_value, StateVector) 42 | 43 | assert qubit.get_state()._array is not None 44 | 45 | 46 | def test_setup_two_qubits(): 47 | device = Device(qubits=2) 48 | qbs = device.get_qubits() # this should get all the qubits 49 | assert qbs is not None 50 | assert len(qbs) == 2 51 | 52 | q0 = device.get_qubit(0) 53 | q1 = device.get_qubit(1) 54 | assert q0 is not None 55 | assert q1 is not None 56 | 57 | 58 | def test_named_qubits_setup(): 59 | device = Device(qubits=['alice', 'bob', 'charlie']) 60 | qbs = device.get_qubits() 61 | assert qbs is not None 62 | assert len(qbs) == 3 63 | 64 | 65 | def test_bad_setup(): 66 | try: 67 | device = Device(qubits=[1, 1, 1]) 68 | assert False, "Should have raised an exception for duplicate qubits!" 69 | except ValueError as e: 70 | print(e) 71 | pass 72 | except Exception as e1: 73 | print(e1) 74 | exit() 75 | 76 | def test_two_qubits(): 77 | device = Device(qubits=2) 78 | device.initialize(StateVector((1, 0, 0, 0))) 79 | 80 | qubit = device.get_qubit(1) 81 | assert isinstance(qubit.get_state(), StateVector) 82 | 83 | assert qubit.get_state()._array is not None 84 | 85 | 86 | def test_matrix_operator(): 87 | 88 | mo = MatrixOperator(((0, 1), 89 | (1, 0))) 90 | 91 | sv = StateVector(array=(0.3, 0.8)) 92 | 93 | expected = StateVector(array=(0.8, 0.3)) 94 | actual = mo @ sv 95 | 96 | assert close_enough(actual.to_array(), expected.to_array()) 97 | 98 | 99 | def test_matrix_operator_dimension(): 100 | mo = MatrixOperator(((0, 1), 101 | (1, 0))) 102 | expected = 2 103 | actual = mo.dim() 104 | assert actual == expected 105 | 106 | 107 | def test_qubit_apply(): 108 | mo = MatrixOperator(((0, 1), 109 | (1, 0))) 110 | 111 | sv = StateVector(array=(0.3, 0.8)) 112 | state = QuantumState([0]) 113 | state.set_value(sv) 114 | qb = Qubit(state=state, label=0) 115 | qb.apply(mo) 116 | 117 | expected = StateVector(array=(0.8, 0.3)) 118 | actual = qb.get_state() 119 | 120 | assert close_enough(expected.to_array(), actual.to_array()) 121 | 122 | 123 | def test_permute_to_end(): 124 | actual = permute_to_end([3, 1], [x for x in range(6)]) 125 | expected = {0: 0, 1: 5, 2: 1, 3: 4, 4: 2, 5: 3} 126 | assert expected == actual 127 | 128 | 129 | def test_superposition(): 130 | 131 | device = Device(qubits=1) 132 | 133 | init_state = StateVector((1, 0)) 134 | device.initialize(init_state) 135 | qubit = device.get_qubit(0) 136 | 137 | Hadamard(qubit) 138 | 139 | state = qubit.get_state() 140 | actual_array = state.to_array() 141 | 142 | expected_state = StateVector((1, 1)) 143 | expected_array = expected_state.to_array() 144 | assert close_enough(actual_array, expected_array) 145 | 146 | 147 | def test_cnot(): 148 | device = Device(qubits=2) 149 | device.initialize(StateVector((0, 0, 1, 0))) # |10> 150 | 151 | qubits = device.get_qubits([0, 1]) 152 | CNOT(qubits) 153 | 154 | expected = StateVector((0, 0, 0, 1)) # |11> 155 | actual = device.get_state() 156 | 157 | assert close_enough(actual.to_array(), expected.to_array()) 158 | 159 | 160 | def test_is_unitary_validation(): 161 | """ Does UnitaryGate check for unitary input?""" 162 | try: 163 | foo = ((1, 0), (0, 2)) # not unitary, deliberately 164 | _ = UnitaryGate(foo) 165 | assert False 166 | except ValueError: 167 | assert True 168 | 169 | 170 | def test_bell_pair(): 171 | 172 | init_state = StateVector((1, 0, 0, 0)) 173 | 174 | hadamard_2qb = hadamard_op.tensor(identity_op) 175 | state2 = hadamard_2qb @ init_state 176 | state3 = cnot_op @ state2 177 | 178 | expected_state = StateVector((math.sqrt(1/2), 0, 0, math.sqrt(1/2))) 179 | assert close_enough(expected_state._array, state3._array) 180 | 181 | 182 | def test_state_vector_to_density_matrix(): 183 | sv = StateVector((1, 0)) 184 | actual = sv.to_density_matrix() 185 | expected = DensityMatrix(((1, 0), (0, 0))) 186 | 187 | assert actual == expected 188 | 189 | 190 | def test_bell_pair_measurement(): 191 | n_runs = 1000 192 | success = 0 193 | for _ in range(n_runs): 194 | bell_state = StateVector((math.sqrt(1/2), 0, 0, math.sqrt(1/2))) 195 | quantum_state = QuantumState((0, 1)) 196 | quantum_state.set_value(bell_state) 197 | bob_op = pauli_z # measure in the lab basis 198 | ev = quantum_state.measure(bob_op, [1]) 199 | 200 | # now allow alice to measure - she should see the same eigenvalue every time 201 | alice_op = pauli_z 202 | ev2 = quantum_state.measure(alice_op, [0]) 203 | 204 | if ev2 == ev: 205 | success += 1 206 | 207 | assert success == n_runs 208 | 209 | 210 | def test_tensor_sv(): 211 | sv1 = StateVector((1, 2)) 212 | sv2 = StateVector((3, 4)) 213 | 214 | sv3 = sv1.tensor(sv2) 215 | actual = sv3.to_array() 216 | expected = StateVector._normalize((3, 4, 6, 8)) 217 | 218 | assert close_enough(actual, expected) 219 | 220 | 221 | def test_density_matrix(): 222 | m = (1, 0) 223 | sv1 = DensityMatrix(m) 224 | 225 | a1_actual = sv1.to_array() 226 | a1_expected = tupleize(np.outer(m, m).tolist()) 227 | assert a1_actual == a1_expected 228 | 229 | sv2 = DensityMatrix((1/2, 1/2)) 230 | 231 | sv3 = sv1.tensor(sv2) 232 | actual = sv3.to_array() 233 | expected = ((0.25, 0.25, 0, 0), 234 | (0.25, 0.25, 0, 0), 235 | (0, 0, 0, 0), 236 | (0, 0, 0, 0)) 237 | 238 | assert actual == expected 239 | 240 | 241 | def test_tensor_mo(): 242 | mo1 = MatrixOperator(((1, 0), (0, -1))) 243 | mo2 = MatrixOperator(((1, 0), (0, 1))) 244 | 245 | mo3 = mo1.tensor(mo2) 246 | actual = mo3.to_array() 247 | expected = ((1, 0, 0, 0), 248 | (0, 1, 0, 0), 249 | (0, 0, -1, 0), 250 | (0, 0, 0, -1)) 251 | assert actual == expected 252 | 253 | mo4 = mo2.tensor(mo1) 254 | actual = mo4.to_array() 255 | expected = ((1, 0, 0, 0), 256 | (0, -1, 0, 0), 257 | (0, 0, 1, 0), 258 | (0, 0, 0, -1)) 259 | assert actual == expected 260 | 261 | 262 | def test_measurement_on_known_simple_states(): 263 | """ 264 | Deterministic if we know the state and measure along 265 | the appropriate axis. 266 | """ 267 | sv_0 = StateVector((1, 0)) 268 | observable = pauli_z 269 | vals, states = observable.eig() 270 | state_index = -1 271 | sv_0_array = sv_0.to_array() 272 | for idx, state in enumerate(states): 273 | if np.array_equal(sv_0_array, state): 274 | state_index = idx 275 | break 276 | 277 | expected = vals[state_index] 278 | actual = observable.measure(sv_0) # the state vector aligns with an eigenvector, so 279 | assert actual == expected 280 | 281 | 282 | def test_new_state(): 283 | """ 284 | Hadamard: measure once, confirm that the state updates 285 | to correspond to the eigenstate of the eigenvalue 286 | """ 287 | init_state = StateVector((1 / 2, 1 / 2)) 288 | quantum_state = QuantumState(qubit_ids=[0]) 289 | quantum_state.set_value(init_state) 290 | 291 | observable = pauli_z 292 | actual = quantum_state.measure(observable, on_qubits=[0]) 293 | 294 | vals, states = observable.eig() 295 | if actual == 1: 296 | expected_state = (1, 0) 297 | elif actual == -1: 298 | expected_state = (0, 1) 299 | else: 300 | assert False # should not happen 301 | 302 | actual_state = states[vals.tolist().index(actual)] 303 | actual_state = tuple(actual_state.tolist()) 304 | assert actual_state == expected_state 305 | 306 | 307 | def test_simple_statistics(): 308 | """ 309 | Hadamard: measure a bunch of independents, get 50/50 310 | """ 311 | n_runs = 10000 312 | p_expected = 0.5 313 | mu = p_expected*n_runs 314 | sigma = math.sqrt(p_expected*p_expected*n_runs) 315 | 316 | observable = pauli_z 317 | vals, states = observable.eig() 318 | 319 | results = {} 320 | for v in vals: 321 | results[v] = 0 322 | 323 | for run in range(n_runs): 324 | init_state = StateVector((1/2, 1/2)) 325 | actual = observable.measure(init_state) 326 | results[actual] += 1 327 | 328 | count1 = results[vals[0]] 329 | zscore = abs(count1 - mu)/sigma 330 | actual_rate = count1/n_runs 331 | 332 | # 3.7 Z-score is about 1 in 10K false-negative 333 | assert math.isclose(actual_rate, 0.5, abs_tol=3.7*zscore) 334 | 335 | 336 | def test_second_measurement_consistency(): 337 | """ 338 | Polarized lenses check: measure 0, then 90, get 0; 339 | This is equivalent to getting the same thing on the 340 | second measurement as on the first. 341 | """ 342 | ev, es = pauli_z.eig() 343 | test_value = ev[0] 344 | good_samples = 0 345 | good_seconds = 0 346 | init_state = StateVector((1 / 2, 1 / 2)) 347 | for n in range(1000): 348 | quantum_state = QuantumState(qubit_ids=[0]) 349 | quantum_state.set_value(init_state) 350 | v = quantum_state.measure(pauli_z) 351 | if v != test_value: 352 | continue 353 | good_samples += 1 354 | v2 = quantum_state.measure(pauli_z) 355 | if v2 == test_value: 356 | good_seconds += 1 357 | assert good_seconds == good_samples # 100% the same, nothing rotated 358 | 359 | 360 | def test0_45_90(): 361 | """ 362 | measure 0, then 45, then 90, get about 1/8 transmission 363 | This should be ZXZ? 364 | """ 365 | n_runs = 10000 366 | p_expected = 0.5 367 | mu = p_expected*n_runs 368 | sigma = math.sqrt(p_expected*p_expected*n_runs) 369 | 370 | evz, esz = pauli_z.eig() 371 | evx, esx = pauli_x.eig() 372 | 373 | good_count = 0 374 | for n in range(n_runs): 375 | a_system = StateVector((1, 1)) 376 | v = pauli_z.measure(a_system) 377 | if v != evz[0]: # only the first passes the filter 378 | continue 379 | v2 = pauli_x.measure(a_system) 380 | if v2 != evx[0]: # only the first passes the filter 381 | continue 382 | v3 = pauli_z.measure(a_system) 383 | if v3 != evz[0]: # only the first passes the filter 384 | continue 385 | 386 | good_count += 1 387 | 388 | actual_rate = good_count/n_runs 389 | zscore = abs(good_count - mu)/sigma 390 | assert math.isclose(actual_rate, 0.125, abs_tol=3.7*zscore) 391 | 392 | 393 | def test_power_of_operator(): 394 | expected = phase_op.to_array(as_numpy=True) 395 | actual = pauli_z ** (1/2) 396 | actual = actual.to_array(as_numpy=True) 397 | assert close_enough(actual, expected) 398 | 399 | 400 | def test_2qubit_gate_on_bigger_state(): 401 | # set up a 5-qubit state - every qubit defaults to |0> 402 | device = Device(qubits=5) 403 | # select a 2-qubit subsystem (#3 and #1) 404 | q1, q3 = device.get_qubits(keys=[1, 3]) 405 | 406 | # initialize q3 to |1> to control the CNOT (X-gate is a classical NOT gate) 407 | PauliX(q3) 408 | # this should flip q1 from |0> to |1> because q3 = |1> 409 | CNOT(q3, q1) 410 | 411 | # this returns the eigenvalue, either 1 (for state |0>) or -1 (|1>) 412 | actual = q3.measure(pauli_z) # should be -1 413 | 414 | # map the measured value to |0> or |1> 415 | expected = -1 416 | 417 | assert actual == expected 418 | 419 | actual = q1.measure(pauli_z) # should also be -1 because of CNOT earlier 420 | assert actual == expected 421 | 422 | 423 | def test_bell_state_coerce_density_matrix(): 424 | device = Device(qubits=2) 425 | device.make_bell_pair([0, 1]) 426 | 427 | qubit = device.get_qubit(0) 428 | state = qubit.get_state(force_density_matrix=True) 429 | 430 | expected_state = DensityMatrix(((0.5, 0, 0, 0.5), 431 | (0, 0, 0, 0), 432 | (0, 0, 0, 0), 433 | (0.5, 0, 0, 0.5))) 434 | 435 | actual_state_array = state.to_array(as_numpy=True) 436 | expected_state_array = expected_state.to_array(as_numpy=True) 437 | assert close_enough(actual_state_array, expected_state_array) 438 | 439 | 440 | def test_bell_state_partial_trace(): 441 | device = Device(qubits=2) 442 | device.make_bell_pair(qubits=[0, 1]) 443 | 444 | qubit = device.get_qubit(0) 445 | state = qubit.get_state(force_density_matrix=True) 446 | 447 | # this is the thing we partial-trace on: trace out Bob / qubit1 448 | alice_state = state.partial_trace(keep_qubits=[0]) 449 | 450 | ket0 = ZERO 451 | ket1 = ONE 452 | 453 | expected = (ket0.to_density_matrix() + ket1.to_density_matrix()) 454 | expected = expected.scale(0.5) 455 | 456 | alice_array = alice_state.to_array(True) 457 | expected_array = expected.to_array(True) 458 | assert close_enough(alice_array, expected_array) 459 | 460 | 461 | def test_partial_trace_00(): 462 | ket = StateVector((1, 0, 0, 0)) 463 | 464 | state = ket.to_density_matrix() 465 | reduced_state = state.partial_trace(keep_qubits=[0]) 466 | reduced_state = reduced_state.to_array(as_numpy=True) 467 | 468 | single_ket = StateVector((1, 0)) 469 | expected_state = single_ket.to_density_matrix().to_array(as_numpy=True) 470 | assert np.array_equal(expected_state, reduced_state) 471 | 472 | 473 | def test_partial_trace_01(): 474 | 475 | ket = StateVector((0, 1, 0, 0)) 476 | 477 | state = ket.to_density_matrix() 478 | reduced_state = state.partial_trace(keep_qubits=[1]) 479 | reduced_state = reduced_state.to_array(as_numpy=True) 480 | 481 | single_ket = StateVector((0, 1)) 482 | expected_state = single_ket.to_density_matrix().to_array(as_numpy=True) 483 | assert np.array_equal(expected_state, reduced_state) 484 | 485 | 486 | def test_partial_trace_100(): 487 | 488 | ket = StateVector((1, 0, 0, 0, 0, 0, 0, 0)) 489 | 490 | state = ket.to_density_matrix() 491 | reduced_state = state.partial_trace(keep_qubits=[0]) 492 | reduced_state = reduced_state.to_array(as_numpy=True) 493 | 494 | expected_state = ZERO.to_density_matrix().to_array(as_numpy=True) 495 | assert np.array_equal(expected_state, reduced_state) 496 | 497 | reduced_state = state.partial_trace(keep_qubits=[1, 2]) 498 | reduced_state = reduced_state.to_array(True) 499 | expected_state = ZERO.tensor(ZERO) 500 | expected_state = expected_state.to_density_matrix().to_array(as_numpy=True) 501 | assert np.array_equal(expected_state, reduced_state) 502 | 503 | 504 | def test_partial_trace_010(): 505 | 506 | ket = ZERO.tensor(ONE).tensor(ZERO) 507 | 508 | state = ket.to_density_matrix() 509 | reduced_state = state.partial_trace(keep_qubits=[1]) 510 | reduced_state = reduced_state.to_array(as_numpy=True) 511 | 512 | expected_state = ONE.to_density_matrix().to_array(as_numpy=True) 513 | assert np.array_equal(expected_state, reduced_state) 514 | 515 | reduced_state = state.partial_trace(keep_qubits=[0, 2]) 516 | reduced_state = reduced_state.to_array(True) 517 | expected_state = ZERO.tensor(ZERO) 518 | expected_state = expected_state.to_density_matrix().to_array(as_numpy=True) 519 | assert np.array_equal(expected_state, reduced_state) 520 | 521 | 522 | def test_partial_trace_01010(): 523 | 524 | ket = ZERO.tensor(ONE) 525 | ket = ket.tensor(ZERO) 526 | ket = ket.tensor(ONE) 527 | ket = ket.tensor(ZERO) 528 | 529 | state = ket.to_density_matrix() 530 | reduced_state = state.partial_trace(keep_qubits=[3, 1]) 531 | reduced_state = reduced_state.to_array(as_numpy=True) 532 | 533 | expected_state = ONE.tensor(ONE) 534 | expected_state = expected_state.to_density_matrix() 535 | expected_state = expected_state.to_array(as_numpy=True) 536 | 537 | assert np.array_equal(expected_state, reduced_state) 538 | 539 | 540 | def test_numeric_list_to_permutation(): 541 | p_expected = {0: 1, 1: 0} 542 | p_actual = numeric_list_to_permutation([3, 1]) 543 | assert p_actual == p_expected 544 | 545 | 546 | def test_mo_distribution(): 547 | op = Z.tensor(I).to_operator() 548 | device = Device(qubits=2) 549 | device.make_bell_pair([0, 1]) 550 | 551 | qubit = device.get_qubit(0) 552 | state = qubit.get_state() 553 | projectors, probabilities = op.distribution(state) 554 | # print(probabilities) 555 | assert all([math.isclose(p, 0.5) for p in probabilities.values()]) 556 | --------------------------------------------------------------------------------