quantum/lib_q_computer_math.py
2019-12-18 12:53:05 +01:00

637 lines
21 KiB
Python

from __future__ import annotations
import random
from collections import defaultdict
from functools import reduce
from typing import Union
import numpy as np
from numbers import Complex
import sympy
from load_test import sizeof_fmt
ListOrNdarray = Union[list, np.ndarray]
REPR_TENSOR_OP = ""
REPR_GREEK_PSI = "ψ"
REPR_GREEK_PHI = "φ"
REPR_MATH_KET = ""
REPR_MATH_BRA = ""
REPR_MATH_SQRT = ""
REPR_MATH_SUBSCRIPT_NUMBERS = "₀₁₂₃₄₅₆₇₈₉"
# Keep a reference to already used states for naming
UNIVERSE_STATES = []
class Matrix(object):
"""Wraps a Matrix... it's for my understanding, this could easily probably be np.array"""
def __init__(self, m: ListOrNdarray = None, *args, **kwargs):
"""
Can be initialized with a matrix, e.g. for |0> this is [[0],[1]]
:param m: a matrix representing the quantum state
:param name: the name of the state (optional)
"""
if m is None:
self.m = np.array([])
elif type(m) is list:
self.m = np.array(m)
elif type(m) is np.ndarray:
self.m = m
else:
raise TypeError("m needs to be a list or ndarray type")
def __add__(self, other):
return Matrix(self.m + other.m)
def __eq__(self, other):
if isinstance(other, Complex):
return bool(np.allclose(self.m, other))
elif isinstance(other, TwoQubitPartial):
return False
return np.allclose(self.m, other.m)
def __mul_or_kron__(self, other):
"""
Multiplication with a number is Linear op;
with another state is a composition via the Kronecker/Tensor product
"""
if isinstance(other, Complex):
return Matrix(self.m * other)
elif isinstance(other, Matrix):
return self.__class__(np.kron(self.m, other.m))
raise NotImplementedError
def __rmul__(self, other):
return self.__mul_or_kron__(other)
def __mul__(self, other):
return self.__mul_or_kron__(other)
def __or__(self, other):
"""Define inner product: <self|other>
"""
m = np.dot(self._conjugate_transpose(), other.m)
try:
return self.__class__(m)
except:
try:
return other.__class__(m)
except:
...
return Matrix(m)
def __len__(self):
return len(self.m)
def outer(self, other):
"""Define outer product |0><0|"""
return Matrix(np.outer(self.m, other.m))
def x(self, other):
"""Define outer product |0><0| looks like |0x0| which is 0.x(0)"""
return self.outer(other)
def _conjugate_transpose(self):
return self.m.transpose().conjugate()
def _complex_conjugate(self):
return self.m.conjugate()
def conjugate_transpose(self):
return Matrix(self._conjugate_transpose())
def complex_conjugate(self):
return Matrix(self._complex_conjugate())
class Vector(Matrix):
def __init__(self, m: ListOrNdarray = None, *args, **kwargs):
super().__init__(m, *args, **kwargs)
if not self._is_vector():
raise TypeError("Not a vector")
def _is_vector(self):
return self.m.shape[1] == 1
class State(Vector):
def __init__(self, m: ListOrNdarray = None, name: str = '', *args, **kwargs):
"""State vector representing quantum state"""
super().__init__(m, *args, **kwargs)
self.name = name
if not self._is_normalized():
raise TypeError("Not a normalized state vector")
def _is_normalized(self):
return np.isclose(np.sum(np.abs(self.m ** 2)), 1.0)
def __repr__(self):
if self.name:
return '|{}>'.format(self.name)
for well_known_state in well_known_states:
if self == well_known_state:
return repr(well_known_state)
for used_state in UNIVERSE_STATES:
if self.m == used_state:
return repr(used_state)
next_state_sub = ''.join([REPR_MATH_SUBSCRIPT_NUMBERS[int(d)] for d in str(len(UNIVERSE_STATES))])
state_name = '|{}{}>'.format(REPR_GREEK_PSI, next_state_sub)
UNIVERSE_STATES.append(self.m)
return state_name
def norm(self):
"""Norm/Length of the vector = sqrt(<self|self>)"""
return self.length()
def length(self):
"""Norm/Length of the vector = sqrt(<self|self>)"""
return np.sqrt((self | self).m).item(0)
def is_orthogonal(self, other):
"""If the inner (dot) product is zero, this vector is orthogonal to other"""
return self | other == 0
def get_prob(self, j):
"""pr(j) = |<e_j|self>|^2"""
# j_th basis vector
e_j = State([[1] if i == int(j) else [0] for i in range(len(self.m))])
return np.absolute((e_j | self).m.item(0)) ** 2
def measure(self):
weights = [self.get_prob(j) for j in range(len(self))]
format_str = "{:0" + str(int(np.ceil(np.log2(len(weights))))) + "b}"
choices = [format_str.format(i) for i in range(len(weights))]
return random.choices(choices, weights)[0]
class Operator(object):
def __init__(self, func=None, *args, **kwargs):
"""An Operator turns one function into another"""
self.func = func
def on(self, *args, **kwargs):
return self(*args, **kwargs)
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
class LinearOperator(Operator):
def __init__(self, func=None, *args, **kwargs):
"""Linear operators satisfy f(x+y) = f(x) + f(y) and a*f(x) = f(a*x)"""
super().__init__(func, *args, **kwargs)
if not self._is_linear():
raise TypeError("Not a linear operator")
def _is_linear(self):
# TODO: How to verify if the func is linear?
# in case of Unitary Operator, self.func is a lambda that takes a Matrix (assumes has .m component)
return True
# a, b = sympy.symbols('a, b')
# expr, vars_ = a+b, [a, b]
# for x in vars_:
# for y in vars_:
# try:
# if not sympy.Eq(sympy.diff(expr, x, y), 0):
# return False
# except TypeError:
# return False
# return True
class SquareMatrix(Matrix):
def __init__(self, m: ListOrNdarray, *args, **kwargs):
super().__init__(m, *args, **kwargs)
if not self._is_square():
raise TypeError("Not a Square matrix")
def _is_square(self):
return self.m.shape[0] == self.m.shape[1]
class UnitaryMatrix(SquareMatrix):
def __init__(self, m: ListOrNdarray, *args, **kwargs):
"""Represents a Unitary matrix that satisfies UU+ = I"""
super().__init__(m, *args, **kwargs)
if not self._is_unitary():
raise TypeError("Not a Unitary matrix")
def _is_unitary(self):
"""Checks if the matrix product of itself with conjugate transpose is Identity UU+ = I"""
UU_ = np.dot(self._conjugate_transpose(), self.m)
I = np.eye(self.m.shape[0])
return np.isclose(UU_, I).all()
class UnitaryOperator(LinearOperator, UnitaryMatrix):
def __init__(self, m: ListOrNdarray, name: str = '', *args, **kwargs):
"""UnitaryOperator inherits from both LinearOperator and a Unitary matrix
It is used to act on a State vector by defining the operator to be the dot product"""
self.name = name
UnitaryMatrix.__init__(self, m=m, *args, **kwargs)
LinearOperator.__init__(self, func=self.operator_func, *args, **kwargs)
def operator_func(self, other):
if not hasattr(other, "m"):
other_m = other
else:
other_m = other.m
return State(np.dot(self.m, other_m))
def __repr__(self):
if self.name:
return '-{}-'.format(self.name)
return str(self.m)
"""
Define States and Operators
"""
_0 = State([[1],
[0]],
name='0')
_1 = State([[0],
[1]],
name='1')
_p = State([[1 / np.sqrt(2)],
[1 / np.sqrt(2)]],
name='+')
_m = State([[1 / np.sqrt(2)],
[- 1 / np.sqrt(2)]],
name='-')
well_known_states = [_0, _1, _p, _m]
_ = I = UnitaryOperator([[1, 0],
[0, 1]],
name="-")
X = UnitaryOperator([[0, 1],
[1, 0]],
name="X")
Y = UnitaryOperator([[0, -1j],
[1j, 0]],
name="Y")
Z = UnitaryOperator([[1, 0],
[0, -1]],
name="Z")
H = UnitaryOperator([[1 / np.sqrt(2), 1 / np.sqrt(2)],
[1 / np.sqrt(2), -1 / np.sqrt(2)], ],
name="H")
# TODO - How to add a CNOT gate to the Quantum Processor?
# Imagine if I have to act on a 3-qubit computer and CNOT(q1, q3)
#
# Decomposed CNOT :
# reverse engineered from
# https://quantumcomputing.stackexchange.com/questions/4252/how-to-derive-the-cnot-matrix-for-a-3-qbit-system-where-the-control-target-qbi
#
# CNOT(q1, I, q2):
# |0><0| x I_2 x I_2 + |1><1| x I_2 x X
# np.kron(np.kron(np.outer(_0.m, _0.m), np.eye(2)), np.eye(2)) + np.kron(np.kron(np.outer(_1.m, _1.m), np.eye(2)), X.m)
#
#
# CNOT(q1, q2):
# |0><0| x I + |1><1| x X
# np.kron(np.outer(_0.m, _0.m), np.eye(2)) + np.kron(np.outer(_1.m, _1.m), X.m)
# _0.x(_0) * Matrix(I.m) + _1.x(_1) * Matrix(X.m)
class TwoQubitPartial(object):
def __init__(self, rpr):
self.rpr = rpr
self.operator = None
def __repr__(self):
return str("-{}-".format(self.rpr))
C_ = TwoQubitPartial("C")
x_ = TwoQubitPartial("x")
class TwoQubitOperator(UnitaryOperator):
def __init__(self, m: ListOrNdarray, A: TwoQubitPartial, B: TwoQubitPartial,
A_p: UnitaryOperator, B_p:UnitaryOperator, *args, **kwargs):
super().__init__(m, *args, **kwargs)
A.operator, B.operator = self, self
self.A = A
self.B = B
self.A_p = A_p
self.B_p = B_p
def verify_step(self, step):
if not (step.count(self.A) == 1 and step.count(self.B) == 1):
raise RuntimeError("Both CONTROL and TARGET need to be defined in the same step exactly once")
def compose(self, step, state):
# TODO: Hacky way to do CNOT
# Should generalize for a 2-Qubit gate
# _0.x(_0) * Matrix(I.m) + _1.x(_1) * Matrix(X.m)
outer_0, outer_1 = [], []
for s in step:
if s == self.A:
outer_0.append(_0.x(_0))
outer_1.append(_1.x(_1))
elif s == self.B:
outer_0.append(Matrix(self.A_p.m))
outer_1.append(Matrix(self.B_p.m))
else:
outer_0.append(Matrix(s.m))
outer_1.append(Matrix(s.m))
return reduce((lambda x, y: x * y), outer_0) + reduce((lambda x, y: x * y), outer_1)
CNOT = TwoQubitOperator([[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 0, 1],
[0, 0, 1, 0], ],
TwoQubitPartial("C"),
TwoQubitPartial("x"),
I,
X)
C, x = CNOT.A, CNOT.B
# TODO: End Hacky way to define 2-qbit gate
###########################################################
def test():
# Test properties of Hilbert vector space
# The four postulates of Quantum Mechanics
# I: States | Associated to any physical system is a complex vector space
# known as the state space of the system. If the system is closed
# then the system is described completely by its state vector
# which is a unit vector in the space.
# Mathematically, this vector space is also a function space
assert _0 + _1 == _1 + _0 # commutativity of vector addition
assert _0 + (_1 + _p) == (_0 + _1) + _p # associativity of vector addition
assert 8 * (_0 + _1) == 8 * _0 + 8 * _1 # Linear when multiplying by constants
assert _0 | _0 == 1 # parallel have 1 product
assert _0 | _1 == 0 # orthogonal have 0 product
assert _0.is_orthogonal(_1)
assert _1 | (8 * _0) == 8 * (_1 | _0) # Inner product is linear multiplied by constants
assert _p | (_1 + _0) == (_p | _1) + (_p | _0) # Inner product is linear in superpos of vectors
assert np.isclose(_1.length(), 1.0) # all of the vector lengths are normalized
assert np.isclose(_0.length(), 1.0)
assert np.isclose(_p.length(), 1.0)
assert _0 | _1 == (_1 | _0).complex_conjugate() # non-commutative inner product
# II: Dynamics | The evolution of a closed system is described by a unitary transformation
#
# Operators turn one vector into another
# the times 2 operator should return the times two multiplication
_times_2 = Operator(lambda x: 2 * x)
assert _times_2.on(5) == 10
assert _times_2(5) == 10
# Pauli X gate flips the |0> to |1> and the |1> to |0>
assert X | _1 == _0
assert X | _0 == _1
# Test the Y Pauli operator with complex number literal notation
assert Y | _0 == State([[0],
[1j]])
assert Y | _1 == State([[-1j],
[0]])
# III: Measurement | A quantum measurement is described by an orthonormal basis |e_j>
# for state space. If the initial state of the system is |psi>
# then we get outcome j with probability pr(j) = |<e_j|psi>|^2
assert _0.get_prob(0) == 1 # Probability for |0> in 0 is 1
assert _0.get_prob(1) == 0 # Probability for |0> in 1 is 0
assert _1.get_prob(0) == 0 # Probability for |1> in 0 is 0
assert _1.get_prob(1) == 1 # Probability for |1> in 1 is 1
assert np.isclose(_p.get_prob(0), 0.5) # Probability for |+> in 0 is 0.5
assert np.isclose(_p.get_prob(1), 0.5) # Probability for |+> in 1 is 0.5
# IV: Compositing | tensor/kronecker product when composing
assert _0 * _0 == State([[1], [0], [0], [0]])
assert _0 * _1 == State([[0], [1], [0], [0]])
assert _1 * _0 == State([[0], [0], [1], [0]])
assert _1 * _1 == State([[0], [0], [0], [1]])
# CNOT applies a control qubit on a target.
# If the control is a |0>, target remains unchanged.
# If the control is a |1>, target is flipped.
assert CNOT.on(_0 * _0) == _0 * _0
assert CNOT.on(_0 * _1) == _0 * _1
assert CNOT.on(_1 * _0) == _1 * _1
assert CNOT.on(_1 * _1) == _1 * _0
# ALL FOUR NOW - Create a Bell state (I) that has H|0> (II), measures (III) and composition in CNOT (IV)
# Bell state
# First - create a superposition
H = UnitaryOperator([[1 / np.sqrt(2), 1 / np.sqrt(2)],
[1 / np.sqrt(2), -1 / np.sqrt(2)], ])
superpos = H | _0
assert superpos == _p
# Then CNOT the superposition with a |0> qubit
bell = CNOT | (superpos * _0)
assert bell == State([[1 / np.sqrt(2)],
[0.],
[0.],
[1 / np.sqrt(2)], ])
assert np.isclose(bell.get_prob(0b00), 0.5) # Probability for bell in 00 is 0.5
assert np.isclose(bell.get_prob(0b01), 0) # Probability for bell in 01 is 0.
assert np.isclose(bell.get_prob(0b10), 0) # Probability for bell in 10 is 0.
assert np.isclose(bell.get_prob(0b11), 0.5) # Probability for bell in 11 is 0.5
################################
# TODO: Don't know where outer product fits...
assert _0.x(_0) == Matrix([[1, 0],
[0, 0]])
assert _0.x(_1) == Matrix([[0, 1],
[0, 0]])
assert _1.x(_0) == Matrix([[0, 0],
[1, 0]])
assert _1.x(_1) == Matrix([[0, 0],
[0, 1]])
def naive_load_test(N):
import os
import psutil
import gc
from time import time
from sys import getsizeof
print("{:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10}".format(
"qbits",
"kron_len",
"mem_used",
"mem_per_q",
"getsizeof",
"getsiz/len",
"nbytes",
"nbytes/len",
"time"))
_0 = State([[1], [0]], name='0')
process = psutil.Process(os.getpid())
mem_init = process.memory_info().rss
for i in range(2, N + 1):
start = time()
m = _0
for _ in range(i):
m = m * _0
len_m = len(m)
elapsed = time() - start
mem_b = process.memory_info().rss - mem_init
print("{:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10}".format(
i,
len(m),
sizeof_fmt(mem_b),
sizeof_fmt(mem_b / len_m),
sizeof_fmt(getsizeof(m)),
sizeof_fmt(getsizeof(m) / len_m),
sizeof_fmt(m.m.nbytes),
sizeof_fmt(m.m.nbytes / len_m),
np.round(elapsed, 2)))
gc.collect()
class QuantumProcessor(object):
HALT_STATE = "HALT"
RUNNING_STATE = "RUNNING"
def __init__(self, n_qubits: int):
self.n_qubits = n_qubits
self.steps = [[_0 for _ in range(n_qubits)], ]
self._called_add_row = 0
self.current_step = 0
self.current_quantum_state = None
self.current_state = self.HALT_STATE
self.reset()
def add_row_step(self, row: int, step: int, qbit_state):
if len(self.steps) <= step:
self.steps += [[I for _ in range(self.n_qubits)] for _ in range(len(self.steps) - step + 1)]
self.steps[step][row] = qbit_state
def add_step(self, step_data: list):
if len(step_data) != self.n_qubits:
raise RuntimeError("Length of step is: {}, should be: {}".format(len(step_data), self.n_qubits))
step_i = len(step_data)
for row, qubit_state in enumerate(step_data):
self.add_row_step(row, step_i, qubit_state)
def add_steps(self, steps_data: list):
for step_data in steps_data:
self.add_step(step_data)
def add_row(self, row_data: list):
if self._called_add_row >= self.n_qubits:
raise RuntimeError("Adding more rows than qubits")
for step_i, qubit_state in enumerate(row_data):
self.add_row_step(self._called_add_row, step_i + 1, qubit_state)
self._called_add_row += 1
def add_rows(self, rows_data: list):
for row_data in rows_data:
self.add_row(row_data)
def compose_quantum_state(self, step):
if any([type(s) is TwoQubitPartial for s in step]):
two_qubit_gates = filter(lambda s: type(s) is TwoQubitPartial, step)
state = []
for two_qubit_gate in two_qubit_gates:
two_qubit_gate.operator.verify_step(step)
state = two_qubit_gate.operator.compose(step, state)
return state
return reduce((lambda x, y: x * y), step)
def step(self):
if self.current_step == 0 and self.current_state == self.HALT_STATE:
self.current_state = self.RUNNING_STATE
if self.current_step >= len(self.steps):
self.current_state = self.HALT_STATE
raise RuntimeWarning("Halted")
running_step = self.steps[self.current_step]
step_quantum_state = self.compose_quantum_state(running_step)
if not self.current_quantum_state:
self.current_quantum_state = step_quantum_state
else:
self.current_quantum_state = State((step_quantum_state | self.current_quantum_state).m)
self.current_step += 1
def run(self):
for _ in self.steps:
self.step()
self.current_state = self.HALT_STATE
def reset(self):
self.current_step = 0
self.current_quantum_state = None
self.current_state = self.HALT_STATE
def print(self):
print("=" * 3 * len(self.steps))
for line_no in range(self.n_qubits):
line = ''
for step in self.steps:
state_repr = repr(step[line_no])
line += state_repr
print(line)
print("=" * 3 * len(self.steps))
def measure(self):
if self.current_state != self.HALT_STATE:
raise RuntimeError("Processor is still running")
return self.current_quantum_state.measure()
def run_n(self, n: int):
for i in range(n):
self.run()
result = self.measure()
print("Run {}: {}".format(i, result))
self.reset()
def get_sample(self, n: int):
rv = defaultdict(int)
for i in range(n):
self.run()
result = self.measure()
rv[result] += 1
self.reset()
for k, v in sorted(rv.items(), key=lambda x: x[0]):
print("{}: {}".format(k, v))
return rv
def test_quantum_processor():
# Produce Bell state between 0 and 2 qubit
qp = QuantumProcessor(3)
qp.add_row([H, C])
qp.add_row([_, _])
qp.add_row([_, x])
qp.print()
qp.get_sample(100)
if __name__ == "__main__":
test()
test_quantum_processor()