diff --git a/11_experiments.py b/11_experiments.py new file mode 100644 index 0000000..8479fb2 --- /dev/null +++ b/11_experiments.py @@ -0,0 +1,100 @@ +import matplotlib.pyplot as plt +from lib import * + + +def plot(t, s): + fig, ax = plt.subplots() + ax.plot(t, s) + + ax.set(xlabel='angle', ylabel='result', title='plot') + ax.grid() + + fig.savefig("test.png") + plt.show() + +# Double-slit, which-way +# https://physics.stackexchange.com/questions/229895/understanding-the-quantum-eraser-from-a-quantum-information-stand-point + +# Delayed-choice quantum eraser: +# https://quantumcomputing.stackexchange.com/questions/1568/what-is-the-quantum-circuit-equivalent-of-a-delayed-choice-quantum-eraser + +# The matrix +# Λ=eiϕ/2|0⟩⟨0|+e−iϕ/2|1⟩⟨1| +# introduces a phase shift between the two paths (this can e.g. be a +# function of the position on the screen, or the relative length of +# the interferometer arms) + +# See note on the post: +# you could use quirk to provide interactive versions of the quantum +# circuits. For example, your first circuit is this +# https://algassert.com/quirk#circuit={%22cols%22:[[%22H%22],[%22Z^t%22], +# [%22H%22],[%22Measure%22]]} +# , while your second is +# https://algassert.com/quirk#circuit=%7B%22cols%22:%5B%5B%22H%22%5D, +# %5B%22%E2%80%A2%22,%22X%22%5D,%5B%22Z%5Et%22%5D,%5B%22H%22%5D%5D%7D +# this. + +# Note(pisquared): The difference seems in the Lambda operator which +# seems to be actually a [T-Gate]( +# https://www.quantum-inspire.com/kbase/t-gate/) +# In quirk, the Z^t keeps |0><0| while the description +# in the post spins |0><0| to oposite degrees which +# cancels after application of the second H-gate + + +def double_slit(): + """ + # + :return: + """ + ms = list() + angles = np.arange(-4 * np.pi, 4 * np.pi, 0.1) + for theta in angles: + # Here, the first H puts the qubit in the superposition of both + # paths/slits -- the states |0⟩ and |1⟩ correspond to the two + # paths/slits + phi1 = H.on(s("|0>")) + phi2 = T(theta).on(phi1) + + # and the second H makes the two paths interfere. + phi3 = H.on(phi2) + + # If the output qubit is measured in state |0⟩, + # the interference is constructive, otherwise desctructive. + # + # You can easily check that this yields an interference pattern which + # varies like prob(0)=cos(ϕ)2. + + ms.append(phi3.get_prob(0)) + # ms.append(phi3.measure()) + plot(angles, ms) + + +def which_way(): + ms = list() + angles = np.arange(-4 * np.pi, 4 * np.pi, 0.1) + for theta in angles: + # Here, the first H puts the qubit in the superposition of both + # paths/slits -- the states |0⟩ and |1⟩ correspond to the two + # paths/slits + phi1 = H.on(s("|0>")) + + # Now imagine that we want to copy the which-way information: + # Then, what we do is + q2 = s("|0>") + phi2 = CNOT.on(phi1 * q2) + phi3 = T(theta).on(phi2, 0) + phi4 = H.on(phi3, 0) + + # this is, we copy the which-way information before traversing the + # interferometer onto qubit c. It is easy to see that this destroys + # the interference pattern, i.e., prob(0)=1/2. + + ms.append(phi4.get_prob(0)) + # ms.append(phi3.measure()) + plot(angles, ms) + + +if __name__ == "__main__": + double_slit() + which_way() diff --git a/lib.py b/lib.py index 6ef1740..12f776f 100644 --- a/lib.py +++ b/lib.py @@ -261,10 +261,11 @@ class State(Vector): def get_computational_basis_vector(self, j): return Vector([[1] if i == int(j) else [0] for i in range(len(self.m))]) - def get_prob(self, j, basis=None): + def get_prob(self, j=None, basis=None): """pr(j) = ||^2 """ if basis is None: + assert j is not None basis = self.get_computational_basis_vector(j) m = MeasurementOperator.create_from_basis(basis) return m.get_prob(self) @@ -408,11 +409,18 @@ def normalize_state(vector: Vector): def s(q, name=None): """Helper method for creating state easily""" if type(q) == str: - # e.g. |000> if q[0] == '|' and q[-1] == '>': + # e.g. |000> state = State.from_string(q) state.name = name return state + elif q[0] == '|' and q[-1] == '|' and "><" in q: + # e.g. |0><0| + f_qbit, s_qbit = q.split("<")[0], q.split(">")[1] + if len(f_qbit) == len(s_qbit): + s_qbit = '|' + s_qbit[1:-1] + '>' + fs, ss = State.from_string(f_qbit), State.from_string(s_qbit) + return DensityMatrix(fs.x(ss).m, name=name) elif type(q) == list: # e.g. s([1,0]) => |0> return State(np.reshape(q, (len(q), 1)), name=name) @@ -567,7 +575,17 @@ class UnitaryOperator(LinearTransformation, UnitaryMatrix): LinearTransformation.__init__(self, m=m, func=self.operator_func, *args, **kwargs) self.name = name - def operator_func(self, other): + def operator_func(self, other, which_qbit=None): + this_cols, other_rows = np.shape(self.m)[1], np.shape(other.m)[0] + if this_cols != other_rows: + if which_qbit is None: + raise Exception("Operating dim-{} operator on a dim-{} state. " + "Please specify which_qubit to operate on".format( + this_cols, other_rows)) + + extended_m = [self if i == which_qbit else I for i in range(int(np.log2(other_rows)))] + extended_op = UnitaryOperator(np.prod(extended_m).m) + return State(np.dot(extended_op.m, other.m)) return State(np.dot(self.m, other.m)) def __repr__(self): @@ -576,9 +594,15 @@ class UnitaryOperator(LinearTransformation, UnitaryMatrix): return str(self.m) +class Gate(UnitaryOperator): + def __init__(self, *args, **kwargs): + """Alias: Gates are Unitary Operators""" + UnitaryOperator.__init__(self, *args, **kwargs) + + class HermitianOperator(LinearTransformation, HermitianMatrix): def __init__(self, m: ListOrNdarray, name: str = '', *args, **kwargs): - """HermitianMatrix inherits from both LinearTransformation and a Hermitian matrix + """HermitianOperator inherits from both LinearTransformation and a Hermitian matrix It is used to act on a State vector by defining the operator to be the dot product""" self.name = name HermitianMatrix.__init__(self, m=m, *args, **kwargs) @@ -594,6 +618,25 @@ class HermitianOperator(LinearTransformation, HermitianMatrix): return str(self.m) +class DensityMatrix(HermitianOperator): + def __init__(self, m: ListOrNdarray, name: str = '', *args, **kwargs): + """The density matrix is a representation of a linear operator called + the density operator. The density matrix is obtained from the density + operator by choice of basis in the underlying space. In practice, + the terms density matrix and density operator are often used + interchangeably. Both matrix and operator are self-adjoint + (or Hermitian), positive semi-definite, of trace one, and may + be infinite-dimensional.""" + self.name = name + HermitianOperator.__init__(self, m=m, *args, **kwargs) + if not self._is_density(): + raise TypeError("Not a Density matrix") + + def _is_density(self): + """Checks if it trace is 1""" + return np.isclose(np.trace(self.m), 1.0) + + # 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) # @@ -712,21 +755,21 @@ b_psi_m = State([[0], well_known_states = [_p, _m, b_phi_p, b_psi_p, b_phi_m, b_psi_m] -_ = I = UnitaryOperator([[1, 0], - [0, 1]], - name="-") +_ = I = Gate([[1, 0], + [0, 1]], + name="-") -X = UnitaryOperator([[0, 1], - [1, 0]], - name="X") +X = Gate([[0, 1], + [1, 0]], + name="X") -Y = UnitaryOperator([[0, -1j], - [1j, 0]], - name="Y") +Y = Gate([[0, -1j], + [1j, 0]], + name="Y") -Z = UnitaryOperator([[1, 0], - [0, -1]], - name="Z") +Z = Gate([[1, 0], + [0, -1]], + name="Z") # These are rotations that are specified commonly e.g. in @@ -736,27 +779,31 @@ Z = UnitaryOperator([[1, 0], # However - they are correct up to a global phase which is all that matters for measurement purposes # -def Rx(theta): - return UnitaryOperator([[np.cos(theta / 2), -1j * np.sin(theta / 2)], - [-1j * np.sin(theta / 2), np.cos(theta / 2)]], - name="Rx") +H = Gate([[1 / np.sqrt(2), 1 / np.sqrt(2)], + [1 / np.sqrt(2), -1 / np.sqrt(2)], ], + name="H") + +# Rotation operators +# https://www.quantum-inspire.com/kbase/rx-gate/ +Rx = lambda theta: Gate([[np.cos(theta / 2), -1j * np.sin(theta / 2)], + [-1j * np.sin(theta / 2), np.cos(theta / 2)]], + name="Rx") -def Ry(theta): - return UnitaryOperator([[np.cos(theta / 2), -np.sin(theta / 2)], - [np.sin(theta / 2), np.cos(theta / 2)]], - name="Ry") +Ry = lambda theta: Gate([[np.cos(theta / 2), -np.sin(theta / 2)], + [np.sin(theta / 2), np.cos(theta / 2)]], + name="Ry") -def Rz(theta): - return UnitaryOperator([[np.power(np.e, -1j * theta / 2), 0], - [0, np.power(np.e, 1j * theta / 2)]], - name="Rz") +Rz = lambda theta: Gate([[np.power(np.e, -1j * theta / 2), 0], + [0, np.power(np.e, 1j * theta / 2)]], + name="Rz") -H = UnitaryOperator([[1 / np.sqrt(2), 1 / np.sqrt(2)], - [1 / np.sqrt(2), -1 / np.sqrt(2)], ], - name="H") +# See [T-Gate](https://www.quantum-inspire.com/kbase/t-gate/) +T = lambda phi: Gate([[1, 0], + [0, np.power(np.e, 1j * phi / 2)]], + name="T") CNOT = TwoQubitOperator([ [1, 0, 0, 0], @@ -997,8 +1044,6 @@ def test(): # 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.on(_0) assert superpos == _p