├── .github └── workflows │ └── ci.yml ├── .gitignore ├── README.md ├── development_requirements.txt ├── mpsim ├── __init__.py ├── core.py ├── core_test.py ├── gates.py ├── gates_test.py └── mpsim_cirq │ ├── __init__.py │ ├── circuits.py │ ├── circuits_test.py │ ├── simulator.py │ └── simulator_test.py ├── requirements.txt └── setup.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python Build 5 | 6 | on: 7 | pull_request: 8 | branches: [ master ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | python-version: [3.6, 3.7, 3.8] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install Python Dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install flake8 pytest 28 | pip install -e .[development] 29 | - name: Lint with flake8 30 | run: | 31 | # Stop the build if there are Python syntax errors or undefined names 32 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 33 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 34 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 35 | - name: Test with pytest 36 | run: | 37 | pytest 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .pytest_cache 3 | .idea 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MPSim 2 | 3 | Package for using Matrix Product States (MPS) to simulate quantum circuits. 4 | 5 | # Installation 6 | 7 | After cloning the repository, run 8 | 9 | ```bash 10 | pip install -e . 11 | ``` 12 | 13 | in the directory with `setup.py`. 14 | 15 | # Getting started 16 | 17 | The main object is `mpsim.MPS` which defines a matrix product state of qudits initialized to the all zero state. 18 | 19 | An `MPS` can be acted on by aribtrary one-qudit and two-qudit operations. Operations on >=3 qubits are not supported and 20 | must be compiled into a sequence of one- and two-qudit operations. 21 | 22 | An `MPSOperation` consists of a gate and tuple of indices specifying which tensor(s) the gate acts on in the MPS. 23 | Gates must be expressed as `TensorNetwork.Node` objects with the appropriate number of edges. 24 | Some common gates are defined in `mpsim.gates`. 25 | 26 | The following program initializes an MPS in the |00> state and prepares a Bell state. 27 | 28 | ```python 29 | import mpsim 30 | 31 | mps = mpsim.MPS(nqudits=2) 32 | mps.h(0) 33 | mps.cnot(0, 1) 34 | 35 | print(mps.wavefunction) 36 | # Displays [0.70710677+0.j 0. +0.j 0. +0.j 0.70710677+0.j] 37 | ``` 38 | 39 | # Two-qubit gate options 40 | 41 | The number of singular values kept for a two-qubit gate can be set by the keyword argument `maxsvals`. 42 | 43 | The following program prepares the same Bell state but only keeps one singular value after the CNOT. 44 | 45 | ```python 46 | import mpsim 47 | 48 | mps = mpsim.MPS(nqudits=2) 49 | mps.h(0) 50 | mps.cnot(0, 1, maxsvals=1) 51 | 52 | print(mps.wavefunction) 53 | # Displays [0.70710674+0.j 0. +0.j 0. +0.j 0. +0.j] 54 | ``` 55 | 56 | Note that the wavefunction after truncation is not normalized. An `MPS` can be renormalized at any time by calling the 57 | `MPS.renormalize()` method. 58 | 59 | # Cirq integration 60 | 61 | Circuits defined in [Cirq](https://github.com/quantumlib/Cirq) can be simulated with MPS as follows. 62 | 63 | ```python 64 | import cirq 65 | from mpsim.mpsim_cirq.simulator import MPSimulator 66 | 67 | # Define the circuit 68 | qreg = cirq.LineQubit.range(2) 69 | circ = cirq.Circuit( 70 | cirq.ops.H.on(qreg[0]), 71 | cirq.ops.CNOT(*qreg) 72 | ) 73 | 74 | # Do the simulation using the MPS Simulator 75 | sim = MPSimulator() 76 | mps = sim.simulate(circ) 77 | print(mps.wavefunction) 78 | # Displays [0.70710677+0.j 0. +0.j 0. +0.j 0.70710677+0.j] 79 | ``` 80 | 81 | One can truncate singular values for two-qubit operations by passing in options to the `MPSimulator`. 82 | 83 | ```python 84 | sim = MPSimulator(options={"maxsvals": 1}) 85 | mps = sim.simulate(circ) 86 | print(mps.wavefunction) 87 | # Displays [0.70710674+0.j 0. +0.j 0. +0.j 0. +0.j] 88 | ``` 89 | 90 | See `help(MPSimulator)` for a full list of options. 91 | 92 | 93 | -------------------------------------------------------------------------------- /development_requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | cirq~=0.8.0 3 | -------------------------------------------------------------------------------- /mpsim/__init__.py: -------------------------------------------------------------------------------- 1 | from mpsim.core import MPS, MPSOperation 2 | from mpsim.gates import ( 3 | igate, 4 | xgate, 5 | ygate, 6 | zgate, 7 | hgate, 8 | rgate, 9 | cnot, 10 | cphase, 11 | swap 12 | ) 13 | -------------------------------------------------------------------------------- /mpsim/core.py: -------------------------------------------------------------------------------- 1 | """Defines matrix product state class.""" 2 | 3 | from copy import deepcopy 4 | from typing import Dict, List, Optional, Sequence, Tuple, Union 5 | 6 | import numpy as np 7 | import tensornetwork as tn 8 | 9 | from mpsim.gates import ( 10 | hgate, rgate, xgate, cnot, swap, is_unitary, is_hermitian, 11 | computational_basis_state, haar_random_unitary 12 | ) 13 | 14 | BITSTRING = Union[Sequence[int], str] 15 | 16 | 17 | class CannotConvertToMPSOperation(Exception): 18 | pass 19 | 20 | 21 | class MPSOperation: 22 | """Defines an operation which can act on a matrix product state.""" 23 | def __init__( 24 | self, 25 | node: tn.Node, 26 | qudit_indices: Union[int, Tuple[int, ...]], 27 | qudit_dimension: int = 2 28 | ) -> None: 29 | """Initialize an MPS Operation. 30 | 31 | Args: 32 | node: TensorNetwork node object representing a gate to apply. 33 | See Notes below. 34 | qudit_indices: Index/indices of qudits to apply the gate to. 35 | In an MPS, qudits are indexed from the left starting at zero. 36 | qudit_dimension: Dimension of qudit(s) to which the MPS Operation 37 | is applied. Default value is 2 (for qubits). 38 | 39 | Notes: 40 | The following gate edge conventions describe which edges connect 41 | to which MPS tensors in one-qudit and two-qudit MPS operations. 42 | 43 | For single qudit gates: 44 | gate edge 1: Connects to the tensor in the MPS. 45 | gate edge 0: Becomes the free edge of the new MPS tensor. 46 | 47 | For two qudit gates: 48 | 49 | Let `matrix` be a 4x4 (unitary) matrix which acts on MPS tensors 50 | at index1 and index2. 51 | 52 | >>> matrix = np.reshape(matrix, newshape=(2, 2, 2, 2)) 53 | >>> gate = tn.Node(matrix) 54 | 55 | ensures the edge convention below is upheld. 56 | 57 | Gate edge convention (assuming index1 < index2) 58 | gate edge 2: Connects to tensor at indexA. 59 | gate edge 3: Connects to tensor at indexB. 60 | gate edge 0: Becomes free index of new tensor at indexA. 61 | gate edge 1: Becomes free index of new tensor at indexB. 62 | 63 | If index1 > index2, 0 <--> 1 and 2 <--> 3. 64 | """ 65 | self._node = node 66 | if isinstance(qudit_indices, int): 67 | qudit_indices = (qudit_indices,) 68 | self._qudit_indices = tuple(qudit_indices) 69 | self._qudit_dimension = int(qudit_dimension) 70 | 71 | @property 72 | def qudit_indices(self) -> Tuple[int, ...]: 73 | """Returns the indices of the qudits that the MPS Operation acts on.""" 74 | return self._qudit_indices 75 | 76 | @property 77 | def qudit_dimension(self) -> int: 78 | """Returns the dimension of the qudits the MPS Operation acts on.""" 79 | return self._qudit_dimension 80 | 81 | @property 82 | def num_qudits(self) -> int: 83 | """Returns the number of qubits the MPS Operation acts on.""" 84 | return len(self._qudit_indices) 85 | 86 | def node(self, copy: bool = True) -> tn.Node: 87 | """Returns the node of the MPS Operation. 88 | 89 | Args: 90 | copy: If True, a copy of the node object is returned. 91 | """ 92 | if not copy: 93 | return self._node 94 | node_dict, _ = tn.copy([self._node]) 95 | return node_dict[self._node] 96 | 97 | def tensor(self, reshape_to_square_matrix: bool = True) -> np.ndarray: 98 | """Returns a copy of the tensor of the MPS Operation. 99 | 100 | Args: 101 | reshape_to_square_matrix: If True, the shape of the returned tensor 102 | is dim x dim where dim is the qudit dimension raised 103 | to the number of qudits that the MPS Operator acts on. 104 | """ 105 | tensor = deepcopy(self._node.tensor) 106 | if reshape_to_square_matrix: 107 | dim = self._qudit_dimension ** self.num_qudits 108 | tensor = np.reshape( 109 | tensor, newshape=(dim, dim) 110 | ) 111 | return tensor 112 | 113 | def is_valid(self) -> bool: 114 | """Returns True if the MPS Operation is valid, else False. 115 | 116 | A valid MPS Operation meets the following criteria: 117 | (1) Tensor of gate has shape (d, ..., d) where d is the qudit 118 | dimension and there are 2 * num_qudits entries in the tuple. 119 | (2) Tensor has 2n free edges where n = number of qudits. 120 | (3) All tensor edges are free edges. 121 | """ 122 | d = self._qudit_dimension 123 | if not self._node.tensor.shape == tuple([d] * 2 * self.num_qudits): 124 | return False 125 | if not len(self._node.get_all_edges()) == 2 * self.num_qudits: 126 | return False 127 | if self._node.has_nondangling_edge(): 128 | return False 129 | return True 130 | 131 | def is_unitary(self) -> bool: 132 | """Returns True if the MPS Operation is unitary, else False. 133 | 134 | An MPS Operation is unitary if its gate tensor U is unitary, i.e. if 135 | U^dag @ U = U @ U^dag = I. 136 | """ 137 | return is_unitary(self.tensor(reshape_to_square_matrix=True)) 138 | 139 | def is_hermitian(self) -> bool: 140 | """Returns True if the MPS Operation is Hermitian, else False. 141 | 142 | An MPS Operation is Hermitian if its gate tensor M is Hermitian, i.e. 143 | if M^dag = M. 144 | """ 145 | return is_hermitian(self.tensor(reshape_to_square_matrix=True)) 146 | 147 | def is_single_qudit_operation(self) -> bool: 148 | """Returns True if the MPS Operation acts on a single qudit.""" 149 | return self.num_qudits == 1 150 | 151 | def is_two_qudit_operation(self) -> bool: 152 | """Returns True if the MPS Operation acts on two qudits.""" 153 | return self.num_qudits == 2 154 | 155 | def __str__(self): 156 | return f"Tensor {self._node.name} on qudit(s) {self._qudit_indices}." 157 | 158 | 159 | class MPS: 160 | """Matrix Product State (MPS) object.""" 161 | def __init__( 162 | self, 163 | nqudits: int, 164 | qudit_dimension: int = 2, 165 | tensor_prefix: str = "q" 166 | ) -> None: 167 | """Initializes an MPS of qudits in the ground (all-zero) state. 168 | 169 | The MPS has the following structure (shown for six qudits): 170 | 171 | @ ---- @ ---- @ ---- @ ---- @ ---- @ 172 | | | | | | | 173 | 174 | Virtual indices have bond dimension one (initially) and physical indices 175 | have bond dimension equal to the qudit_dimension. 176 | 177 | Args: 178 | nqudits: Number of qubits in the all zero state. 179 | qudit_dimension: Dimension of qudits. Default value is 2 (qubits). 180 | tensor_prefix: Prefix for tensor names. 181 | The full name is prefix + numerical index, numbered from 182 | left to right starting with zero. 183 | """ 184 | if nqudits < 2: 185 | raise ValueError( 186 | f"Number of qudits must be greater than 2 but is {nqudits}." 187 | ) 188 | 189 | # Set nodes on the interior 190 | nodes = [ 191 | tn.Node( 192 | np.array( 193 | [[[1.0]], *[[[0]]] * (qudit_dimension - 1)], 194 | dtype=np.complex64 195 | ), 196 | name=tensor_prefix + str(i + 1), 197 | ) 198 | for i in range(nqudits - 2) 199 | ] 200 | 201 | # Set nodes on the left and right edges 202 | nodes.insert( 203 | 0, 204 | tn.Node( 205 | np.array( 206 | [[1.0], *[[0]] * (qudit_dimension - 1)], dtype=np.complex64 207 | ), 208 | name=tensor_prefix + str(0), 209 | ), 210 | ) 211 | nodes.append( 212 | tn.Node( 213 | np.array( 214 | [[1.0], *[[0]] * (qudit_dimension - 1)], dtype=np.complex64 215 | ), 216 | name=tensor_prefix + str(nqudits - 1), 217 | ) 218 | ) 219 | 220 | # Connect edges between interior nodes 221 | for i in range(1, nqudits - 2): 222 | tn.connect(nodes[i].get_edge(2), nodes[i + 1].get_edge(1)) 223 | 224 | # Connect edge nodes to their neighbors 225 | if nqudits < 3: 226 | tn.connect(nodes[0].get_edge(1), nodes[1].get_edge(1)) 227 | else: 228 | tn.connect(nodes[0].get_edge(1), nodes[1].get_edge(1)) 229 | tn.connect(nodes[-1].get_edge(1), nodes[-2].get_edge(2)) 230 | 231 | self._nqudits = nqudits 232 | self._qudit_dimension = qudit_dimension 233 | self._prefix = tensor_prefix 234 | self._nodes = nodes 235 | self._max_bond_dimensions = [ 236 | self._qudit_dimension ** (i + 1) for i in range(self._nqudits // 2) 237 | ] 238 | self._max_bond_dimensions += list(reversed(self._max_bond_dimensions)) 239 | if self._nqudits % 2 == 0: 240 | self._max_bond_dimensions.remove( 241 | self._qudit_dimension ** (self._nqudits // 2) 242 | ) 243 | self._norms = [] # type: List[float] 244 | 245 | @staticmethod 246 | def from_wavefunction( 247 | wavefunction: np.ndarray, 248 | nqudits: int, 249 | qudit_dimension: int = 2, 250 | tensor_prefix: str = "q" 251 | ) -> 'MPS': 252 | """Returns an MPS constructed from the initial wavefunction. 253 | 254 | Args: 255 | wavefunction: Vector (numpy array) representing the wavefunction. 256 | nqudits: Number of qudits in the wavefunction. 257 | qudit_dimension: Dimension of qudits. (Default is 2 for qubits.) 258 | tensor_prefix: Prefix for tensor names. 259 | The full name is prefix + numerical index, numbered from 260 | left to right starting with zero. 261 | 262 | Raises: 263 | TypeError: Wavefunction is not a numpy.ndarray or cannot be 264 | converted to one. 265 | ValueError: If: 266 | * The wavefunction is not one-dimensional (a vector). 267 | * If the number of elements in the wavefunction is not 268 | equal to qudit_dimension ** nqudits. 269 | * nqudits is less than two. 270 | """ 271 | if not isinstance(wavefunction, (list, tuple, np.ndarray)): 272 | raise TypeError("Invalid type for wavefunction.") 273 | wavefunction = np.array(wavefunction) 274 | 275 | if len(wavefunction.shape) != 1: 276 | raise ValueError( 277 | "Invalid shape for wavefunction. Should be a vector." 278 | ) 279 | 280 | if nqudits < 2: 281 | raise ValueError("At least two qudits are required.") 282 | 283 | if wavefunction.size != qudit_dimension ** nqudits: 284 | raise ValueError( 285 | "Mismatch between wavefunction, qudit_dimension, and nqudits. " 286 | f"Expected {qudit_dimension ** nqudits} elements in the " 287 | f"wavefunction, but wavefunction has {wavefunction.size} " 288 | f"elements." 289 | ) 290 | 291 | # Reshape the wavefunction 292 | wavefunction = np.reshape( 293 | wavefunction, newshape=[qudit_dimension] * nqudits 294 | ) 295 | to_split = tn.Node( 296 | wavefunction, axis_names=[str(i) for i in range(nqudits)] 297 | ) 298 | 299 | # Perform SVD across each cut 300 | nodes = [] 301 | for i in range(nqudits - 1): 302 | left_edges = [] 303 | right_edges = [] 304 | for edge in to_split.get_all_dangling(): 305 | if edge.name == str(i): 306 | left_edges.append(edge) 307 | else: 308 | right_edges.append(edge) 309 | if nodes: 310 | for edge in nodes[-1].get_all_nondangling(): 311 | if to_split in edge.get_nodes(): 312 | left_edges.append(edge) 313 | 314 | left_node, right_node, _ = tn.split_node( 315 | to_split, 316 | left_edges, 317 | right_edges, 318 | left_name=tensor_prefix + str(i) 319 | ) 320 | nodes.append(left_node) 321 | to_split = right_node 322 | to_split.name = tensor_prefix + str(nqudits - 1) 323 | nodes.append(to_split) 324 | 325 | # Return the MPS 326 | mps = MPS(nqudits, qudit_dimension, tensor_prefix) 327 | mps._nodes = nodes 328 | return mps 329 | 330 | @property 331 | def nqudits(self) -> int: 332 | """Returns the number of qudits in the MPS.""" 333 | return self._nqudits 334 | 335 | @property 336 | def qudit_dimension(self) -> int: 337 | """Returns the dimension of each qudit in the MPS.""" 338 | return self._qudit_dimension 339 | 340 | def bond_dimension_of(self, node_index: int) -> int: 341 | """Returns the bond dimension of the right edge of the node 342 | at the given index. 343 | 344 | Args: 345 | node_index: Index of the node. 346 | The returned bond dimension is that of the right edge 347 | of the given node. 348 | """ 349 | if not self.is_valid(): 350 | raise ValueError("MPS is invalid.") 351 | 352 | if node_index >= self._nqudits: 353 | raise ValueError( 354 | f"Index should be less than {self._nqudits} but is {node_index}." 355 | ) 356 | 357 | left = self.get_node(node_index, copy=False) 358 | right = self.get_node(node_index + 1, copy=False) 359 | tn.check_connected((left, right)) 360 | edge = tn.get_shared_edges(left, right).pop() 361 | return edge.dimension 362 | 363 | def bond_dimensions(self) -> List[int]: 364 | """Returns the bond dimension of each edge in the MPS.""" 365 | return [self.bond_dimension_of(i) for i in range(self._nqudits - 1)] 366 | 367 | def max_bond_dimension_of(self, edge_index: int) -> int: 368 | """Returns the maximum bond dimension of the right edge 369 | of the node at the given index. 370 | 371 | Args: 372 | edge_index: Index of the node. 373 | The returned bond dimension is that of the right edge 374 | of the given node. 375 | Negative indices count backwards from the right of the MPS. 376 | """ 377 | if edge_index >= self._nqudits: 378 | raise ValueError( 379 | f"Edge index should be less than {self._nqudits} " 380 | f"but is {edge_index}." 381 | ) 382 | return self._max_bond_dimensions[edge_index] 383 | 384 | def max_bond_dimensions(self) -> List[int]: 385 | """Returns the maximum bond dimensions of the MPS.""" 386 | return self._max_bond_dimensions 387 | 388 | def is_valid(self) -> bool: 389 | """Returns true if the MPS is valid, else False. 390 | 391 | A valid MPS satisfies the following criteria: 392 | (1) At least two tensors. 393 | (2) Every tensor has exactly one free (dangling) edge. 394 | (3) Every tensor has connected edges to its nearest neighbor(s). 395 | """ 396 | if len(self._nodes) < 2: 397 | return False 398 | 399 | for (i, tensor) in enumerate(self._nodes): 400 | # Exterior nodes 401 | if i == 0 or i == len(self._nodes) - 1: 402 | if len(tensor.get_all_dangling()) != 1: 403 | return False 404 | if len(tensor.get_all_nondangling()) != 1: 405 | return False 406 | # Interior nodes 407 | else: 408 | if len(tensor.get_all_dangling()) != 1: 409 | return False 410 | if len(tensor.get_all_nondangling()) != 2: 411 | return False 412 | 413 | if i < len(self._nodes) - 1: 414 | try: 415 | tn.check_connected((self._nodes[i], self._nodes[i + 1])) 416 | except ValueError: 417 | print(f"Nodes at index {i} and {i + 1} are not connected.") 418 | return False 419 | return True 420 | 421 | def get_nodes(self, copy: bool = True) -> List[tn.Node]: 422 | """Returns the nodes of the MPS. 423 | 424 | Args: 425 | copy: If True, a copy of the nodes are returned, else the actual 426 | nodes are returned. 427 | """ 428 | if not copy: 429 | return self._nodes 430 | nodes_dict, _ = tn.copy(self._nodes) 431 | return list(nodes_dict.values()) 432 | 433 | def get_node(self, node_index: int, copy: bool = True) -> tn.Node: 434 | """Returns the ith node in the MPS counting from the left. 435 | 436 | Args: 437 | node_index: Index of node to get. 438 | copy: If true, a copy of the node is returned, 439 | else the actual node is returned. 440 | """ 441 | return self.get_nodes(copy)[node_index] 442 | 443 | def get_free_edge_of(self, node_index: int, copy: bool = True) -> tn.Edge: 444 | """Returns the free (dangling) edge of a node with specified index. 445 | 446 | Args: 447 | node_index: Specifies the node. 448 | copy: If True, returns a copy of the edge. 449 | If False, returns the actual edge. 450 | """ 451 | return self.get_node(node_index, copy).get_all_dangling().pop() 452 | 453 | def get_left_connected_edge_of( 454 | self, node_index: int 455 | ) -> Union[tn.Edge, None]: 456 | """Returns the left connected edge of the specified node, if it exists. 457 | 458 | Args: 459 | node_index: Index of node to get the left connected edge of. 460 | """ 461 | if node_index == 0: 462 | return None 463 | 464 | return tn.get_shared_edges( 465 | self._nodes[node_index], self._nodes[node_index - 1] 466 | ).pop() 467 | 468 | def get_right_connected_edge_of( 469 | self, node_index: int 470 | ) -> Union[tn.Edge, None]: 471 | """Returns the left connected edge of the specified node, if it exists. 472 | 473 | Args: 474 | node_index: Index of node to get the right connected edge of. 475 | """ 476 | if node_index == self._nqudits - 1: 477 | return None 478 | 479 | return tn.get_shared_edges( 480 | self._nodes[node_index], self._nodes[node_index + 1] 481 | ).pop() 482 | 483 | def wavefunction(self) -> np.array: 484 | """Returns the wavefunction of the MPS as a vector.""" 485 | if not self.is_valid(): 486 | raise ValueError("MPS is not valid.") 487 | 488 | # Replicate the mps 489 | nodes = self.get_nodes(copy=True) 490 | fin = nodes.pop(0) 491 | for node in nodes: 492 | fin = tn.contract_between(fin, node) 493 | 494 | # Make sure all edges are free 495 | if set(fin.get_all_dangling()) != set(fin.get_all_edges()): 496 | raise ValueError("Invalid MPS.") 497 | 498 | return np.reshape( 499 | fin.tensor, newshape=(self._qudit_dimension ** self._nqudits) 500 | ) 501 | 502 | def dagger(self): 503 | """Takes the dagger (conjugate transpose) of the MPS.""" 504 | for i in range(self._nqudits): 505 | self._nodes[i].set_tensor(np.conj(self._nodes[i].tensor)) 506 | 507 | def inner_product(self, other: 'MPS') -> np.complex: 508 | """Returns the inner product between self and other computed by 509 | contraction. Mathematically, the inner product is . 510 | 511 | Args: 512 | other: Other MPS to take inner product with. This MPS is the "ket" 513 | in the inner product . 514 | 515 | Raises: 516 | ValueError: If: 517 | * Number of qudits don't match in both MPS. 518 | * Qudit dimensions don't match in both MPS. 519 | * Either MPS is invalid 520 | """ 521 | if other._nqudits != self._nqudits: 522 | raise ValueError( 523 | f"Cannot compute inner product between self which has " 524 | f"{self._nqudits} qudits and other which has {other._nqudits} " 525 | f"qudits." 526 | "\nNumber of qudits must be equal." 527 | ) 528 | 529 | if other._qudit_dimension != self._qudit_dimension: 530 | raise ValueError( 531 | f"Cannot compute inner product between self which has qudit" 532 | f"dimension {self._qudit_dimension} and other which as qudit" 533 | f"dimension {other._qudit_dimension}." 534 | "Qudit dimensions must be equal." 535 | ) 536 | 537 | if not self.is_valid(): 538 | raise ValueError("MPS is invalid.") 539 | 540 | if not other.is_valid(): 541 | raise ValueError("Other MPS is invalid.") 542 | 543 | a = self.get_nodes(copy=True) 544 | b = other.get_nodes(copy=True) 545 | for n in b: 546 | n.set_tensor(np.conj(n.tensor)) 547 | 548 | for i in range(self._nqudits): 549 | tn.connect( 550 | a[i].get_all_dangling().pop(), b[i].get_all_dangling().pop() 551 | ) 552 | 553 | for i in range(self._nqudits - 1): 554 | # TODO: Optimize by flattening edges 555 | mid = tn.contract_between(a[i], b[i]) 556 | new = tn.contract_between(mid, a[i + 1]) 557 | a[i + 1] = new 558 | 559 | fin = tn.contract_between(a[-1], b[-1]) 560 | assert len(fin.edges) == 0 # Debug check 561 | return np.complex(fin.tensor) 562 | 563 | def norm(self) -> float: 564 | """Returns the norm of the MPS computed by contraction.""" 565 | return np.sqrt(self.inner_product(self).real) 566 | 567 | def renormalize(self, to_norm: float = 1.0) -> None: 568 | """Renormalizes the MPS. 569 | 570 | Args: 571 | to_norm: The new norm of the MPS. 572 | 573 | Raises: 574 | ValueError: If to_norm is negative or too close to zero, or if 575 | the current norm of the MPS is too close to zero. 576 | """ 577 | if to_norm < 0.: 578 | raise ValueError(f"Arg to_norm must be positive but is {to_norm}") 579 | 580 | if np.isclose(to_norm, 0., atol=1e-15): 581 | raise ValueError( 582 | f"Arg to_norm = {to_norm} is too close to numerical zero." 583 | ) 584 | 585 | if np.isclose(self.norm(), 0., atol=1e-15): 586 | raise ValueError( 587 | "Norm of MPS is numerically zero, cannot renormalize." 588 | ) 589 | 590 | norm = self.norm() 591 | for i, node in enumerate(self._nodes): 592 | self._nodes[i].set_tensor( 593 | (to_norm / norm)**(1 / self.nqudits) * node.tensor 594 | ) 595 | 596 | def reduced_density_matrix( 597 | self, 598 | node_indices: Union[int, Sequence[int]] 599 | ) -> np.ndarray: 600 | """Computes the reduced density matrix of the MPS on the given nodes. 601 | 602 | Args: 603 | node_indices: Node index, or list of node indices, to keep. 604 | Indices not in node_indices are traced out. 605 | 606 | Raises: 607 | ValueError: If the node_indices contain duplicate indices. 608 | IndexError: If the indices are out of bounds for the MPS. 609 | """ 610 | try: 611 | node_indices = iter(node_indices) 612 | except TypeError: 613 | node_indices = [node_indices] 614 | node_indices = tuple(node_indices) 615 | 616 | if len(set(node_indices)) < len(node_indices): 617 | raise ValueError("Node indices contains duplicates.") 618 | 619 | if min(node_indices) < 0 or max(node_indices) > self._nqudits - 1: 620 | raise IndexError("One or more invalid node indices.") 621 | 622 | ket = self.copy() 623 | bra = self.copy() 624 | bra.dagger() 625 | ket_edges = [node.get_all_dangling().pop() for node in ket._nodes] 626 | bra_edges = [node.get_all_dangling().pop() for node in bra._nodes] 627 | 628 | # Store ordered free edges to reorder edges in the final tensor 629 | ket_free_edges = [] 630 | bra_free_edges = [] 631 | for i in node_indices: 632 | ket_free_edges.append(ket_edges[i]) 633 | bra_free_edges.append(bra_edges[i]) 634 | 635 | for i in range(self._nqudits): 636 | # If this node is not in node_indices, trace it out 637 | if i not in node_indices: 638 | _ = tn.connect(ket_edges[i], bra_edges[i]) 639 | 640 | # Contract while allowing outer product for unconnected nodes 641 | mid = tn.contract_between( 642 | ket._nodes[i], bra._nodes[i], allow_outer_product=True 643 | ) 644 | 645 | if i < self._nqudits - 1: 646 | new = tn.contract_between(mid, ket._nodes[i + 1]) 647 | ket._nodes[i + 1] = new 648 | 649 | mid.reorder_edges(ket_free_edges + bra_free_edges) 650 | n = len(node_indices) 651 | d = self._qudit_dimension 652 | return np.reshape(mid.tensor, newshape=(d**n, d**n)) 653 | 654 | def _sample(self, as_string: bool = False) -> BITSTRING: 655 | """Returns a string of measured states at each site 656 | by sampling once from the MPS. 657 | 658 | Args: 659 | as_string: If True, each measurement result is a string instead of 660 | a list of integers. 661 | """ 662 | string = [] 663 | states = list(range(self._qudit_dimension)) 664 | copy = self.copy() 665 | for i in range(self._nqudits): 666 | qubit = self.reduced_density_matrix(i).diagonal().real 667 | string.append(np.random.choice(states, size=1, p=qubit)[0]) 668 | state = computational_basis_state( 669 | string[-1], dim=self._qudit_dimension 670 | ) 671 | edge = tn.connect( 672 | copy._nodes[i].get_all_dangling().pop(), 673 | state.get_all_dangling().pop() 674 | ) 675 | mid = tn.contract(edge) 676 | if i < self._nqudits - 1: 677 | new = tn.contract_between(mid, copy._nodes[i + 1]) 678 | copy._nodes[i + 1] = new 679 | 680 | if as_string: 681 | return "".join(str(bit) for bit in string) 682 | return string 683 | 684 | def sample( 685 | self, nsamples: int, as_hist: bool = False, as_string: bool = False 686 | ) -> Union[Dict[BITSTRING, int], BITSTRING]: 687 | """Samples from the MPS, returning a list of (bit)strings of measured 688 | states on each site. 689 | 690 | Args: 691 | nsamples: Number of times to sample from the MPS. 692 | as_hist: If True, a histogram of measurement results is returned. 693 | If True, as_string is set to True. 694 | as_string: If True, each measurement result is a string instead of 695 | a list of integers. 696 | 697 | Raises: ValueError: If nsamples is negative or non-integer. 698 | """ 699 | # TODO: If self._nqudits is small enough, this can be significantly sped 700 | # up by computing the wavefunction. 701 | if not isinstance(nsamples, int): 702 | raise ValueError( 703 | f"Arg nsamples should be an int but is a {type(nsamples)}." 704 | ) 705 | if nsamples <= 0: 706 | raise ValueError( 707 | f"Arg nsamples should be positive but is {nsamples}." 708 | ) 709 | if as_hist: 710 | as_string = True 711 | 712 | raw = [self._sample(as_string) for _ in range(nsamples)] 713 | if as_hist: 714 | hist = {} 715 | for bitstring in raw: 716 | if bitstring not in hist.keys(): 717 | hist[bitstring] = 1 718 | else: 719 | hist[bitstring] += 1 720 | return hist 721 | return raw 722 | 723 | def expectation(self, observable: MPSOperation) -> float: 724 | """Returns the expectation value of an observable . 725 | 726 | Args: 727 | observable: Hermitian operator expressed as an MPSOperation. 728 | Example: 729 | To compute the expectation of H \otimes I on a two-qubit MPS 730 | 731 | >>> observable = MPSOperation(mpsim.hgate(), 0) 732 | >>> mps.expectation(observable) 733 | 734 | Raises: 735 | ValueError: If the observable is not Hermitian. 736 | """ 737 | if not observable.is_hermitian(): 738 | raise ValueError("Observable is not Hermitian.") 739 | 740 | if observable.qudit_dimension != self._qudit_dimension: 741 | obs_dim = observable.qudit_dimension 742 | mps_dim = self._qudit_dimension 743 | raise ValueError( 744 | f"Dimension mismatch between observable and MPS. " 745 | f"Observable is ({obs_dim}, {obs_dim}) but MPS has qudit " 746 | f"dimension {mps_dim}." 747 | ) 748 | 749 | mps_copy = self.copy() 750 | mps_copy.apply(observable) 751 | return self.inner_product(mps_copy).real 752 | 753 | def apply_one_qudit_gate( 754 | self, 755 | gate: tn.Node, 756 | node_index: int, 757 | **kwargs 758 | ) -> None: 759 | """Applies a single qubit gate to a specified node. 760 | 761 | Args: 762 | gate: Single qubit gate to apply. A tensor with two free indices. 763 | node_index: Index of tensor (qubit) in the MPS to apply 764 | the single qubit gate to. 765 | 766 | Keyword Args: 767 | ortho_after_non_unitary (bool): If True, orthonormalizes edge(s) 768 | of the node after applying a non-unitary gate. 769 | renormalize_after_non_unitary (bool): If True, renormalize the MPS 770 | after applying a non-unitary gate. 771 | 772 | Notes: 773 | Edge convention. 774 | Gate edge 1: Connects to MPS node. 775 | Gate edge 0: Becomes free edge of new MPS node. 776 | 777 | Raises: 778 | ValueError: 779 | On invalid MPS, invalid index, invalid gate, and edge dimension 780 | mismatch between gate edges and MPS qudit edges. 781 | """ 782 | if not self.is_valid(): 783 | raise ValueError("MPS is invalid.") 784 | 785 | if node_index not in range(self._nqudits): 786 | raise ValueError( 787 | f"Input tensor index={node_index} is out of bounds for" 788 | f" an MPS on {self._nqudits} qudits." 789 | ) 790 | 791 | if (len(gate.get_all_dangling()) != 2 792 | or len(gate.get_all_nondangling()) != 0): 793 | raise ValueError( 794 | "Single qudit gate must have two free edges" 795 | " and zero connected edges." 796 | ) 797 | 798 | if gate.get_edge(0).dimension != gate.get_edge(1).dimension: 799 | raise ValueError("Gate edge dimensions must be equal.") 800 | 801 | if gate.get_edge(0).dimension != self._qudit_dimension: 802 | raise ValueError( 803 | f"Gate edges have dimension {gate.get_edge(0).dimension} " 804 | f"but should have MPS qudit dimension = {self._qudit_dimension}" 805 | ) 806 | 807 | # Parse the keyword arguments 808 | renormalize_after_non_unitary = True 809 | ortho_after_non_unitary = True 810 | if kwargs.get("renormalize_after_non_unitary") is False: 811 | renormalize_after_non_unitary = False 812 | if kwargs.get("ortho_after_non_unitary") is False: 813 | ortho_after_non_unitary = False 814 | 815 | # Store the norm for optional renormalization after non-unitary gate 816 | if not is_unitary(gate) and renormalize_after_non_unitary: 817 | norm = self.norm() 818 | 819 | # Connect the MPS and gate edges 820 | mps_edge = list(self._nodes[node_index].get_all_dangling())[0] 821 | gate_edge = gate[1] 822 | connected = tn.connect(mps_edge, gate_edge) 823 | 824 | # Contract the edge to get the new tensor 825 | new = tn.contract(connected, name=self._nodes[node_index].name) 826 | self._nodes[node_index] = new 827 | 828 | # Optional orthonormalization after a non-unitary gate 829 | if not is_unitary(gate) and ortho_after_non_unitary: 830 | # Edge case: Left-most node 831 | if node_index == 0: 832 | self.orthonormalize_right_edge_of(node_index) 833 | 834 | # Edge case: Right-most node 835 | elif node_index == self._nqudits - 1: 836 | self.orthonormalize_left_edge_of(node_index) 837 | 838 | # General case 839 | else: 840 | self.orthonormalize_right_edge_of(node_index) 841 | self.orthonormalize_left_edge_of(node_index) 842 | 843 | # Optional renormalization after non-unitary gate 844 | if not is_unitary(gate) and renormalize_after_non_unitary: 845 | self.renormalize(norm) 846 | 847 | def orthonormalize_right_edge_of( 848 | self, node_index: int, threshold: float = 1e-8 849 | ) -> None: 850 | """Performs SVD on the specified node to orthonormalize the right edge. 851 | 852 | Let N be the specified node. Then, this function: 853 | (1) Performs SVD on N to get N = U @ S @ Vdag, 854 | (2) Sets the node at N to be U, and 855 | (3) Sets the node to the right of N (call it M) to be S @ Vdag @ M. 856 | 857 | Args: 858 | node_index: Index which specifies the node. 859 | threshold: Throw away singular values below threshold * self.norm(). 860 | 861 | Raises: 862 | ValueError: If the node_index is out of bounds for the MPS. 863 | """ 864 | if not 0 <= node_index < self._nqudits - 1: 865 | raise ValueError("Invalid edge index.") 866 | 867 | # Get the node 868 | node = self._nodes[node_index] 869 | 870 | # Get the left and right edges to do the SVD 871 | left_edges = [self.get_free_edge_of(node_index, copy=False)] 872 | if self.get_left_connected_edge_of(node_index): 873 | left_edges.append(self.get_left_connected_edge_of(node_index)) 874 | right_edges = [self.get_right_connected_edge_of(node_index)] 875 | 876 | # Do the SVD 877 | u, s, vdag, _ = tn.split_node_full_svd( 878 | node, 879 | left_edges=left_edges, 880 | right_edges=right_edges, 881 | max_truncation_err=threshold * self.norm(), 882 | ) 883 | 884 | # Set the new node 885 | self._nodes[node_index] = u 886 | self._nodes[node_index].name = self._prefix + str(node_index) 887 | 888 | # Mutlipy S and Vdag to the right 889 | temp = tn.contract_between(s, vdag) 890 | new_right = tn.contract_between(temp, self._nodes[node_index + 1]) 891 | new_right.name = self._nodes[node_index + 1].name 892 | self._nodes[node_index + 1] = new_right 893 | 894 | def orthonormalize_left_edge_of( 895 | self, node_index: int, threshold: float = 1e-8 896 | ) -> None: 897 | """Performs SVD on the specified node to orthonormalize the left edge. 898 | 899 | Let M be the specified node. Then, this function: 900 | (1) Performs SVD on M to get M = U @ S @ Vdag, 901 | (2) Sets the node at M to be Vdag, and 902 | (3) Sets the node to the left of M (call it N) to be N @ U @ S. 903 | 904 | Args: 905 | node_index: Index which specifies the node. 906 | threshold: Throw away singular values below threshold * self.norm(). 907 | 908 | Raises: 909 | ValueError: If the node_index is out of bounds for the MPS. 910 | """ 911 | if not 0 < node_index <= self._nqudits - 1: 912 | raise ValueError("Invalid edge index.") 913 | 914 | # Get the node 915 | node = self._nodes[node_index] 916 | 917 | # Get the left and right edges to do the SVD 918 | left_edges = [self.get_left_connected_edge_of(node_index)] 919 | right_edges = [self.get_free_edge_of(node_index, copy=False)] 920 | if self.get_right_connected_edge_of(node_index): 921 | right_edges.append(self.get_right_connected_edge_of(node_index)) 922 | 923 | # Do the SVD 924 | u, s, vdag, _ = tn.split_node_full_svd( 925 | node, 926 | left_edges=left_edges, 927 | right_edges=right_edges, 928 | max_truncation_err=threshold * self.norm(), 929 | ) 930 | 931 | # Set the new node 932 | self._nodes[node_index] = vdag 933 | self._nodes[node_index].name = self._prefix + str(node_index) 934 | 935 | # Mutlipy U and S to the left 936 | temp = tn.contract_between(u, s) 937 | new_left = tn.contract_between(self._nodes[node_index - 1], temp) 938 | new_left.name = self._nodes[node_index - 1].name 939 | self._nodes[node_index - 1] = new_left 940 | 941 | def apply_one_qudit_gate_to_all(self, gate: tn.Node) -> None: 942 | """Applies a single qudit gate to all tensors in the MPS. 943 | 944 | Args: 945 | gate: Single qudit gate to apply. A tensor with two free indices. 946 | """ 947 | for i in range(self._nqudits): 948 | self.apply_one_qudit_gate(gate, i) 949 | 950 | def apply_two_qudit_gate( 951 | self, gate: tn.Node, node_index1: int, node_index2: int, **kwargs 952 | ) -> None: 953 | """Applies a two qubit gate to the specified nodes. 954 | 955 | Args: 956 | gate: Two qubit gate to apply. See Notes for the edge convention. 957 | node_index1: Index of first node in the MPS the gate acts on. 958 | node_index2: Index of second node in the MPS the gate acs on. 959 | 960 | Keyword Arguments: 961 | keep_left_canonical: After performing an SVD on the new node to 962 | obtain U, S, and Vdag, S is grouped with Vdag to form the 963 | new right tensor. That is, the left tensor is U, and the 964 | right tensor is S @ Vdag. This keeps the MPS in left canonical 965 | form if it already was in left canonical form. 966 | 967 | If False, S is grouped with U so that the new left tensor 968 | is U @ S and the new right tensor is Vdag. 969 | 970 | maxsvals (int): Number of singular values to keep 971 | for all two-qubit gates. 972 | 973 | fraction (float): Number of singular values to keep expressed as a 974 | fraction of the maximum bond dimension. 975 | Must be between 0 and 1, inclusive. 976 | 977 | Notes: 978 | The following gate edge convention is used to connect gate edges to 979 | MPS edges. Let `matrix` be a 4x4 (unitary) matrix. Then, 980 | 981 | >>> matrix = np.reshape(matrix, newshape=(2, 2, 2, 2)) 982 | >>> gate = tn.Node(matrix) 983 | 984 | ensures the edge convention below is satisfied. 985 | 986 | Gate edge convention (assuming indexA < indexB) 987 | gate edge 2: Connects to tensor at indexA. 988 | gate edge 3: Connects to tensor at indexB. 989 | gate edge 0: Becomes free index of new tensor at indexA. 990 | gate edge 1: Becomes free index of new tensor at indexB. 991 | 992 | If indexA > indexB, 0 <--> 1 and 2 <--> 3. 993 | 994 | Raises: 995 | ValueError: On the following: 996 | * Invalid MPS. 997 | * Invalid indices (equal or out of bounds). 998 | * Invalid two-qudit gate. 999 | """ 1000 | if not self.is_valid(): 1001 | raise ValueError("MPS is not valid.") 1002 | 1003 | if (node_index1 not in range(self._nqudits) 1004 | or node_index2 not in range(self.nqudits)): 1005 | raise ValueError( 1006 | f"Input tensor indices={(node_index1, node_index2)} are out of " 1007 | f"bounds for an MPS on {self._nqudits} qudits." 1008 | ) 1009 | 1010 | if node_index1 == node_index2: 1011 | raise ValueError("Node indices cannot be identical.") 1012 | 1013 | if (len(gate.get_all_dangling()) != 4 1014 | or len(gate.get_all_nondangling()) != 0): 1015 | raise ValueError( 1016 | "Two qubit gate must have four free edges" 1017 | " and zero connected edges." 1018 | ) 1019 | 1020 | edge_dimensions = set([edge.dimension for edge in gate.edges]) 1021 | if len(edge_dimensions) != 1: 1022 | raise ValueError("All gate edges must have the same dimension.") 1023 | 1024 | if edge_dimensions.pop() != self._qudit_dimension: 1025 | raise ValueError( 1026 | f"Gate edges have dimension {gate.get_edge(0).dimension} " 1027 | f"but should have MPS qudit dimension = {self._qudit_dimension}" 1028 | ) 1029 | 1030 | # Flip the "control"/"target" gate edges and tensor edges if needed 1031 | if node_index2 < node_index1: 1032 | gate.reorder_edges([gate[1], gate[0], gate[3], gate[2]]) 1033 | node_index1, node_index2 = node_index2, node_index1 1034 | 1035 | # Swap tensors until adjacent if necessary 1036 | invert_swap_network = False 1037 | if node_index1 < node_index2 - 1: 1038 | invert_swap_network = True 1039 | original_index1 = node_index1 1040 | self.move_node_from_left_to_right( 1041 | node_index1, node_index2 - 1, **kwargs 1042 | ) 1043 | node_index1 = node_index2 - 1 1044 | 1045 | # Connect the MPS tensors to the gate edges 1046 | _ = tn.connect( 1047 | self.get_free_edge_of(node_index=node_index1, copy=False), 1048 | gate.get_edge(2) 1049 | ) 1050 | _ = tn.connect( 1051 | self.get_free_edge_of(node_index=node_index2, copy=False), 1052 | gate.get_edge(3) 1053 | ) 1054 | 1055 | # Store the free edges of the gate 1056 | left_gate_edge = gate.get_edge(0) 1057 | right_gate_edge = gate.get_edge(1) 1058 | 1059 | # Contract the tensors in the MPS 1060 | new_node = tn.contract_between( 1061 | self._nodes[node_index1], self._nodes[node_index2] 1062 | ) 1063 | 1064 | # Flatten the two edges from the MPS node to the gate node 1065 | node_gate_edge = tn.flatten_edges_between(new_node, gate) 1066 | 1067 | # Contract the flattened edge to get a new single MPS node 1068 | new_node = tn.contract(node_gate_edge) 1069 | 1070 | # Get the left and right connected edges (if any) 1071 | left_connected_edge = None 1072 | right_connected_edge = None 1073 | for connected_edge in new_node.get_all_nondangling(): 1074 | if self._prefix in connected_edge.node1.name: 1075 | # Use the "node1" node by default 1076 | index = int(connected_edge.node1.name.split(self._prefix)[-1]) 1077 | else: 1078 | # If "node1" is the new_mps_node, use "node2" 1079 | index = int(connected_edge.node2.name.split(self._prefix)[-1]) 1080 | 1081 | # Get the connected edges (if any) 1082 | if index <= node_index1: 1083 | left_connected_edge = connected_edge 1084 | else: 1085 | right_connected_edge = connected_edge 1086 | 1087 | # ================================================ 1088 | # Do the SVD to split the single MPS node into two 1089 | # ================================================ 1090 | # Get the left and right free edges from the original gate 1091 | left_free_edge = left_gate_edge 1092 | right_free_edge = right_gate_edge 1093 | 1094 | # Group the left (un)connected and right (un)connected edges 1095 | left_edges = [ 1096 | edge for edge in (left_free_edge, left_connected_edge) 1097 | if edge is not None 1098 | ] 1099 | right_edges = [ 1100 | edge for edge in (right_free_edge, right_connected_edge) 1101 | if edge is not None 1102 | ] 1103 | 1104 | # Options for canonicalization + truncation 1105 | if "keep_left_canonical" in kwargs.keys(): 1106 | keep_left_canonical = kwargs.get("keep_left_canonical") 1107 | else: 1108 | keep_left_canonical = True 1109 | 1110 | if "fraction" in kwargs.keys() and "maxsvals" in kwargs.keys(): 1111 | raise ValueError( 1112 | "Only one of (fraction, maxsvals) can be provided as kwargs." 1113 | ) 1114 | 1115 | if "fraction" in kwargs.keys(): 1116 | fraction = kwargs.get("fraction") 1117 | if not (0 <= fraction <= 1): 1118 | raise ValueError( 1119 | "Keyword fraction must be between 0 and 1 but is", fraction 1120 | ) 1121 | maxsvals = int( 1122 | round(fraction * self.max_bond_dimension_of( 1123 | min(node_index1, node_index2) 1124 | )) 1125 | ) 1126 | else: 1127 | maxsvals = None # Keeps all singular values 1128 | 1129 | if "maxsvals" in kwargs.keys(): 1130 | maxsvals = int(kwargs.get("maxsvals")) 1131 | 1132 | u, s, vdag, truncated_svals = tn.split_node_full_svd( 1133 | new_node, 1134 | left_edges=left_edges, 1135 | right_edges=right_edges, 1136 | max_singular_values=maxsvals, 1137 | ) 1138 | 1139 | # Contract the tensors to keep left or right canonical form 1140 | if keep_left_canonical: 1141 | new_left = u 1142 | new_right = tn.contract_between(s, vdag) 1143 | else: 1144 | new_left = tn.contract_between(u, s) 1145 | new_right = vdag 1146 | 1147 | # Put the new tensors after applying the gate back into the MPS list 1148 | new_left.name = self._nodes[node_index1].name 1149 | new_right.name = self._nodes[node_index2].name 1150 | 1151 | self._nodes[node_index1] = new_left 1152 | self._nodes[node_index2] = new_right 1153 | 1154 | # Invert the Swap network, if necessary 1155 | if invert_swap_network: 1156 | self.move_node_from_right_to_left( 1157 | node_index1, original_index1, **kwargs 1158 | ) 1159 | 1160 | # TODO: Remove. This is only for convenience in benchmarking. 1161 | self._norms.append(self.norm()) 1162 | 1163 | def move_node_from_left_to_right( 1164 | self, current_node_index: int, final_node_index: int, **kwargs 1165 | ) -> None: 1166 | """Moves the MPS node at current_node_index to the final_node_index by 1167 | implementing a sequence of SWAP gates from left to right. 1168 | 1169 | Args: 1170 | current_node_index: Index of the node to move from left to right. 1171 | final_node_index: Final index location of the node to move. 1172 | """ 1173 | if current_node_index > final_node_index: 1174 | raise ValueError( 1175 | "current_node_index should be smaller than final_node_index." 1176 | ) 1177 | 1178 | if current_node_index < 0: 1179 | raise ValueError("current_node_index out of range.") 1180 | 1181 | if final_node_index >= self._nqudits: 1182 | raise ValueError("final_node_index out of range.") 1183 | 1184 | if current_node_index == final_node_index: 1185 | return 1186 | 1187 | while current_node_index < final_node_index: 1188 | # TODO: SWAP is only for qubits 1189 | self.swap(current_node_index, current_node_index + 1, **kwargs) 1190 | current_node_index += 1 1191 | 1192 | def move_node_from_right_to_left( 1193 | self, current_node_index: int, final_node_index: int, **kwargs 1194 | ) -> None: 1195 | """Moves the MPS node at current_node_index to the final_node_index by 1196 | implementing a sequence of SWAP gates from right to left. 1197 | 1198 | Args: 1199 | current_node_index: Index of the node to move from right to left. 1200 | final_node_index: Final index location of the node to move. 1201 | """ 1202 | if current_node_index < final_node_index: 1203 | raise ValueError( 1204 | "current_node_index should be larger than final_node_index." 1205 | ) 1206 | 1207 | if current_node_index > self._nqudits: 1208 | raise ValueError("current_node_index out of range.") 1209 | 1210 | if final_node_index < 0: 1211 | raise ValueError("final_node_index out of range.") 1212 | 1213 | if current_node_index == final_node_index: 1214 | return 1215 | 1216 | while current_node_index > final_node_index: 1217 | # TODO: SWAP is only for qubits 1218 | self.swap(current_node_index - 1, current_node_index, **kwargs) 1219 | current_node_index -= 1 1220 | 1221 | def apply( 1222 | self, 1223 | operations: Union[MPSOperation, Sequence[MPSOperation]], 1224 | **kwargs, 1225 | ) -> None: 1226 | """Apply operations to the MPS. 1227 | 1228 | Args: 1229 | operations: (Sequence of) valid MPS Operation(s) to apply. 1230 | 1231 | Keyword Args: 1232 | For one qudit gates: 1233 | 1234 | 1235 | For two qudit gates: 1236 | 1237 | Raises: 1238 | ValueError: On an invalid operation. 1239 | """ 1240 | try: 1241 | operations = iter(operations) 1242 | except TypeError: 1243 | operations = (operations,) 1244 | 1245 | # TODO: Parallelize application of operations 1246 | for op in operations: 1247 | self._apply_mps_operation(op, **kwargs) 1248 | 1249 | def _apply_mps_operation(self, operation: MPSOperation, **kwargs) -> None: 1250 | """Applies the MPS Operation to the MPS. 1251 | 1252 | Args: 1253 | operation: Valid MPS Operation to apply to the MPS. 1254 | """ 1255 | if not isinstance(operation, MPSOperation): 1256 | raise TypeError( 1257 | "Argument operation should be of type MPSOperation but is " 1258 | f"of type {type(operation)}." 1259 | ) 1260 | if not operation.is_valid(): 1261 | raise ValueError("Input MPS Operation is not valid.") 1262 | 1263 | if operation.is_single_qudit_operation(): 1264 | self.apply_one_qudit_gate( 1265 | operation.node(), *operation.qudit_indices, **kwargs 1266 | ) 1267 | elif operation.is_two_qudit_operation(): 1268 | self.apply_two_qudit_gate( 1269 | operation.node(), *operation.qudit_indices, **kwargs 1270 | ) 1271 | else: 1272 | raise ValueError( 1273 | "Only one-qudit and two-qudit gates are supported. " 1274 | "To apply a gate on three or more qudits, the gate must be " 1275 | "compiled into a sequence of one- and two-qudit gates." 1276 | ) 1277 | 1278 | # TODO: Remove single qubit gates -- these don't generalize to qudits. 1279 | def x(self, index: int) -> None: 1280 | """Applies a NOT (Pauli-X) gate to a qubit specified by the index. 1281 | 1282 | If index == -1, the gate is applied to all qubits. 1283 | 1284 | Args: 1285 | index: Index of qubit (tensor) to apply X gate to. 1286 | """ 1287 | if index == -1: 1288 | self.apply_one_qudit_gate_to_all(xgate()) 1289 | else: 1290 | self.apply_one_qudit_gate(xgate(), index) 1291 | 1292 | def h(self, index: int) -> None: 1293 | """Applies a Hadamard gate to a qubit specified by the index. 1294 | 1295 | If index == -1, the gate is applied to all qubits. 1296 | 1297 | Args: 1298 | index: Index of qubit (tensor) to apply X gate to. 1299 | """ 1300 | if index == -1: 1301 | self.apply_one_qudit_gate_to_all(hgate()) 1302 | else: 1303 | self.apply_one_qudit_gate(hgate(), index) 1304 | 1305 | def r(self, index, seed: Optional[int] = None, 1306 | angle_scale: float = 1.0) -> None: 1307 | """Applies a random rotation to the qubit indexed by `index`. 1308 | 1309 | If index == -1, (different) random rotations are applied to all qubits. 1310 | Args: 1311 | index: Index of tensor to apply rotation to. 1312 | seed: Seed for random number generator. 1313 | angle_scale: Floating point value to scale angles by. Default 1. 1314 | """ 1315 | if index == -1: 1316 | for i in range(self._nqudits): 1317 | self.apply_one_qudit_gate( 1318 | rgate(seed, angle_scale), i, 1319 | ) 1320 | else: 1321 | self.apply_one_qudit_gate(rgate(seed, angle_scale), index) 1322 | 1323 | # TODO: Remove. This doesn't generalize to qudits. 1324 | def cnot(self, a: int, b: int, **kwargs) -> None: 1325 | """Applies a CNOT gate with qubit indexed `a` as control 1326 | and qubit indexed `b` as target. 1327 | """ 1328 | self.apply_two_qudit_gate(cnot(), a, b, **kwargs) 1329 | 1330 | def haar_random( 1331 | self, qudit1_index: int, qudit2_index: int, **kwargs 1332 | ) -> None: 1333 | """Applies a two-qudit Haar random unitary with qubit indexed `a` as 1334 | "control" and qubit index `b` as "target". 1335 | 1336 | Args: 1337 | qudit1_index: Index of the first qudit. 1338 | qudit2_index: Index of the second qudit. 1339 | 1340 | Notes: 1341 | See help(MPS.apply_two_qudit_gate) for keyword arguments. 1342 | """ 1343 | gate = haar_random_unitary( 1344 | nqudits=2, qudit_dimension=self._qudit_dimension 1345 | ) 1346 | self.apply_two_qudit_gate(gate, qudit1_index, qudit2_index, **kwargs) 1347 | 1348 | def sweep_haar_random_left_to_right(self, **kwargs) -> None: 1349 | """Applies a layer of two-qudit Haar random unitaries from left to right 1350 | in the MPS. 1351 | """ 1352 | for i in range(0, self._nqudits - 1, 2): 1353 | self.haar_random(i, i + 1, keep_left_canonical=True, **kwargs) 1354 | 1355 | def sweep_haar_random_right_to_left(self, **kwargs) -> None: 1356 | """Applies a layer of two-qudit Haar random unitaries from right to left 1357 | in the MPS. 1358 | """ 1359 | for i in range(self._nqudits - 2, 0, -2): 1360 | self.haar_random(i - 1, i, keep_left_canonical=False, **kwargs) 1361 | 1362 | def sweep_cnots_left_to_right(self, **kwargs) -> None: 1363 | """Applies a layer of CNOTs between adjacent qubits 1364 | going from left to right. 1365 | """ 1366 | for i in range(0, self._nqudits - 1, 2): 1367 | self.cnot(i, i + 1, keep_left_canonical=True, **kwargs) 1368 | 1369 | def sweep_cnots_right_to_left(self, **kwargs) -> None: 1370 | """Applies a layer of CNOTs between adjacent qubits 1371 | going from right to left. 1372 | """ 1373 | for i in range(self._nqudits - 2, 0, -2): 1374 | self.cnot(i - 1, i, keep_left_canonical=False, **kwargs) 1375 | 1376 | def swap(self, a: int, b: int, **kwargs) -> None: 1377 | """Applies a SWAP gate between qubits indexed `a` and `b`.""" 1378 | if b < a: 1379 | a, b = b, a 1380 | self.apply_two_qudit_gate(swap(), a, b, **kwargs) 1381 | 1382 | def copy(self) -> 'MPS': 1383 | """Returns a copy of the MPS.""" 1384 | return self.__copy__() 1385 | 1386 | def __str__(self): 1387 | return "----".join(str(tensor) for tensor in self._nodes) 1388 | 1389 | def __eq__(self, other: 'MPS'): 1390 | if not isinstance(other, MPS): 1391 | return False 1392 | if self is other: 1393 | return True 1394 | if not self.is_valid(): 1395 | raise ValueError( 1396 | "MPS is invalid and cannot be compared to another MPS." 1397 | ) 1398 | if not other.is_valid(): 1399 | raise ValueError( 1400 | "Other MPS is invalid." 1401 | ) 1402 | if (other._qudit_dimension != self._qudit_dimension or 1403 | other._nqudits != self._nqudits): 1404 | return False 1405 | for i in range(self._nqudits): 1406 | if not np.allclose( 1407 | self.get_node(i).tensor, other.get_node(i).tensor 1408 | ): 1409 | return False 1410 | if i > 0: 1411 | if (self.get_left_connected_edge_of(i).dimension != 1412 | other.get_left_connected_edge_of(i).dimension): 1413 | return False 1414 | if i < self._nqudits - 1: 1415 | if (self.get_right_connected_edge_of(i).dimension != 1416 | other.get_right_connected_edge_of(i).dimension): 1417 | return False 1418 | return True 1419 | 1420 | def __copy__(self): 1421 | new = MPS(self._nqudits, self._qudit_dimension, self._prefix) 1422 | new._nodes = self.get_nodes(copy=True) 1423 | return new 1424 | -------------------------------------------------------------------------------- /mpsim/core_test.py: -------------------------------------------------------------------------------- 1 | """Unit tests for inital MPS states.""" 2 | 3 | from copy import copy 4 | import pytest 5 | 6 | import numpy as np 7 | import tensornetwork as tn 8 | 9 | from mpsim import MPS, MPSOperation 10 | from mpsim.gates import ( 11 | igate, 12 | xgate, 13 | zgate, 14 | hgate, 15 | cnot, 16 | cphase, 17 | zero_state, 18 | one_state, 19 | plus_state, 20 | computational_basis_projector 21 | ) 22 | from cirq.qis import density_matrix_from_state_vector 23 | 24 | 25 | def test_single_qubit_identity_mps_operation(): 26 | """Unit tests for a single-qubit identity MPS Operation.""" 27 | node = igate() 28 | mps_operation = MPSOperation(node, qudit_indices=0, qudit_dimension=2) 29 | assert mps_operation.qudit_indices == (0,) 30 | assert mps_operation.qudit_dimension == 2 31 | assert mps_operation.is_valid() 32 | assert mps_operation.is_unitary() 33 | assert mps_operation.is_single_qudit_operation() 34 | assert not mps_operation.is_two_qudit_operation() 35 | 36 | 37 | def test_get_node_and_tensor_one_qubit_mps_operation(): 38 | """Tests getting the node of a one-qubit MPS Operation.""" 39 | np.random.seed(1) 40 | tensor = np.random.randn(2, 2) 41 | node = tn.Node(tensor) 42 | mps_operation = MPSOperation(node, qudit_indices=(0,), qudit_dimension=2) 43 | copy_node = mps_operation.node(copy=True) 44 | # TODO: How to check Node equality with tensornetwork? 45 | assert len(node.edges) == len(copy_node.edges) 46 | # assert node == copy_node 47 | copy_tensor = mps_operation.tensor() 48 | assert np.allclose(tensor, copy_tensor) 49 | 50 | 51 | def test_two_qubit_mps_operation_cnot(): 52 | """Performs simple checks on a two-qubit CNOT MPS Operation.""" 53 | node = cnot() 54 | mps_operation = MPSOperation(node, qudit_indices=(0, 1), qudit_dimension=2) 55 | assert mps_operation.qudit_indices == (0, 1) 56 | assert mps_operation.qudit_dimension == 2 57 | assert not mps_operation.is_single_qudit_operation() 58 | assert mps_operation.is_two_qudit_operation() 59 | 60 | 61 | def test_two_qubit_mps_operation_nonlocal_cnot(): 62 | """Performs simple checks on a two-qubit non-local CNOT MPS Operation.""" 63 | node = cnot() 64 | mps_operation = MPSOperation(node, qudit_indices=(0, 2), qudit_dimension=2) 65 | assert mps_operation.qudit_indices == (0, 2) 66 | assert mps_operation.is_valid() 67 | assert not mps_operation.is_single_qudit_operation() 68 | assert mps_operation.is_two_qudit_operation() 69 | 70 | 71 | def test_mps_one_qudit(): 72 | """Ensures an error is raised if the number of qudits is less than two.""" 73 | for d in (2, 3, 10, 20, 100): 74 | with pytest.raises(ValueError): 75 | MPS(nqudits=1, qudit_dimension=d) 76 | 77 | 78 | def test_is_valid_for_product_states(): 79 | """Tests that a product state on different numbers of qudits is valid.""" 80 | for n in range(2, 20): 81 | for d in range(2, 10): 82 | mps = MPS(nqudits=n, qudit_dimension=d) 83 | assert mps.is_valid() 84 | 85 | 86 | def test_max_bond_dimensions_odd_nqubits(): 87 | """Tests for correctness of maximum bond dimensions for an MPS with 88 | an odd number of qubits. 89 | """ 90 | mps = MPS(nqudits=5) 91 | assert mps._max_bond_dimensions == [2, 4, 4, 2] 92 | mps = MPS(nqudits=7) 93 | assert mps._max_bond_dimensions == [2, 4, 8, 8, 4, 2] 94 | 95 | 96 | def test_max_bond_dimensions_even_nqubits(): 97 | """Tests for correctness of maximum bond dimensions for an MPS 98 | with an even number of qubits. 99 | """ 100 | mps = MPS(nqudits=6) 101 | assert mps._max_bond_dimensions == [2, 4, 8, 4, 2] 102 | mps = MPS(nqudits=8) 103 | assert mps._max_bond_dimensions == [2, 4, 8, 16, 8, 4, 2] 104 | 105 | 106 | def test_max_bond_dimensions_odd_nqudits(): 107 | """Tests for correctness of maximum bond dimensions for an MPS with 108 | an odd number of qudits. 109 | """ 110 | d = 4 111 | mps = MPS(nqudits=5, qudit_dimension=d) 112 | assert mps._max_bond_dimensions == [4, 16, 16, 4] 113 | assert mps.wavefunction().shape == (d**5,) 114 | 115 | mps = MPS(nqudits=7, qudit_dimension=d) 116 | assert mps._max_bond_dimensions == [4, 16, 64, 64, 16, 4] 117 | assert mps.wavefunction().shape == (d**7,) 118 | 119 | 120 | def test_max_bond_dimensions_even_nqudits(): 121 | """Tests for correctness of maximum bond dimensions for an MPS 122 | with an even number of qudits. 123 | """ 124 | d = 10 125 | mps = MPS(nqudits=4, qudit_dimension=d) 126 | assert mps._max_bond_dimensions == [10, 100, 10] 127 | assert mps.wavefunction().shape == (d ** 4,) 128 | 129 | mps = MPS(nqudits=6, qudit_dimension=d) 130 | assert mps._max_bond_dimensions == [10, 100, 1000, 100, 10] 131 | assert mps.wavefunction().shape == (d**6,) 132 | 133 | 134 | def test_get_max_bond_dimension_qubits(): 135 | """Tests correctness for getting maximum bond dimensions in a qubit MPS.""" 136 | mps = MPS(nqudits=10) 137 | # Correct max bond dimensions: [2, 4, 8, 16, 32, 16, 8, 4, 2] 138 | assert mps.max_bond_dimension_of(0) == 2 139 | assert mps.max_bond_dimension_of(-1) == 2 140 | assert mps.max_bond_dimension_of(3) == 16 141 | assert mps.max_bond_dimension_of(4) == 32 142 | assert mps.max_bond_dimension_of(5) == 16 143 | 144 | 145 | def test_get_max_bond_dimension_qudits(): 146 | """Tests correctness for getting maximum bond dimensions in a qudit MPS.""" 147 | d = 10 148 | mps = MPS(nqudits=6, qudit_dimension=d) 149 | # Correct max bond dimensions: [10, 100, 1000, 100, 10] 150 | assert mps.max_bond_dimension_of(0) == d 151 | assert mps.max_bond_dimension_of(1) == d ** 2 152 | assert mps.max_bond_dimension_of(2) == d ** 3 153 | assert mps.max_bond_dimension_of(3) == d ** 2 154 | assert mps.max_bond_dimension_of(-1) == d 155 | 156 | 157 | def test_get_bond_dimensions_product_state(): 158 | """Tests correctness for bond dimensions of a product state MPS.""" 159 | n = 5 160 | for d in range(3, 10): 161 | mps = MPS(nqudits=n, qudit_dimension=d) 162 | assert mps.bond_dimensions() == [1] * (n - 1) 163 | 164 | 165 | def test_get_free_edge_of(): 166 | """Tests getting the free edge of nodes in an MPS.""" 167 | for n in range(2, 10): 168 | for d in (2, 3, 4): 169 | mps = MPS(nqudits=n, qudit_dimension=d) 170 | for i in range(n): 171 | free_edge = mps.get_free_edge_of(i, copy=False) 172 | assert free_edge.is_dangling() 173 | assert free_edge.node1.name == f"q{i}" 174 | 175 | 176 | def test_get_left_connected_edge(): 177 | """Tests getting the left connected edge of nodes in an MPS.""" 178 | for d in (2, 3, 4): 179 | mps = MPS(nqudits=3, qudit_dimension=d) 180 | 181 | # Left edge of first node should be None 182 | edge = mps.get_left_connected_edge_of(0) 183 | assert edge is None 184 | 185 | # Left edge of second node 186 | edge = mps.get_left_connected_edge_of(1) 187 | assert not edge.is_dangling() 188 | assert edge.node1.name == "q0" 189 | assert edge.node2.name == "q1" 190 | 191 | # Left edge of third node 192 | edge = mps.get_left_connected_edge_of(2) 193 | assert not edge.is_dangling() 194 | assert edge.node1.name == "q2" 195 | assert edge.node2.name == "q1" 196 | 197 | 198 | def test_get_right_connected_edge(): 199 | """Tests getting the left connected edge of nodes in an MPS.""" 200 | for d in (2, 3, 4): 201 | mps = MPS(nqudits=3, qudit_dimension=d) 202 | 203 | # Right edge of first node 204 | edge = mps.get_right_connected_edge_of(0) 205 | assert not edge.is_dangling() 206 | assert edge.node1.name == "q0" 207 | assert edge.node2.name == "q1" 208 | 209 | # Right edge of second node 210 | edge = mps.get_right_connected_edge_of(1) 211 | assert not edge.is_dangling() 212 | assert edge.node1.name == "q2" 213 | assert edge.node2.name == "q1" 214 | 215 | # Right edge of third node should be None 216 | edge = mps.get_right_connected_edge_of(2) 217 | assert edge is None 218 | 219 | 220 | def test_get_left_and_get_right_connected_edges(): 221 | """Tests correctness of getting left edges and getting right edges.""" 222 | n = 10 223 | for d in (2, 3, 4): 224 | mps = MPS(nqudits=n, qudit_dimension=d) 225 | for i in range(1, n - 1): 226 | assert (mps.get_right_connected_edge_of(i - 1) == 227 | mps.get_left_connected_edge_of(i)) 228 | 229 | 230 | def test_from_wavefunction_two_qubits_all_zero_state(): 231 | """Tests constructing an MPS from an initial wavefunction.""" 232 | wavefunction = np.array([1, 0, 0, 0]) 233 | mps = MPS.from_wavefunction(wavefunction, nqudits=2, qudit_dimension=2) 234 | assert isinstance(mps, MPS) 235 | assert mps.nqudits == 2 236 | assert mps.qudit_dimension == 2 237 | assert np.allclose(mps.wavefunction(), wavefunction) 238 | assert mps.is_valid() 239 | assert np.isclose(mps.norm(), 1.) 240 | 241 | 242 | def test_from_wavefunction_three_qubits_all_zero_state(): 243 | """Tests constructing an MPS from an initial wavefunction.""" 244 | wavefunction = np.array([1, 0, 0, 0, 0, 0, 0, 0]) 245 | mps = MPS.from_wavefunction(wavefunction, nqudits=3, qudit_dimension=2) 246 | assert isinstance(mps, MPS) 247 | assert mps.nqudits == 3 248 | assert mps.qudit_dimension == 2 249 | assert np.allclose(mps.wavefunction(), wavefunction) 250 | assert mps.is_valid() 251 | assert np.isclose(mps.norm(), 1.) 252 | 253 | 254 | def test_from_wavefunction_random_qubit_wavefunctions(): 255 | """Tests correctness of MPS wavefunction for creating an MPS from 256 | several initial random wavefunctions. 257 | """ 258 | np.random.seed(1) 259 | for _ in range(100): 260 | for n in range(2, 8): 261 | wavefunction = np.random.rand(2**n) 262 | wavefunction /= np.linalg.norm(wavefunction, ord=2) 263 | mps = MPS.from_wavefunction(wavefunction, nqudits=n) 264 | assert np.allclose(mps.wavefunction(), wavefunction) 265 | 266 | 267 | def test_from_wavefunction_random_qudit_wavefunctions(): 268 | """Tests correctness of MPS wavefunction for creating an MPS from 269 | several initial random qudit wavefunctions. 270 | """ 271 | np.random.seed(11) 272 | for _ in range(100): 273 | for n in range(2, 5): 274 | for d in (2, 3, 4): 275 | wavefunction = np.random.rand(d**n) 276 | wavefunction /= np.linalg.norm(wavefunction, ord=2) 277 | mps = MPS.from_wavefunction( 278 | wavefunction, nqudits=n, qudit_dimension=d 279 | ) 280 | assert np.allclose(mps.wavefunction(), wavefunction) 281 | 282 | 283 | def test_from_wavefunction_invalid_args(): 284 | """Tests MPS.from_wavefunction raises errors with invalid args.""" 285 | with pytest.raises(TypeError): 286 | MPS.from_wavefunction({1, 2, 3, 4}, nqudits=2, qudit_dimension=2) 287 | 288 | with pytest.raises(ValueError): 289 | MPS.from_wavefunction([1., 0., 0., 0.], nqudits=3, qudit_dimension=2) 290 | 291 | with pytest.raises(ValueError): 292 | MPS.from_wavefunction([1., 0.], nqudits=1, qudit_dimension=2) 293 | 294 | twod_wavefunction = np.array([[1., 0.], [0., 1.]]) 295 | with pytest.raises(ValueError): 296 | MPS.from_wavefunction(twod_wavefunction, nqudits=2, qudit_dimension=2) 297 | 298 | 299 | def test_get_wavefunction_simple_qubits(): 300 | """Tests getting the wavefunction of a simple qubit MPS.""" 301 | mps = MPS(nqudits=3) 302 | assert isinstance(mps.wavefunction(), np.ndarray) 303 | assert mps.wavefunction().shape == (8,) 304 | correct = np.array([1.0] + [0.0] * 7, dtype=np.complex64) 305 | assert np.allclose(mps.wavefunction(), correct) 306 | 307 | 308 | def test_get_wavefunction_qutrits_simple(): 309 | """Tests getting the wavefunction of a simple qutrit MPS.""" 310 | mps = MPS(nqudits=3, qudit_dimension=3) 311 | assert mps.wavefunction().shape == (27,) 312 | assert np.allclose(mps.wavefunction(), [1] + [0] * 26) 313 | assert mps.is_valid() 314 | 315 | 316 | def test_get_wavefunction_deosnt_modify_mps_qubits(): 317 | """Tests that getting the wavefunction doesn't affect the nodes of a 318 | qubit MPS. 319 | """ 320 | mps = MPS(nqudits=2) 321 | left_node, right_node = mps.get_nodes(copy=False) 322 | _ = mps.wavefunction() 323 | assert len(left_node.edges) == 2 324 | assert len(left_node.get_all_nondangling()) == 1 325 | assert len(left_node.get_all_dangling()) == 1 326 | assert len(right_node.edges) == 2 327 | assert len(right_node.get_all_nondangling()) == 1 328 | assert len(right_node.get_all_dangling()) == 1 329 | 330 | 331 | def test_get_wavefunction_deosnt_modify_mps_qudits(): 332 | """Tests that getting the wavefunction doesn't affect the nodes of a 333 | qudit MPS. 334 | """ 335 | mps = MPS(nqudits=2, qudit_dimension=5) 336 | left_node, right_node = mps.get_nodes(copy=False) 337 | _ = mps.wavefunction() 338 | assert len(left_node.edges) == 2 339 | assert len(left_node.get_all_nondangling()) == 1 340 | assert len(left_node.get_all_dangling()) == 1 341 | assert len(right_node.edges) == 2 342 | assert len(right_node.get_all_nondangling()) == 1 343 | assert len(right_node.get_all_dangling()) == 1 344 | 345 | 346 | def test_correctness_of_initial_product_state_two_qubits(): 347 | """Tests that the contracted MPS is indeed the all zero state 348 | for two qubits. 349 | """ 350 | mps = MPS(nqudits=2) 351 | lq, _ = mps.get_nodes() 352 | wavefunction_node = tn.contract(lq[1]) 353 | wavefunction = np.reshape(wavefunction_node.tensor, newshape=(4,)) 354 | correct = np.array([1, 0, 0, 0], dtype=np.complex64) 355 | assert np.allclose(wavefunction, correct) 356 | 357 | 358 | def test_correctness_of_initial_product_state(): 359 | """Tests that the contracted MPS is indeed the all zero state 360 | for multiple qudits. 361 | """ 362 | for n in range(3, 10): 363 | for d in range(2, 5): 364 | mps = MPS(n, d) 365 | wavefunction = mps.wavefunction() 366 | correct = np.array([1] + [0] * (d ** n - 1), dtype=np.complex64) 367 | assert np.allclose(wavefunction, correct) 368 | 369 | 370 | # Unit tests for applying gates to qubit (as opposed to qudit) MPS 371 | @pytest.mark.parametrize( 372 | ["gate", "expected"], 373 | [(xgate(), one_state), 374 | (hgate(), plus_state), 375 | (zgate(), zero_state), 376 | (igate(), zero_state)], 377 | ) 378 | def test_apply_one_qubit_gate(gate, expected): 379 | """Tests application of a single qubit gate to several MPS.""" 380 | for n in range(2, 8): 381 | for j in range(n): 382 | mps = MPS(n) 383 | mps.apply_one_qudit_gate(gate, j) 384 | final_state = np.reshape(mps.get_node(j).tensor, newshape=(2,)) 385 | assert np.allclose(final_state, expected) 386 | 387 | 388 | def test_apply_oneq_gate_to_all(): 389 | """Tests correctness for final wavefunction after applying a 390 | NOT gate to all qubits in a two-qubit MPS. 391 | """ 392 | mps = MPS(nqudits=2) 393 | mps.apply_one_qudit_gate_to_all(xgate()) 394 | correct = np.array([0.0, 0.0, 0.0, 1.0], dtype=np.complex64) 395 | assert np.allclose(mps.wavefunction(), correct) 396 | 397 | 398 | def test_apply_oneq_gate_to_all_hadamard(): 399 | """Tests correctness for final wavefunction after applying a Hadamard 400 | gate to all qubits in a five-qubit MPS. 401 | """ 402 | n = 5 403 | mps = MPS(nqudits=n) 404 | mps.apply_one_qudit_gate_to_all(hgate()) 405 | correct = 1 / 2 ** (n / 2) * np.ones(2 ** n) 406 | assert np.allclose(mps.wavefunction(), correct) 407 | 408 | 409 | def test_apply_twoq_cnot_two_qubits(): 410 | """Tests for correctness of final wavefunction after applying a CNOT 411 | to a two-qubit MPS. 412 | """ 413 | # In the following tests, the first qubit is always the control qubit. 414 | # Check that CNOT|10> = |11> 415 | mps = MPS(nqudits=2) 416 | mps.x(0) 417 | mps.apply_two_qudit_gate(cnot(), 0, 1) 418 | correct = np.array([0.0, 0.0, 0.0, 1.0], dtype=np.complex64) 419 | assert np.allclose(mps.wavefunction(), correct) 420 | 421 | # Check that CNOT|00> = |00> 422 | mps = MPS(nqudits=2) 423 | mps.apply_two_qudit_gate(cnot(), 0, 1) 424 | correct = np.array([1.0, 0.0, 0.0, 0.0], dtype=np.complex64) 425 | assert np.allclose(mps.wavefunction(), correct) 426 | 427 | # Check that CNOT|01> = |01> 428 | mps = MPS(nqudits=2) 429 | mps.x(1) 430 | mps.apply_two_qudit_gate(cnot(), 0, 1) 431 | correct = np.array([0.0, 1.0, 0.0, 0.0], dtype=np.complex64) 432 | assert np.allclose(mps.wavefunction(), correct) 433 | 434 | # Check that CNOT|11> = |10> 435 | mps = MPS(nqudits=2) 436 | mps.x(-1) # Applies to all qubits in the MPS 437 | mps.apply_two_qudit_gate(cnot(), 0, 1) 438 | correct = np.array([0.0, 0.0, 1.0, 0.0], dtype=np.complex64) 439 | assert np.allclose(mps.wavefunction(), correct) 440 | 441 | 442 | def test_apply_twoq_cnot_two_qubits_flipped_control_and_target(): 443 | """Tests for correctness of final wavefunction after applying a CNOT 444 | to a two-qubit MPS. 445 | """ 446 | # In the following tests, the first qubit is always the target qubit. 447 | # Check that CNOT|10> = |10> 448 | mps = MPS(nqudits=2) 449 | mps.x(0) 450 | mps.cnot(1, 0) 451 | correct = np.array([0.0, 0.0, 1.0, 0.0], dtype=np.complex64) 452 | assert np.allclose(mps.wavefunction(), correct) 453 | 454 | # Check that CNOT|00> = |00> 455 | mps = MPS(nqudits=2) 456 | mps.cnot(1, 0) 457 | correct = np.array([1.0, 0.0, 0.0, 0.0], dtype=np.complex64) 458 | assert np.allclose(mps.wavefunction(), correct) 459 | 460 | # Check that CNOT|01> = |11> 461 | mps = MPS(nqudits=2) 462 | mps.x(1) 463 | mps.cnot(1, 0) 464 | correct = np.array([0.0, 0.0, 0.0, 1.0], dtype=np.complex64) 465 | assert np.allclose(mps.wavefunction(), correct) 466 | 467 | # Check that CNOT|11> = |01> 468 | mps = MPS(nqudits=2) 469 | mps.x(-1) 470 | mps.cnot(1, 0) 471 | correct = np.array([0.0, 1.0, 0.0, 0.0], dtype=np.complex64) 472 | assert np.allclose(mps.wavefunction(), correct) 473 | 474 | 475 | def test_apply_twoq_identical_indices_raises_error(): 476 | """Tests that a two-qubit gate application with 477 | identical indices raises an error. 478 | """ 479 | mps2q = MPS(nqudits=2) 480 | mps3q = MPS(nqudits=3) 481 | mps9q = MPS(nqudits=9) 482 | with pytest.raises(ValueError): 483 | for mps in (mps2q, mps3q, mps9q): 484 | mps.apply_two_qudit_gate(cnot(), 0, 0) 485 | mps.cnot(1, 1) 486 | 487 | 488 | @pytest.mark.parametrize(["left"], [[True], [False]]) 489 | def test_apply_twoq_cnot_four_qubits_interior_qubits(left): 490 | """Tests with a CNOT on four qubits acting on "interior" qubits.""" 491 | mps = MPS(nqudits=4) # State: |0000> 492 | mps.x(1) # State: |0100> 493 | mps.cnot(1, 2, keep_left_canonical=left) # State: |0110> 494 | correct = np.zeros(shape=(16,)) 495 | correct[6] = 1. 496 | assert np.allclose(mps.wavefunction(), correct) 497 | 498 | mps = MPS(nqudits=4) # State: |0000> 499 | mps.cnot(1, 2, keep_left_canonical=left) # State: |0000> 500 | correct = np.zeros(shape=(16,)) 501 | correct[0] = 1. 502 | assert np.allclose(mps.wavefunction(), correct) 503 | 504 | 505 | @pytest.mark.parametrize(["left"], [[True], [False]]) 506 | def test_apply_twoq_cnot_four_qubits_edge_qubits(left): 507 | """Tests with a CNOT on four qubits acting on "edge" qubits.""" 508 | mps = MPS(nqudits=4) # State: |0000> 509 | mps.x(2) # State: |0010> 510 | mps.cnot(2, 3, keep_left_canonical=left) # State: Should be |0011> 511 | correct = np.zeros(shape=(16,)) 512 | correct[3] = 1. 513 | assert np.allclose(mps.wavefunction(), correct) 514 | 515 | mps = MPS(nqudits=4) # State: |0000> 516 | mps.x(0) # State: |1000> 517 | mps.cnot(0, 1, keep_left_canonical=left) # State: Should be |1100> 518 | correct = np.zeros(shape=(16,)) 519 | correct[12] = 1. 520 | assert np.allclose(mps.wavefunction(), correct) 521 | 522 | 523 | @pytest.mark.parametrize(["left"], [[True], [False]]) 524 | def test_apply_twoq_cnot_five_qubits_all_combinations(left): 525 | """Tests applying a CNOT to a five qubit MPS with all index combinations. 526 | 527 | That is, CNOT_01, CNOT_02, CNOT_03, ..., CNOT_24, CNOT_34 where 528 | the first number is the control qubit and the second is the target qubit. 529 | """ 530 | n = 5 531 | indices = [(a, a + 1) for a in range(n - 1)] 532 | 533 | for (a, b) in indices: 534 | # Apply the gates 535 | mps = MPS(n) 536 | mps.x(a) 537 | mps.cnot(a, b, keep_left_canonical=left) 538 | 539 | # Get the correct wavefunction 540 | correct = np.zeros((2 ** n,)) 541 | bits = ["0"] * n 542 | bits[a] = "1" 543 | bits[b] = "1" 544 | correct[int("".join(bits), 2)] = 1.0 545 | assert np.allclose(mps.wavefunction(), correct) 546 | 547 | 548 | @pytest.mark.parametrize(["left"], [[True], [False]]) 549 | def test_apply_twoq_swap_two_qubits(left): 550 | """Tests swapping two qubits in a two-qubit MPS.""" 551 | mps = MPS(nqudits=2) # State: |00> 552 | mps.x(0) # State: |10> 553 | mps.swap(0, 1, keep_left_canonical=left) # State: |01> 554 | correct = np.array([0.0, 1.0, 0.0, 0.0]) 555 | assert np.allclose(mps.wavefunction(), correct) 556 | 557 | mps = MPS(nqudits=2) # State: |00> 558 | mps.swap(0, 1, keep_left_canonical=left) # State: |00> 559 | correct = np.array([1.0, 0.0, 0.0, 0.0]) 560 | assert np.allclose(mps.wavefunction(), correct) 561 | 562 | mps = MPS(nqudits=2) # State: |00> 563 | mps.x(1) # State: |01> 564 | mps.swap(0, 1, keep_left_canonical=left) # State: |10> 565 | correct = np.array([0.0, 0.0, 1.0, 0.0]) 566 | assert np.allclose(mps.wavefunction(), correct) 567 | 568 | mps = MPS(nqudits=2) # State: |00> 569 | mps.x(-1) # State: |11> 570 | mps.swap(0, 1, keep_left_canonical=left) # State: |11> 571 | correct = np.array([0.0, 0.0, 0.0, 1.0]) 572 | assert np.allclose(mps.wavefunction(), correct) 573 | 574 | 575 | @pytest.mark.parametrize(["left"], [[True], [False]]) 576 | def test_apply_twoq_swap_two_qubits(left): 577 | """Tests swapping two qubits in a two-qubit MPS.""" 578 | mps = MPS(nqudits=2) # State: |00> 579 | mps.x(0) # State: |10> 580 | mps.swap(0, 1, keep_left_canonical=left) # State: |01> 581 | correct = np.array([0.0, 1.0, 0.0, 0.0]) 582 | assert np.allclose(mps.wavefunction(), correct) 583 | 584 | mps = MPS(nqudits=2) # State: |00> 585 | mps.swap(0, 1, keep_left_canonical=left) # State: |01> 586 | correct = np.array([1.0, 0.0, 0.0, 0.0]) 587 | assert np.allclose(mps.wavefunction(), correct) 588 | 589 | mps = MPS(nqudits=2) # State: |00> 590 | mps.x(1) # State: |01> 591 | mps.swap(0, 1, keep_left_canonical=left) # State: |01> 592 | correct = np.array([0.0, 0.0, 1.0, 0.0]) 593 | assert np.allclose(mps.wavefunction(), correct) 594 | 595 | mps = MPS(nqudits=2) # State: |00> 596 | mps.x(-1) # State: |11> 597 | mps.swap(0, 1, keep_left_canonical=left) # State: |01> 598 | correct = np.array([0.0, 0.0, 0.0, 1.0]) 599 | assert np.allclose(mps.wavefunction(), correct) 600 | 601 | 602 | @pytest.mark.parametrize(["left"], [[True], [False]]) 603 | def test_apply_swap_five_qubits(left): 604 | """Tests applying a swap gate to an MPS with five qubits.""" 605 | n = 5 606 | for i in range(n - 1): 607 | mps = MPS(n) 608 | mps.x(i) 609 | mps.swap(i, i + 1, keep_left_canonical=left) 610 | # Get the correct wavefunction 611 | correct = np.zeros((2 ** n,)) 612 | bits = ["0"] * n 613 | bits[i + 1] = "1" 614 | correct[int("".join(bits), 2)] = 1.0 615 | assert np.allclose(mps.wavefunction(), correct) 616 | 617 | 618 | @pytest.mark.parametrize(["left"], [[True], [False]]) 619 | def test_qubit_hopping_left_to_right(left): 620 | """Tests "hopping" a qubit with a sequence of swap gates.""" 621 | n = 8 622 | mps = MPS(n) 623 | mps.h(0) 624 | for i in range(1, n - 1): 625 | mps.swap(i, i + 1, keep_left_canonical=left) 626 | correct = np.zeros(2 ** n) 627 | correct[0] = correct[2 ** (n - 1)] = 1.0 / np.sqrt(2) 628 | assert np.allclose(mps.wavefunction(), correct) 629 | 630 | 631 | def test_move_node_left_to_right_three_qubits_one_state(): 632 | """Tests moving a node from left to right.""" 633 | mps = MPS(nqudits=3, qudit_dimension=2) # State: |000> 634 | mps.x(0) # State: |100> 635 | mps.move_node_from_left_to_right(0, 1) # State: |010> 636 | correct = [0., 0., 1., 0., 0., 0., 0., 0.] 637 | assert np.allclose(mps.wavefunction(), correct) 638 | 639 | 640 | def test_move_node_right_to_left_three_qubits_one_state(): 641 | """Tests moving a node from right to left.""" 642 | mps = MPS(nqudits=3, qudit_dimension=2) # State: |000> 643 | mps.x(2) # State: |001> 644 | mps.move_node_from_right_to_left(2, 0) # State: |100> 645 | correct = [0., 0., 0., 0., 1., 0., 0., 0.] 646 | assert np.allclose(mps.wavefunction(), correct) 647 | 648 | 649 | def test_move_node_left_to_right_three_qubits_plus_state(): 650 | """Tests moving a node from left to right.""" 651 | mps = MPS(nqudits=3, qudit_dimension=2) # State: |000> 652 | mps.h(0) # State: |000> + |100> 653 | mps.move_node_from_left_to_right(0, 1) # State: |000> + |010> 654 | correct = np.array([1., 0., 1., 0., 0., 0., 0., 0.]) / np.sqrt(2) 655 | assert np.allclose(mps.wavefunction(), correct) 656 | 657 | 658 | def test_move_node_right_to_left_three_qubits_plus_state(): 659 | """Tests moving a node from right to left.""" 660 | mps = MPS(nqudits=3, qudit_dimension=2) # State: |000> 661 | mps.h(2) # State: |000> + |001> 662 | mps.move_node_from_right_to_left(2, 1) # State: |000> + |010> 663 | correct = np.array([1., 0., 1., 0., 0., 0., 0., 0.]) / np.sqrt(2) 664 | assert np.allclose(mps.wavefunction(), correct) 665 | 666 | 667 | def test_move_node_left_to_right_ten_qubits_end_nodes(): 668 | """Tests moving a node from left to right.""" 669 | n = 10 670 | mps = MPS(nqudits=n, qudit_dimension=2) # State: |0000000000> 671 | mps.x(0) # State: |1000000000> 672 | mps.move_node_from_left_to_right(0, 4) # State: |0000010000> 673 | correct = np.zeros((2**n,)) 674 | correct[2**5] = 1. 675 | assert np.allclose(mps.wavefunction(), correct) 676 | 677 | mps.move_node_from_left_to_right(4, 9) # State: |0000000001> 678 | correct = np.zeros((2**n,)) 679 | correct[1] = 1. 680 | assert np.allclose(mps.wavefunction(), correct) 681 | 682 | 683 | def test_move_node_right_to_left_ten_qubits_end_nodes(): 684 | """Tests moving a node from left to right.""" 685 | n = 10 686 | mps = MPS(nqudits=n, qudit_dimension=2) # State: |0000000000> 687 | mps.x(9) # State: |0000000001> 688 | mps.move_node_from_right_to_left(9, 5) # State: |0000010000> 689 | correct = np.zeros((2**n,)) 690 | correct[2**4] = 1. 691 | assert np.allclose(mps.wavefunction(), correct) 692 | 693 | mps.move_node_from_right_to_left(5, 0) # State: |1000000000> 694 | correct = np.zeros((2**n,)) 695 | correct[2**(n - 1)] = 1. 696 | assert np.allclose(mps.wavefunction(), correct) 697 | 698 | 699 | def test_move_node_left_to_right_raises_error_with_left_greater_than_right(): 700 | mps = MPS(nqudits=5) 701 | with pytest.raises(ValueError): 702 | mps.move_node_from_left_to_right( 703 | current_node_index=4, final_node_index=0 704 | ) 705 | 706 | 707 | def test_move_node_right_to_left_raises_error_with_right_greater_than_left(): 708 | mps = MPS(nqudits=5) 709 | with pytest.raises(ValueError): 710 | mps.move_node_from_right_to_left( 711 | current_node_index=0, final_node_index=4 712 | ) 713 | 714 | 715 | def test_move_node_left_to_right_then_apply_two_qubit_gate(): 716 | n = 5 717 | mps = MPS(nqudits=n) # State: |00000> 718 | mps.x(0) # State: |10000> 719 | correct = np.zeros(shape=(2**n,)) 720 | correct[16] = 1. 721 | assert np.allclose(mps.wavefunction(), correct) 722 | 723 | mps.swap(3, 4) # State: |10000> 724 | assert np.allclose(mps.wavefunction(), correct) 725 | 726 | mps.move_node_from_left_to_right(0, 3) # State: |00010> 727 | mps.swap(3, 4) # State: |00001> 728 | 729 | correct = np.zeros(shape=(2**n,)) 730 | correct[1] = 1. 731 | assert np.allclose(mps.wavefunction(), correct) 732 | 733 | 734 | def test_move_node_right_to_left_then_apply_two_qubit_gate(): 735 | n = 5 736 | mps = MPS(nqudits=n) # State: |00000> 737 | mps.x(n - 1) # State: |00001> 738 | correct = np.zeros(shape=(2**n,)) 739 | correct[1] = 1. 740 | assert np.allclose(mps.wavefunction(), correct) 741 | 742 | mps.swap(0, 1) # State: |00001> 743 | assert np.allclose(mps.wavefunction(), correct) 744 | 745 | mps.move_node_from_right_to_left(4, 1) # State: |01000> 746 | mps.swap(0, 1) # State: |10000> 747 | 748 | correct = np.zeros(shape=(2**n,)) 749 | correct[2**(n - 1)] = 1. 750 | assert np.allclose(mps.wavefunction(), correct) 751 | 752 | 753 | def test_move_right_apply_gate_then_move_left(): 754 | """Tests applying a non-adjacent two-qubit gate by moving nodes around 755 | by swapping. In particular, tests CNOT between the first and last qubits 756 | in a 3-10 qubit MPS. 757 | """ 758 | for n in range(3, 10 + 1): 759 | mps = MPS(nqudits=n) 760 | mps.x(0) # State: |100> 761 | 762 | # Do SWAPs to implement a non-local gate 763 | mps.move_node_from_left_to_right(0, n - 2) # State: |010> 764 | mps.cnot(n - 2, n - 1) # State: |011> 765 | 766 | # Invert the SWAP network 767 | mps.move_node_from_right_to_left(n - 2, 0) # State: |101> 768 | correct = np.zeros(shape=(2**n,)) 769 | correct[2**(n - 1) + 1] = 1. 770 | assert np.allclose(mps.wavefunction(), correct) 771 | 772 | 773 | @pytest.mark.parametrize(["left"], [[True], [False]]) 774 | def test_bell_state(left): 775 | """Tests for wavefunction correctness after preparing a Bell state.""" 776 | n = 2 777 | mps = MPS(n) 778 | mps.h(0) 779 | mps.cnot(0, 1, keep_left_canonical=left) 780 | correct = 1.0 / np.sqrt(2) * np.array([1.0, 0.0, 0.0, 1.0]) 781 | assert np.allclose(mps.wavefunction(), correct) 782 | 783 | 784 | @pytest.mark.parametrize(["left"], [[True], [False]]) 785 | def test_twoq_gates_in_succession(left): 786 | """Tests for wavefunction correctness after applying a 787 | series of two-qubit gates. 788 | """ 789 | n = 2 790 | mps = MPS(n) 791 | mps.x(0) # State: |10> 792 | mps.h(-1) 793 | mps.cnot(0, 1, keep_left_canonical=left) 794 | mps.h(-1) # State: |10> 795 | mps.cnot(0, 1, keep_left_canonical=left) # State: |11> 796 | mps.x(0) # State: |01> 797 | correct = np.array([0.0, 1.0, 0.0, 0.0]) 798 | assert np.allclose(mps.wavefunction(), correct) 799 | 800 | 801 | def test_left_vs_right_canonical_two_qubit_one_gate(): 802 | """Performs a two-qubit gate keeping left-canonical and right-canonical, 803 | checks for equality in final wavefunction. 804 | """ 805 | n = 2 806 | lmps = MPS(nqudits=n) 807 | rmps = MPS(nqudits=n) 808 | lmps.x(0) 809 | rmps.x(0) 810 | lmps.cnot(0, 1) 811 | rmps.cnot(0, 1) 812 | lwavefunction = lmps.wavefunction() 813 | rwavefunction = rmps.wavefunction() 814 | cwavefunction = np.array([0.0, 0.0, 0.0, 1.0]) 815 | assert np.allclose(lwavefunction, cwavefunction) 816 | assert np.allclose(rwavefunction, cwavefunction) 817 | 818 | 819 | def test_apply_cnot_right_to_left_sweep_twoq_mps(): 820 | """Tests applying a CNOT in a "right to left sweep" in a two-qubit MPS.""" 821 | n = 2 822 | mps = MPS(n) 823 | mps.x(1) 824 | mps.h(-1) 825 | mps.cnot(0, 1, keep_left_canonical=False) 826 | mps.h(-1) 827 | 828 | mps.h(-1) 829 | mps.cnot(0, 1, keep_left_canonical=False) 830 | mps.h(-1) 831 | 832 | mps.cnot(0, 1, keep_left_canonical=False) 833 | mps.cnot(0, 1, keep_left_canonical=False) 834 | assert mps.is_valid() 835 | 836 | 837 | @pytest.mark.parametrize(["left"], [[True], [False]]) 838 | def test_valid_mps_indexA_greater_than_indexB_twoq_three_qubits(left): 839 | """Tests successive application of two CNOTs in a three-qubit MPS.""" 840 | n = 3 841 | mps = MPS(n) 842 | mps.x(0) 843 | mps.cnot(0, 1, keep_left_canonical=left) 844 | assert mps.is_valid() 845 | 846 | mps.h(0) 847 | mps.h(1) 848 | mps.cnot(0, 1, keep_left_canonical=left) 849 | mps.h(0) 850 | mps.h(1) 851 | assert mps.is_valid() 852 | 853 | 854 | @pytest.mark.parametrize(["left"], [[True], [False]]) 855 | def test_three_cnots_is_swap(left): 856 | for n in range(2, 11): 857 | mps = MPS(n) 858 | mps.x(0) 859 | 860 | # CNOT(0, 1) 861 | mps.cnot(0, 1, keep_left_canonical=left) 862 | 863 | # CNOT(1, 0) 864 | mps.h(-1) 865 | mps.cnot(0, 1, keep_left_canonical=left) 866 | mps.h(-1) 867 | 868 | # CNOT(0, 1) 869 | mps.cnot(0, 1) 870 | 871 | correct = np.zeros((2 ** n)) 872 | correct[2 ** (n - 2)] = 1 873 | assert np.allclose(mps.wavefunction(), correct) 874 | 875 | 876 | def test_apply_cnot_right_to_left_sweep_threeq_mps(): 877 | """Tests applying a CNOT in a "right to left sweep" in a 878 | three-qubit MPS retains a valid MPS. 879 | """ 880 | n = 3 881 | mps = MPS(n) 882 | mps.x(2) 883 | mps.cnot(1, 2, keep_left_canonical=True) 884 | mps.cnot(0, 1, keep_left_canonical=True) 885 | assert mps.is_valid() 886 | 887 | 888 | def test_qubit_hopping_left_to_right_and_back(): 889 | """Tests "hopping" a qubit with a sequence of swap gates in 890 | several n-qubit MPS states. 891 | """ 892 | for n in range(2, 20): 893 | mps = MPS(n) 894 | mps.x(0) 895 | for i in range(n - 1): 896 | mps.swap(i, i + 1, keep_left_canonical=True) 897 | for i in range(n - 1, 0, -1): 898 | mps.swap(i - 1, i, keep_left_canonical=True) 899 | assert mps.is_valid() 900 | correct = np.zeros(2 ** n) 901 | correct[2 ** (n - 1)] = 1 902 | assert np.allclose(mps.wavefunction(), correct) 903 | 904 | 905 | @pytest.mark.parametrize(["left"], [[True], [False]]) 906 | def test_cnot_truncation_two_qubits_product(left): 907 | """Tests applying a CNOT with truncation on a product state.""" 908 | mps = MPS(nqudits=2) 909 | mps.x(0) 910 | mps.cnot(0, 1, max_singular_values=0.5, keep_left_canonical=left) 911 | correct = np.array([0.0, 0.0, 0.0, 1.0]) 912 | assert np.allclose(mps.wavefunction(), correct) 913 | 914 | 915 | def test_cnot_truncation_on_bell_state(): 916 | """Tests CNOT with truncation on the state |00> + |10>.""" 917 | # Test with truncation 918 | mps = MPS(nqudits=2) 919 | mps.h(0) 920 | mps.cnot(0, 1, fraction=0.5) 921 | correct = np.array([1 / np.sqrt(2), 0.0, 0.0, 0.0]) 922 | assert np.allclose(mps.wavefunction(), correct) 923 | 924 | # Test keeping all singular values ==> Bell state 925 | mps = MPS(nqudits=2) 926 | mps.h(0) 927 | mps.cnot(0, 1, fraction=1) 928 | correct = np.array([1 / np.sqrt(2), 0.0, 0.0, 1 / np.sqrt(2)]) 929 | assert np.allclose(mps.wavefunction(), correct) 930 | 931 | 932 | def test_bond_dimension_doubles_two_qubit_gate(): 933 | """Tests that the bond dimension doubles after applying a 934 | two-qubit gate to a product state. 935 | """ 936 | mps = MPS(nqudits=2) 937 | assert mps.bond_dimension_of(0) == 1 938 | mps.h(0) 939 | assert mps.bond_dimension_of(0) == 1 940 | mps.cnot(0, 1) 941 | assert mps.is_valid() 942 | assert mps.bond_dimension_of(0) == 2 943 | mps.cnot(0, 1) 944 | assert mps.bond_dimension_of(0) == 2 945 | 946 | 947 | def test_keep_half_bond_dimension_singular_values(): 948 | """Tests keeping a number of singular values which is half 949 | the maximum bond dimension. 950 | """ 951 | # Get an MPS and test the initial bond dimensions and max bond dimensions 952 | mps = MPS(nqudits=4) 953 | assert mps.bond_dimensions() == [1, 1, 1] 954 | assert mps.max_bond_dimensions() == [2, 4, 2] 955 | 956 | # Apply a two qubit gate explicitly keeping all singular values 957 | mps.r(-1) 958 | mps.apply_two_qudit_gate( 959 | cnot(), 0, 1, fraction=1, 960 | ) 961 | assert mps.bond_dimensions() == [2, 1, 1] 962 | 963 | # Get an MPS and test the initial bond dimensions and max bond dimensions 964 | mps = MPS(nqudits=4) 965 | assert mps.bond_dimensions() == [1, 1, 1] 966 | assert mps.max_bond_dimensions() == [2, 4, 2] 967 | 968 | # Apply a two qubit gate keeping half the singular values 969 | mps.r(-1) 970 | mps.apply_two_qudit_gate( 971 | cnot(), 0, 1, fraction=0.5 972 | ) 973 | assert mps.bond_dimensions() == [1, 1, 1] 974 | 975 | 976 | def test_inner_product_basis_states(): 977 | """Tests inner products of four two-qubit basis states.""" 978 | # Get the four MPS 979 | mps00 = MPS(nqudits=2) 980 | mps01 = MPS(nqudits=2) 981 | mps01.apply(MPSOperation(xgate(), 1)) 982 | mps10 = MPS(nqudits=2) 983 | mps10.apply(MPSOperation(xgate(), 0)) 984 | mps11 = MPS(nqudits=2) 985 | mps11.apply(MPSOperation(xgate(), 0)) 986 | mps11.apply(MPSOperation(xgate(), 1)) 987 | allmps = (mps00, mps01, mps10, mps11) 988 | 989 | # Test inner products 990 | for i in range(4): 991 | for j in range(4): 992 | assert np.isclose(allmps[i].inner_product(allmps[j]), i == j) 993 | 994 | 995 | @pytest.mark.parametrize("n", [2, 3, 5, 8, 10]) 996 | def test_inner_product_correctness_with_qubit_wavefunctions(n: int): 997 | """Tests correctness of MPS.inner_product by computing the inner product 998 | from the wavefunctions. 999 | """ 1000 | np.random.seed(1) 1001 | 1002 | for _ in range(50): 1003 | # Get the wavefunctions 1004 | wavefunction1 = np.random.randn(2**n) + np.random.randn(2**n) * 1j 1005 | wavefunction1 /= np.linalg.norm(wavefunction1) 1006 | wavefunction2 = np.random.randn(2**n) + np.random.randn(2**n) * 1j 1007 | wavefunction2 /= np.linalg.norm(wavefunction2) 1008 | 1009 | # Get the MPS from the wavefunctions 1010 | mps1 = MPS.from_wavefunction( 1011 | wavefunction1, nqudits=n, qudit_dimension=2 1012 | ) 1013 | mps2 = MPS.from_wavefunction( 1014 | wavefunction2, nqudits=n, qudit_dimension=2 1015 | ) 1016 | 1017 | # Check correctness for the inner products 1018 | assert np.isclose( 1019 | mps1.inner_product(mps2), 1020 | np.inner(wavefunction1, wavefunction2.conj()) 1021 | ) 1022 | assert np.isclose( 1023 | mps2.inner_product(mps1), 1024 | np.inner(wavefunction2, wavefunction1.conj()) 1025 | ) 1026 | 1027 | 1028 | def test_inner_product_raises_error_mismatch_nqudits(): 1029 | """Tests that raises an error when 1030 | self.nqudits != other.nqudits. 1031 | """ 1032 | mps1 = MPS(nqudits=5) 1033 | mps2 = MPS(nqudits=6) 1034 | with pytest.raises(ValueError): 1035 | mps1.inner_product(mps2) 1036 | 1037 | 1038 | def test_inner_product_raises_error_mismatch_qudit_dimension(): 1039 | """Tests that raises an error when 1040 | self.qudit_dimension != other.qudit_dimension. 1041 | """ 1042 | mps1 = MPS(nqudits=5, qudit_dimension=2) 1043 | mps2 = MPS(nqudits=5, qudit_dimension=3) 1044 | with pytest.raises(ValueError): 1045 | mps1.inner_product(mps2) 1046 | 1047 | 1048 | def test_norm_two_qubit_product_simple(): 1049 | """Tests norm of a two-qubit product state MPS.""" 1050 | mps = MPS(nqudits=2) 1051 | assert mps.norm() == 1 1052 | 1053 | # Make sure the wavefunction hasn't changed 1054 | assert np.allclose(mps.wavefunction(), [1, 0, 0, 0]) 1055 | 1056 | 1057 | @pytest.mark.parametrize(["n"], 1058 | [[3], [4], [5], [6], [7], [8], [9], [10]] 1059 | ) 1060 | def test_norm_nqubit_product_state(n): 1061 | """Tests n qubit MPS in the all |0> state have norm 1.""" 1062 | assert MPS(nqudits=n).norm() == 1 1063 | 1064 | 1065 | def test_norm_after_local_rotations(): 1066 | """Applies local rotations (single qubit gates) to an MPS and ensures 1067 | the norm stays one. 1068 | """ 1069 | mps = MPS(nqudits=10) 1070 | assert mps.norm() == 1 1071 | mps.h(-1) 1072 | assert np.isclose(mps.norm(), 1.) 1073 | 1074 | 1075 | def test_norm_after_two_qubit_gate(): 1076 | """Tests computing the norm of an MPS after a two-qubit gate.""" 1077 | mps = MPS(nqudits=2) 1078 | assert mps.norm() == 1 1079 | mps.h(0) 1080 | mps.cnot(0, 1) 1081 | assert np.isclose(mps.norm(), 1.0) 1082 | 1083 | 1084 | def test_norm_decreases_after_two_qubit_gate_with_truncation(): 1085 | """Tests that the norm of an MPS decreases when we throw away svals.""" 1086 | mps = MPS(nqudits=2) 1087 | assert mps.norm() == 1 1088 | mps.h(0) 1089 | mps.cnot(0, 1, maxsvals=1) 1090 | assert np.isclose(mps.norm(), 1. / np.sqrt(2)) 1091 | 1092 | 1093 | def test_norm_is_zero_after_throwing_away_all_singular_values(): 1094 | """Does a two-qubit gate and throws away all singular values, 1095 | checks that the norm is zero. 1096 | """ 1097 | mps = MPS(nqudits=2) 1098 | assert mps.norm() == 1 1099 | mps.h(0) 1100 | mps.cnot(0, 1, maxsvals=0) 1101 | assert mps.norm() == 0 1102 | 1103 | 1104 | def test_renormalize_mps_which_are_normalized(): 1105 | """Makes sure renormalizing a normalized MPS does nothing.""" 1106 | for n in range(2, 8): 1107 | for d in (2, 3, 4, 5): 1108 | # All zero state 1109 | mps = MPS(nqudits=n, qudit_dimension=d) 1110 | assert np.isclose(mps.norm(), 1.0) 1111 | mps.renormalize() 1112 | assert np.isclose(mps.norm(), 1.0) 1113 | 1114 | # Plus state on qubits 1115 | if d == 2: 1116 | ops = [MPSOperation(hgate(), (i,)) for i in range(n)] 1117 | mps.apply(ops) 1118 | assert np.isclose(mps.norm(), 1.0) 1119 | mps.renormalize() 1120 | assert np.isclose(mps.norm(), 1.0) 1121 | 1122 | 1123 | def test_renormalize_after_throwing_away_singular_values_bell_state(): 1124 | """Prepares a Bell state only keeping one singular value with the CNOT 1125 | and checks that renormalization works correctly. 1126 | """ 1127 | mps = MPS(nqudits=2) 1128 | mps.apply( 1129 | [MPSOperation(hgate(), (0,)), MPSOperation(cnot(), (0, 1))], 1130 | maxsvals=1 1131 | ) 1132 | correct = np.array([1. / np.sqrt(2), 0., 0., 0.]) 1133 | assert np.allclose(mps.wavefunction(), correct) 1134 | assert np.isclose(mps.norm(), 1. / np.sqrt(2)) 1135 | 1136 | # Renormalize 1137 | mps.renormalize() 1138 | print(mps.wavefunction()) 1139 | correct = np.array([1., 0., 0., 0.]) 1140 | assert np.allclose(mps.wavefunction(), correct) 1141 | assert np.isclose(mps.norm(), 1.) 1142 | 1143 | 1144 | def test_renormalize_to_value_after_throwing_away_singular_values_bell_state(): 1145 | """Prepares a Bell state only keeping one singular value with the CNOT 1146 | and checks that renormalization to a provided value works correctly. 1147 | """ 1148 | mps = MPS(nqudits=2) 1149 | mps.apply( 1150 | [MPSOperation(hgate(), (0,)), MPSOperation(cnot(), (0, 1))], 1151 | maxsvals=1 1152 | ) 1153 | correct = np.array([1. / np.sqrt(2), 0., 0., 0.]) 1154 | assert np.allclose(mps.wavefunction(), correct) 1155 | assert np.isclose(mps.norm(), 1. / np.sqrt(2)) 1156 | 1157 | # Renormalize to different values 1158 | for norm in np.linspace(0.1, 2., 100): 1159 | mps.renormalize(to_norm=norm) 1160 | correct = np.array([norm, 0., 0., 0.]) 1161 | assert np.allclose(mps.wavefunction(), correct) 1162 | assert np.isclose(mps.norm(), norm) 1163 | 1164 | 1165 | def test_renormalize_an_mps_with_too_small_norm_raises_error(): 1166 | """Asserts that renormalizing an MPS with zero norm raises an error.""" 1167 | mps = MPS(nqudits=2) 1168 | mps.apply( 1169 | [MPSOperation(hgate(), (0,)), MPSOperation(cnot(), (0, 1))], 1170 | maxsvals=0 1171 | ) 1172 | assert np.isclose(mps.norm(), 0.) 1173 | with pytest.raises(ValueError): 1174 | mps.renormalize() 1175 | 1176 | 1177 | def test_renormalize_to_invalid_norms_raises_errors(): 1178 | """Asserts that renormalizing an MPS to invalid norms raise errors.""" 1179 | mps = MPS(nqudits=2) 1180 | with pytest.raises(ValueError): 1181 | mps.renormalize(to_norm=-3.14) 1182 | 1183 | with pytest.raises(ValueError): 1184 | mps.renormalize(to_norm=1e-20) 1185 | 1186 | 1187 | def test_apply_one_qubit_mps_operation_xgate(): 1188 | """Tests applying a single qubit MPS Operation.""" 1189 | mps = MPS(nqudits=2) 1190 | mps_operation = MPSOperation(xgate(), qudit_indices=(0,), qudit_dimension=2) 1191 | assert np.allclose(mps.wavefunction(), [1., 0., 0., 0.]) 1192 | 1193 | mps.apply(mps_operation) # Applies NOT to the first qubit 1194 | assert np.allclose(mps.wavefunction(), [0., 0., 1., 0.]) 1195 | 1196 | 1197 | def test_mps_operation_prepare_bell_state(): 1198 | """Tests preparing a Bell state using MPS Operations.""" 1199 | mps = MPS(nqudits=2) 1200 | h_op = MPSOperation(hgate(), qudit_indices=(0,), qudit_dimension=2) 1201 | cnot_op = MPSOperation(cnot(), qudit_indices=(0, 1), qudit_dimension=2) 1202 | assert np.allclose(mps.wavefunction(), [1., 0., 0., 0.]) 1203 | 1204 | mps.apply(h_op) 1205 | mps.apply(cnot_op) 1206 | correct = 1 / np.sqrt(2) * np.array([1, 0, 0, 1]) 1207 | assert np.allclose(mps.wavefunction(), correct) 1208 | 1209 | 1210 | def test_mps_operation_prepare_bell_state_with_truncation(): 1211 | """Tests preparing a Bell state using MPS Operations providng maxsvals 1212 | as a keyword argument to MPS.apply. 1213 | """ 1214 | mps = MPS(nqudits=2) 1215 | h_op = MPSOperation(hgate(), qudit_indices=(0,), qudit_dimension=2) 1216 | cnot_op = MPSOperation(cnot(), qudit_indices=(0, 1), qudit_dimension=2) 1217 | assert np.allclose(mps.wavefunction(), [1., 0., 0., 0.]) 1218 | 1219 | mps.apply(h_op) 1220 | mps.apply(cnot_op, maxsvals=1) 1221 | correct = 1 / np.sqrt(2) * np.array([1., 0., 0., 0.]) 1222 | assert np.allclose(mps.wavefunction(), correct) 1223 | 1224 | 1225 | def test_apply_nonlocal_two_qubit_gate(): 1226 | """Tests applying a non-local CNOT in a 3-10 qubit MPS.""" 1227 | for n in range(3, 10): 1228 | mps = MPS(nqudits=n) # State: |00...0> 1229 | mps.x(0) # State: |10...0> 1230 | mps.cnot(0, n - 1) # State: |10...1> 1231 | correct = np.zeros(shape=(2**n,)) 1232 | correct[2**(n - 1) + 1] = 1. 1233 | assert np.allclose(mps.wavefunction(), correct) 1234 | 1235 | 1236 | def test_prepare_ghz_states_using_nonlocal_gates(): 1237 | """Tests preparing n-qubit GHZ states using non-local CNOT gates.""" 1238 | for n in range(3, 10): 1239 | mps = MPS(nqudits=n) 1240 | mps.h(0) 1241 | for i in range(1, n): 1242 | mps.cnot(0, i) 1243 | correct = np.zeros(shape=(2**n,)) 1244 | correct[0] = correct[-1] = 1. / np.sqrt(2) 1245 | assert np.allclose(mps.wavefunction(), correct) 1246 | 1247 | 1248 | def test_apply_qft_nonlocal_gates(): 1249 | """Tests applying the QFT to an n-qubit MPS in the all zero state.""" 1250 | for n in range(3, 10): 1251 | mps = MPS(nqudits=n) 1252 | for i in range(n - 1, -1, -1): 1253 | mps.h(i) 1254 | for j in range(i - 1, -1, -1): 1255 | mps.apply_two_qudit_gate(cphase(2 ** (j - i)), j, i) 1256 | correct = np.ones(shape=(2**n,)) 1257 | correct /= 2**(n / 2) 1258 | assert np.allclose(mps.wavefunction(), correct) 1259 | 1260 | 1261 | def test_valid_after_orthonormalize_right_edges(): 1262 | """Tests |+++> MPS remains valid, retains correct bond dimensions, and 1263 | retains correct wavefunction after orthonormalizing right edges. 1264 | """ 1265 | n = 3 1266 | mps = MPS(nqudits=n) 1267 | mps_operations = [MPSOperation(hgate(), (i,)) for i in range(n)] 1268 | mps.apply(mps_operations) 1269 | wavefunction_before = mps.wavefunction() 1270 | assert mps.bond_dimension_of(0) == 1 1271 | assert mps.bond_dimension_of(1) == 1 1272 | 1273 | # Orthonormalize the right edge of the first node 1274 | mps.orthonormalize_right_edge_of(0) 1275 | assert mps.is_valid() 1276 | assert mps.bond_dimension_of(0) == 1 1277 | assert mps.bond_dimension_of(1) == 1 1278 | assert np.allclose(mps.wavefunction(), wavefunction_before) 1279 | 1280 | # Orthonormalize the right edge of the second node 1281 | mps.orthonormalize_right_edge_of(1) 1282 | assert mps.is_valid() 1283 | assert mps.bond_dimension_of(0) == 1 1284 | assert mps.bond_dimension_of(1) == 1 1285 | assert np.allclose(mps.wavefunction(), wavefunction_before) 1286 | 1287 | 1288 | def test_apply_povm_product_state(): 1289 | """Tests applying a POVM + orthonormalizing the index to the |+++> state.""" 1290 | # Get the projector 1291 | pi0 = computational_basis_projector(state=0) 1292 | 1293 | # Create an MPS in the H|0> state 1294 | n = 3 1295 | mps = MPS(nqudits=n) # State: |000> 1296 | mps_operations = [MPSOperation(hgate(), i) for i in range(n)] 1297 | mps.apply(mps_operations) # State |+++> 1298 | assert np.isclose(mps.norm(), 1.0) 1299 | assert mps.bond_dimensions() == [1, 1] 1300 | 1301 | # Apply |0><0| to the first qubit 1302 | mps.apply_one_qudit_gate( 1303 | pi0, 1304 | 0, 1305 | ortho_after_non_unitary=False, 1306 | renormalize_after_non_unitary=False 1307 | ) # State: 1 / sqrt(2) * |0++> 1308 | assert mps.is_valid() 1309 | assert np.isclose(mps.norm(), 1. / np.sqrt(2)) 1310 | assert mps.bond_dimensions() == [1, 1] 1311 | correct = 1. / np.sqrt(2)**3 * np.array([1] * 4 + [0] * 4) 1312 | assert np.allclose(mps.wavefunction(), correct) 1313 | 1314 | # Apply |0><0| to the second qubit 1315 | mps.apply_one_qudit_gate( 1316 | pi0, 1317 | 1, 1318 | ortho_after_non_unitary=False, 1319 | renormalize_after_non_unitary=False 1320 | ) # State: 1 / 2 * |00+> 1321 | assert mps.is_valid() 1322 | assert np.isclose(mps.norm(), 1. / 2.) 1323 | assert mps.bond_dimensions() == [1, 1] 1324 | correct = 1. / np.sqrt(2)**3 * np.array([1] * 2 + [0] * 6) 1325 | assert np.allclose(mps.wavefunction(), correct) 1326 | 1327 | # Apply |0><0| to the third qubit 1328 | mps.apply_one_qudit_gate( 1329 | pi0, 1330 | 2, 1331 | ortho_after_non_unitary=False, 1332 | renormalize_after_non_unitary=False 1333 | ) # State: 1 / sqrt(2)**3 * |000> 1334 | assert mps.is_valid() 1335 | assert np.isclose(mps.norm(), 1. / 2. / np.sqrt(2)) 1336 | assert mps.bond_dimensions() == [1, 1] 1337 | correct = 1. / np.sqrt(2) ** 3 * np.array([1] * 1 + [0] * 7) 1338 | assert np.allclose(mps.wavefunction(), correct) 1339 | 1340 | 1341 | def test_apply_povm_bell_state_right_ortho_reduces_bond_dimension(): 1342 | """Tests applying a POVM + orthonormalizing the index to a bell state.""" 1343 | # Get the projector 1344 | pi0 = computational_basis_projector(state=0) 1345 | 1346 | # Create an MPS in the Bell state 1347 | n = 2 1348 | mps = MPS(nqudits=n) # State: |00> 1349 | mps_operations = [ 1350 | MPSOperation(hgate(), (0,)), 1351 | MPSOperation(cnot(), (0, 1)) 1352 | ] 1353 | mps.apply(mps_operations) # State: 1 / sqrt(2) |00> + |11> 1354 | assert np.isclose(mps.norm(), 1.0) 1355 | assert mps.bond_dimensions() == [2] 1356 | wavefunction_before = mps.wavefunction() 1357 | 1358 | # Check that orthonormalization does nothing to the Bell state 1359 | mps.orthonormalize_right_edge_of(node_index=0) 1360 | assert mps.is_valid() 1361 | assert np.isclose(mps.norm(), 1.0) 1362 | assert np.allclose(mps.wavefunction(), wavefunction_before) 1363 | assert mps.bond_dimensions() == [2] 1364 | 1365 | # Apply |0><0| to the first qubit 1366 | mps.apply_one_qudit_gate( 1367 | pi0, 1368 | 0, 1369 | ortho_after_non_unitary=False, 1370 | renormalize_after_non_unitary=False 1371 | ) 1372 | assert mps.is_valid() 1373 | assert np.isclose(mps.norm(), 1. / np.sqrt(2)) 1374 | correct = 1. / np.sqrt(2) * np.array([1., 0., 0., 0.]) 1375 | assert np.allclose(mps.wavefunction(), correct) 1376 | assert mps.bond_dimensions() == [2] 1377 | 1378 | # Now do the orthonormalization to reduce the bond dimension 1379 | mps.orthonormalize_right_edge_of(node_index=0) 1380 | assert mps.is_valid() 1381 | assert np.isclose(mps.norm(), 1. / np.sqrt(2)) 1382 | assert np.allclose(mps.wavefunction(), correct) 1383 | assert mps.bond_dimensions() == [1] 1384 | 1385 | 1386 | def test_apply_povm_bell_state_left_ortho_reduces_bond_dimension(): 1387 | """Tests applying a POVM + orthonormalizing the index to a bell state.""" 1388 | # Get the projector 1389 | pi0 = computational_basis_projector(state=0) 1390 | 1391 | # Create an MPS in the Bell state 1392 | n = 2 1393 | mps = MPS(nqudits=n) # State: |00> 1394 | mps_operations = [ 1395 | MPSOperation(hgate(), (0,)), 1396 | MPSOperation(cnot(), (0, 1)) 1397 | ] 1398 | mps.apply(mps_operations) # State: 1 / sqrt(2) |00> + |11> 1399 | assert np.isclose(mps.norm(), 1.0) 1400 | assert mps.bond_dimensions() == [2] 1401 | wavefunction_before = mps.wavefunction() 1402 | 1403 | # Check that orthonormalization does nothing to the Bell state 1404 | mps.orthonormalize_left_edge_of(node_index=1) 1405 | assert mps.is_valid() 1406 | assert np.isclose(mps.norm(), 1.0) 1407 | assert np.allclose(mps.wavefunction(), wavefunction_before) 1408 | assert mps.bond_dimensions() == [2] 1409 | 1410 | # Apply |0><0| to the second qubit 1411 | mps.apply_one_qudit_gate( 1412 | pi0, 1413 | 1, 1414 | ortho_after_non_unitary=False, 1415 | renormalize_after_non_unitary=False 1416 | ) 1417 | assert mps.is_valid() 1418 | assert np.isclose(mps.norm(), 1. / np.sqrt(2)) 1419 | correct = 1. / np.sqrt(2) * np.array([1., 0., 0., 0.]) 1420 | assert np.allclose(mps.wavefunction(), correct) 1421 | assert mps.bond_dimensions() == [2] 1422 | 1423 | # Now do the orthonormalization to reduce the bond dimension 1424 | mps.orthonormalize_left_edge_of(node_index=1) 1425 | assert mps.is_valid() 1426 | assert np.isclose(mps.norm(), 1. / np.sqrt(2)) 1427 | assert np.allclose(mps.wavefunction(), correct) 1428 | assert mps.bond_dimensions() == [1] 1429 | 1430 | 1431 | def test_orthonormalize_all_tensors_edge_cases(): 1432 | """Tests orthonormalizing all tensors in an MPS and ensures the MPS remains 1433 | valid after. 1434 | """ 1435 | for n in range(2, 8): 1436 | for d in (2, 3, 4): 1437 | mps = MPS(nqudits=n, qudit_dimension=d) 1438 | correct = mps.wavefunction() 1439 | for node_index in range(n - 1): 1440 | mps.orthonormalize_right_edge_of(node_index) 1441 | assert mps.is_valid() 1442 | assert np.allclose(mps.wavefunction(), correct) 1443 | assert np.isclose(mps.norm(), 1.) 1444 | for node_index in range(1, n): 1445 | mps.orthonormalize_left_edge_of(node_index) 1446 | assert mps.is_valid() 1447 | assert np.allclose(mps.wavefunction(), correct) 1448 | assert np.isclose(mps.norm(), 1.) 1449 | 1450 | 1451 | def test_renormalize_after_non_unitary(): 1452 | """Applies non-unitary POVMs and tests that norm remains the same.""" 1453 | nqubits = 6 1454 | depth = 10 1455 | mps = MPS(nqudits=nqubits) 1456 | pi0 = computational_basis_projector(state=0, dim=2) 1457 | for _ in range(depth): 1458 | mps.r(-1) 1459 | mps.sweep_cnots_left_to_right() 1460 | assert np.isclose(mps.norm(), 1.) 1461 | for i in range(nqubits): 1462 | mps.apply_one_qudit_gate( 1463 | gate=pi0, 1464 | node_index=i, 1465 | ortho_after_non_unitary=True, 1466 | renormalize_after_non_unitary=True 1467 | ) 1468 | assert np.isclose(mps.norm(), 1.) 1469 | 1470 | 1471 | @pytest.mark.parametrize("chi", [16, 32, 64, 128]) 1472 | def test_max_bond_dimension_not_surpassed(chi: int): 1473 | """Applies operations with a max chi value and ensures the bond dimensions 1474 | of the tensors never gets bigger than chi. 1475 | """ 1476 | nqubits = 10 1477 | depth = 10 1478 | mps = MPS(nqudits=nqubits, qudit_dimension=2) 1479 | 1480 | singles = (hgate(), xgate(), zgate()) 1481 | czgate = cphase(exp=0.5) 1482 | 1483 | # Apply operations 1484 | for _ in range(depth): 1485 | for i in range(nqubits): 1486 | gate = np.random.choice(singles) 1487 | op = MPSOperation(gate, (i,)) 1488 | mps.apply(op) 1489 | 1490 | for i in range(nqubits): 1491 | other_qubits = list(set(range(nqubits)) - {i}) 1492 | j = np.random.choice(other_qubits) 1493 | op = MPSOperation(czgate, (i, j)) 1494 | mps.apply(op, maxsvals=chi) 1495 | 1496 | assert all(bond_dimension <= chi 1497 | for bond_dimension in mps.bond_dimensions()) 1498 | 1499 | 1500 | def test_equal(): 1501 | """Tests checking equality of MPS.""" 1502 | for n in (2, 3, 5, 10): 1503 | for d in (2, 3, 5, 10): 1504 | mps1 = MPS(nqudits=n, qudit_dimension=d) 1505 | mps2 = MPS(nqudits=n, qudit_dimension=d) 1506 | assert mps1 == mps1 1507 | assert mps2 == mps2 1508 | assert mps1 == mps2 1509 | 1510 | if d == 2: 1511 | mps1.apply(MPSOperation(xgate(), 0)) 1512 | assert mps1 != mps2 1513 | 1514 | mps2.apply(MPSOperation(xgate(), 0)) 1515 | assert mps1 == mps2 1516 | 1517 | 1518 | def test_equal_different_prefixes(): 1519 | """Tests identical MPS with different tensor names are still equal.""" 1520 | mps1 = MPS(nqudits=10, qudit_dimension=2, tensor_prefix="mps1_") 1521 | mps2 = MPS(nqudits=10, qudit_dimension=2, tensor_prefix="mps2_") 1522 | assert mps1 == mps2 1523 | 1524 | 1525 | def test_copy(): 1526 | """Tests copying an MPS by calling copy(MPS).""" 1527 | for n in (2, 3, 5, 10): 1528 | for d in (2, 3, 5, 10): 1529 | mps = MPS(nqudits=n, qudit_dimension=d) 1530 | mps_copy = copy(mps) 1531 | assert mps_copy is not mps 1532 | assert mps_copy == mps 1533 | 1534 | 1535 | def test_copy_method(): 1536 | """Tests copying an MPS by calling MPS.copy().""" 1537 | for n in (2, 3, 5, 10): 1538 | for d in (2, 3, 5, 10): 1539 | mps = MPS(nqudits=n, qudit_dimension=d) 1540 | mps_copy = mps.copy() 1541 | assert mps_copy is not mps 1542 | assert mps_copy == mps 1543 | 1544 | 1545 | def test_expectation_two_qubit_mps(): 1546 | """Tests some expectation values for a two-qubit MPS.""" 1547 | # |00> 1548 | mps = MPS(nqudits=2) 1549 | mps_copy = mps.copy() 1550 | 1551 | # <00|HI|00> = 1 / sqrt(2) 1552 | h0 = MPSOperation(hgate(), 0) 1553 | assert np.isclose(mps.expectation(h0), 1. / np.sqrt(2)) 1554 | assert mps == mps_copy 1555 | 1556 | # <00|XI|00> = 0 1557 | x0 = MPSOperation(xgate(), 0) 1558 | assert np.isclose(mps.expectation(x0), 0.) 1559 | assert mps == mps_copy 1560 | 1561 | # <10|HI|10> = - 1 / sqrt(2) 1562 | mps.apply(MPSOperation(xgate(), 0)) 1563 | assert np.isclose(mps.expectation(h0), -1. / np.sqrt(2)) 1564 | 1565 | 1566 | def test_dagger_simple(): 1567 | """Tests taking the dagger of an MPS.""" 1568 | wavefunction = np.array([1j, 0., 0., 0.]) 1569 | mps = MPS.from_wavefunction(wavefunction, nqudits=2, qudit_dimension=2) 1570 | assert np.allclose(mps.wavefunction(), wavefunction) 1571 | mps.dagger() 1572 | assert np.allclose(mps.wavefunction(), wavefunction.conj().T) 1573 | 1574 | 1575 | def test_dagger_random_qubit_wavefunctions(): 1576 | """Tests taking the dagger of an MPS created from random wavefunctions.""" 1577 | np.random.seed(10) 1578 | for n in (2, 3, 5, 10): 1579 | for _ in range(20): 1580 | wavefunction = np.random.randn(2**n) + np.random.randn(2**n) * 1j 1581 | wavefunction /= np.linalg.norm(wavefunction, ord=2) 1582 | mps = MPS.from_wavefunction(wavefunction, nqudits=n) 1583 | assert np.allclose(mps.wavefunction(), wavefunction) 1584 | mps.dagger() 1585 | assert np.allclose(mps.wavefunction(), wavefunction.conj().T) 1586 | 1587 | 1588 | def test_reduced_density_matrix_simple(): 1589 | """Tests computing the reduced density matrix of both sites of a two-qubit 1590 | MPS product states. 1591 | """ 1592 | # State: |00> 1593 | mps = MPS(nqudits=2, qudit_dimension=2) 1594 | for i in (0, 1): 1595 | rdm = mps.reduced_density_matrix(node_indices=i) 1596 | correct = np.array([[1., 0.], [0., 0.]]) 1597 | assert np.allclose(rdm, correct) 1598 | assert mps == MPS(nqudits=2, qudit_dimension=2) 1599 | 1600 | # State: |10> 1601 | mps.apply(MPSOperation(xgate(), 0)) 1602 | rdm = mps.reduced_density_matrix(node_indices=0) 1603 | correct = np.array([[0., 0.], [0., 1.]]) 1604 | assert np.allclose(rdm, correct) 1605 | 1606 | rdm = mps.reduced_density_matrix(node_indices=1) 1607 | correct = np.array([[1., 0.], [0., 0.]]) 1608 | assert np.allclose(rdm, correct) 1609 | 1610 | # State |11> 1611 | mps.apply(MPSOperation(xgate(), 1)) 1612 | rdm = mps.reduced_density_matrix(node_indices=0) 1613 | correct = np.array([[0., 0.], [0., 1.]]) 1614 | assert np.allclose(rdm, correct) 1615 | 1616 | rdm = mps.reduced_density_matrix(node_indices=1) 1617 | correct = np.array([[0., 0.], [0., 1.]]) 1618 | assert np.allclose(rdm, correct) 1619 | 1620 | 1621 | def test_reduced_density_matrix_invalid_indices(): 1622 | """Tests the correct errors are raised for invalid indices.""" 1623 | mps = MPS(nqudits=2) 1624 | 1625 | with pytest.raises(IndexError): 1626 | mps.reduced_density_matrix(node_indices=-1) 1627 | 1628 | with pytest.raises(IndexError): 1629 | mps.reduced_density_matrix(node_indices=22) 1630 | 1631 | with pytest.raises(ValueError): 1632 | mps.reduced_density_matrix(node_indices=[0, 0]) 1633 | 1634 | 1635 | def test_reduced_density_matrix_two_qubits(): 1636 | """Tests computing the reduced density matrix for a two-qubit MPS with 1637 | random wavefunctions. 1638 | """ 1639 | np.random.seed(3) 1640 | for _ in range(50): 1641 | wavefunction = np.random.randn(4) + np.random.randn(4) * 1j 1642 | wavefunction /= np.linalg.norm(wavefunction) 1643 | mps = MPS.from_wavefunction(wavefunction, nqudits=2) 1644 | 1645 | for i in (0, 1): 1646 | correct = density_matrix_from_state_vector( 1647 | state=wavefunction, indices=[i] 1648 | ) 1649 | rdm = mps.reduced_density_matrix(node_indices=i) 1650 | assert np.allclose(rdm, correct) 1651 | assert np.allclose(mps.wavefunction(), wavefunction) 1652 | 1653 | 1654 | def test_density_matrix_three_qubits(): 1655 | """Tests computing the full density matrix on a three-qubit MPS.""" 1656 | np.random.seed(5) 1657 | for _ in range(50): 1658 | wavefunction = np.random.randn(8) + np.random.randn(8) * 1j 1659 | wavefunction /= np.linalg.norm(wavefunction) 1660 | mps = MPS.from_wavefunction(wavefunction, nqudits=3) 1661 | 1662 | for i in [(0,), (1,), (2,), (0, 1), (0, 2), (1, 2), (0, 1, 2)]: 1663 | correct = density_matrix_from_state_vector( 1664 | state=wavefunction, indices=i 1665 | ) 1666 | rdm = mps.reduced_density_matrix(node_indices=i) 1667 | assert np.allclose(rdm, correct) 1668 | 1669 | correct = density_matrix_from_state_vector( 1670 | state=wavefunction, indices=tuple(reversed(i)) 1671 | ) 1672 | rdm = mps.reduced_density_matrix(node_indices=tuple(reversed(i))) 1673 | assert np.allclose(rdm, correct) 1674 | assert np.allclose(mps.wavefunction(), wavefunction) 1675 | 1676 | 1677 | @pytest.mark.parametrize("n", [3, 5, 8]) 1678 | def test_qubit_mps_single_site_density_matrices(n: int): 1679 | """Tests computing single-site partial density matrices on qubit MPS.""" 1680 | np.random.seed(1) 1681 | wavefunction = np.random.randn(2**n) + np.random.randn(2**n) * 1j 1682 | wavefunction /= np.linalg.norm(wavefunction) 1683 | mps = MPS.from_wavefunction(wavefunction, nqudits=n) 1684 | 1685 | site = [int(np.random.choice(range(n)))] 1686 | rdm = mps.reduced_density_matrix(node_indices=site) 1687 | correct = density_matrix_from_state_vector( 1688 | state=wavefunction, indices=site 1689 | ) 1690 | assert np.allclose(rdm, correct) 1691 | assert np.allclose(mps.wavefunction(), wavefunction) 1692 | 1693 | 1694 | @pytest.mark.parametrize("n", [3, 5, 8]) 1695 | def test_qubit_mps_multi_site_density_matrices(n: int): 1696 | """Tests computing partial density matrices.""" 1697 | np.random.seed(1) 1698 | for _ in range(50): 1699 | wavefunction = np.random.randn(2**n) + np.random.randn(2**n) * 1j 1700 | wavefunction /= np.linalg.norm(wavefunction) 1701 | mps = MPS.from_wavefunction(wavefunction, nqudits=n) 1702 | 1703 | size = np.random.randint(low=1, high=n) 1704 | qubits = list(np.random.choice(range(n), size=size, replace=False)) 1705 | sites = [int(q) for q in qubits] 1706 | rdm = mps.reduced_density_matrix(node_indices=sites) 1707 | correct = density_matrix_from_state_vector( 1708 | state=wavefunction, indices=sites 1709 | ) 1710 | assert np.allclose(rdm, correct) 1711 | assert np.allclose(mps.wavefunction(), wavefunction) 1712 | 1713 | 1714 | def test_sample_zero_state(): 1715 | """Sampling from the |00> MPS.""" 1716 | for d in (2, 3, 5): 1717 | mps = MPS(nqudits=2, qudit_dimension=d) 1718 | samples = mps.sample(nsamples=100) 1719 | assert len(samples) == 100 1720 | for sample in samples: 1721 | assert set(sample) == {0} 1722 | 1723 | 1724 | def test_sample_uniform(): 1725 | """Sampling from the |++...+> MPS.""" 1726 | np.random.seed(1) 1727 | n = 3 1728 | prob = 1. / 2**n 1729 | nsamples = 100 1730 | var = 1. / np.sqrt(nsamples) 1731 | 1732 | mps = MPS(nqudits=n) 1733 | mps.apply([MPSOperation(hgate(), i) for i in range(n)]) 1734 | hist = mps.sample(nsamples=nsamples, as_hist=True, as_string=True) 1735 | freqs = np.array(list(hist.values())) / nsamples 1736 | for freq in freqs: 1737 | assert np.abs(freq - prob) < var 1738 | -------------------------------------------------------------------------------- /mpsim/gates.py: -------------------------------------------------------------------------------- 1 | """Declarations of single qubit and two-qubit gates.""" 2 | 3 | from copy import deepcopy 4 | from typing import Optional, Union 5 | 6 | import numpy as np 7 | from scipy.linalg import expm 8 | from scipy.stats import unitary_group 9 | 10 | import tensornetwork as tn 11 | 12 | 13 | # Helper functions 14 | # TODO: Reduce code duplication in helper functions 15 | def is_unitary(gate: Union[np.ndarray, tn.Node]) -> bool: 16 | """Returns True if the gate (of the node) is unitary, else False.""" 17 | if not isinstance(gate, (np.ndarray, tn.Node)): 18 | raise TypeError("Invalid type for gate.") 19 | 20 | if isinstance(gate, tn.Node): 21 | gate = gate.tensor 22 | 23 | # Reshape the matrix if necessary 24 | if len(gate.shape) > 2: 25 | if len(set(gate.shape)) != 1: 26 | raise ValueError("Gate shape should be of the form (d, d, ..., d).") 27 | dim = int(np.sqrt(gate.size)) 28 | gate = np.reshape(gate, newshape=(dim, dim)) 29 | 30 | # Check that the matrix is unitary 31 | return np.allclose( 32 | gate.conj().T @ gate, np.identity(gate.shape[0]), atol=1e-5 33 | ) 34 | 35 | 36 | def is_hermitian(gate: Union[np.ndarray, tn.Node]) -> bool: 37 | """Returns True if the gate (of the node) is Hermitian, else False.""" 38 | if not isinstance(gate, (np.ndarray, tn.Node)): 39 | raise TypeError("Invalid type for gate.") 40 | 41 | if isinstance(gate, tn.Node): 42 | gate = gate.tensor 43 | 44 | # Reshape the matrix if necessary 45 | if len(gate.shape) > 2: 46 | if len(set(gate.shape)) != 1: 47 | raise ValueError("Gate shape should be of the form (d, d, ..., d).") 48 | dim = int(np.sqrt(gate.size)) 49 | gate = np.reshape(gate, newshape=(dim, dim)) 50 | 51 | # Check if the matrix is Hermitian 52 | return np.allclose(gate.conj().T, gate, atol=1e-5) 53 | 54 | 55 | def is_projector(gate: Union[np.ndarray, tn.Node]) -> bool: 56 | """Returns True if the gate (of the node) is a projector, else False.""" 57 | if not isinstance(gate, (np.ndarray, tn.Node)): 58 | raise TypeError("Invalid type for gate.") 59 | 60 | if isinstance(gate, tn.Node): 61 | gate = gate.tensor 62 | 63 | # Reshape the matrix if necessary 64 | if len(gate.shape) > 2: 65 | if len(set(gate.shape)) != 1: 66 | raise ValueError("Gate shape should be of the form (d, d, ..., d).") 67 | dim = int(np.sqrt(gate.size)) 68 | gate = np.reshape(gate, newshape=(dim, dim)) 69 | 70 | return np.linalg.matrix_rank(gate) == 1 71 | 72 | 73 | # Common single qubit states as np.ndarray objects 74 | zero_state = np.array([1.0, 0.0], dtype=np.complex64) 75 | one_state = np.array([0.0, 1.0], dtype=np.complex64) 76 | plus_state = 1.0 / np.sqrt(2) * (zero_state + one_state) 77 | 78 | 79 | def computational_basis_state(state: int, dim: int = 2) -> tn.Node: 80 | """Returns a computational basis state. 81 | 82 | Args: 83 | state: Integer which labels the state from the set {0, 1, ..., d - 1}. 84 | dim: Dimension of the qudit. Default is two for qubits. 85 | 86 | Raises: 87 | ValueError: If state < 0, dim < 0, or state >= dim. 88 | """ 89 | if state < 0: 90 | raise ValueError(f"Argument state should be positive but is {state}.") 91 | 92 | if dim < 0: 93 | raise ValueError(f"Argument dim should be positive but is {dim}.") 94 | 95 | if state >= dim: 96 | raise ValueError( 97 | f"Requires state < dim but state = {state} and dim = {dim}." 98 | ) 99 | vector = np.zeros((dim,)) 100 | vector[state] = 1. 101 | return tn.Node(vector, name=f"|{state}>") 102 | 103 | # Common single qubit gates as np.ndarray objects 104 | _hmatrix = ( 105 | 1 / np.sqrt(2) * np.array([[1.0, 1.0], [1.0, -1.0]], dtype=np.complex64) 106 | ) 107 | _imatrix = np.array([[1.0, 0.0], [0.0, 1.0]], dtype=np.complex64) 108 | _xmatrix = np.array([[0.0, 1.0], [1.0, 0.0]], dtype=np.complex64) 109 | _ymatrix = np.array([[0.0, -1j], [1j, 0.0]], dtype=np.complex64) 110 | _zmatrix = np.array([[1.0, 0.0], [0.0, -1.0]], dtype=np.complex64) 111 | 112 | 113 | # Common single qubit gates as tn.Node objects 114 | # Note that functions are used because TensorNetwork connect/contract 115 | # functions modify Node objects 116 | def igate() -> tn.Node: 117 | """Returns a single qubit identity gate.""" 118 | return tn.Node(deepcopy(_imatrix), name="igate") 119 | 120 | 121 | def xgate() -> tn.Node: 122 | """Returns a Pauli X (NOT) gate.""" 123 | return tn.Node(deepcopy(_xmatrix), name="xgate") 124 | 125 | 126 | def ygate() -> tn.Node: 127 | """Returns a Pauli Y gate.""" 128 | return tn.Node(deepcopy(_ymatrix), name="ygate") 129 | 130 | 131 | def zgate() -> tn.Node: 132 | """Returns a Pauli Z gate.""" 133 | return tn.Node(deepcopy(_zmatrix), name="zmat") 134 | 135 | 136 | def hgate() -> tn.Node: 137 | """Returns a Hadamard gate.""" 138 | return tn.Node(deepcopy(_hmatrix), name="hgate") 139 | 140 | 141 | def rgate(seed: Optional[int] = None, angle_scale: float = 1.0): 142 | """Returns the random single qubit gate described in 143 | https://arxiv.org/abs/2002.07730. 144 | 145 | Args: 146 | seed: Seed for random number generator. 147 | angle_scale: Floating point value to scale angles by. Default 1. 148 | """ 149 | if seed: 150 | np.random.seed(seed) 151 | 152 | # Get the random parameters 153 | theta, alpha, phi = np.random.rand(3) * 2 * np.pi 154 | mx = np.sin(alpha) * np.cos(phi) 155 | my = np.sin(alpha) * np.sin(phi) 156 | mz = np.cos(alpha) 157 | 158 | theta *= angle_scale 159 | 160 | # Get the unitary 161 | unitary = expm( 162 | -1j * theta * (mx * _xmatrix + my * _ymatrix * mz * _zmatrix) 163 | ) 164 | return tn.Node(unitary) 165 | 166 | 167 | # TODO: Simplify implementation using computational_basis_state 168 | def computational_basis_projector(state: int, dim: int = 2) -> tn.Node: 169 | """Returns a projector onto a computational basis state which acts on a 170 | single qudit of dimension dim. 171 | 172 | Args: 173 | state: Basis state to project onto. 174 | dim: Dimension of the qudit. Default is two for qubits. 175 | 176 | Raises: 177 | ValueError: If state < 0, dim < 0, or state >= dim. 178 | """ 179 | if state < 0: 180 | raise ValueError(f"Argument state should be positive but is {state}.") 181 | 182 | if dim < 0: 183 | raise ValueError(f"Argument dim should be positive but is {dim}.") 184 | 185 | if state >= dim: 186 | raise ValueError( 187 | f"Requires state < dim but state = {state} and dim = {dim}." 188 | ) 189 | projector = np.zeros((dim, dim)) 190 | projector[state, state] = 1. 191 | return tn.Node(projector, name=f"|{state}><{state}|") 192 | 193 | 194 | # Common two qubit gates as np.ndarray objects 195 | _cnot_matrix = np.array( 196 | [ 197 | [1.0, 0.0, 0.0, 0.0], 198 | [0.0, 1.0, 0.0, 0.0], 199 | [0.0, 0.0, 0.0, 1.0], 200 | [0.0, 0.0, 1.0, 0.0], 201 | ] 202 | ) 203 | _cnot_matrix = np.reshape(_cnot_matrix, newshape=(2, 2, 2, 2)) 204 | _swap_matrix = np.array( 205 | [ 206 | [1.0, 0.0, 0.0, 0.0], 207 | [0.0, 0.0, 1.0, 0.0], 208 | [0.0, 1.0, 0.0, 0.0], 209 | [0.0, 0.0, 0.0, 1.0], 210 | ] 211 | ) 212 | _swap_matrix = np.reshape(_swap_matrix, newshape=(2, 2, 2, 2)) 213 | 214 | 215 | # Common two qubit gates as tn.Node objects 216 | def cnot() -> tn.Node: 217 | return tn.Node(deepcopy(_cnot_matrix), name="cnot") 218 | 219 | 220 | def swap() -> tn.Node: 221 | return tn.Node(deepcopy(_swap_matrix), name="swap") 222 | 223 | 224 | def cphase(exp: float) -> tn.Node: 225 | matrix = np.array([ 226 | [1., 0., 0., 0.], 227 | [0., 1., 0., 0.], 228 | [0., 0., 1., 0.], 229 | [0., 0., 0., np.exp(1j * 2 * np.pi * exp)] 230 | ], dtype=np.complex64) 231 | matrix = np.reshape(matrix, newshape=(2, 2, 2, 2)) 232 | return tn.Node(matrix, name="cphase") 233 | 234 | 235 | def random_two_qubit_gate(seed: Optional[int] = None) -> tn.Node: 236 | """Returns a random two-qubit gate. 237 | 238 | Args: 239 | seed: Seed for random number generator. 240 | """ 241 | if seed: 242 | np.random.seed(seed) 243 | unitary = unitary_group.rvs(dim=4) 244 | unitary = np.reshape(unitary, newshape=(2, 2, 2, 2)) 245 | return tn.Node(deepcopy(unitary), name="R2Q") 246 | 247 | 248 | def haar_random_unitary( 249 | nqudits: int = 2, 250 | qudit_dimension: int = 2, 251 | name: str = "Haar", 252 | seed: Optional[int] = None 253 | ) -> tn.Node: 254 | """Returns a Haar random unitary matrix of dimension 2**nqubits using 255 | the algorithm in https://arxiv.org/abs/math-ph/0609050. 256 | 257 | Args: 258 | nqudits: Number of qudits the unitary acts on. 259 | qudit_dimension: Dimension of each qudit. 260 | name: Name for the gate. 261 | seed: Seed for random number generator. 262 | 263 | Notes: 264 | See also 265 | http://qutip.org/docs/4.3/apidoc/functions.html#qutip.random_objects 266 | which was used as a template for this function. 267 | """ 268 | # Seed for random number generator 269 | rng = np.random.RandomState(seed) 270 | 271 | # Generate an N x N matrix of complex standard normal random variables 272 | units = np.array([1, 1j]) 273 | shape = (qudit_dimension ** nqudits, qudit_dimension ** nqudits) 274 | mat = np.sum(rng.randn(*(shape + (2,))) * units, axis=-1) / np.sqrt(2) 275 | 276 | # Do the QR decomposition 277 | qmat, rmat = np.linalg.qr(mat) 278 | 279 | # Create a diagonal matrix by rescaling the diagonal elements of rmat 280 | diag = np.diag(rmat).copy() 281 | diag /= np.abs(diag) 282 | 283 | # Reshape the tensor 284 | tensor = qmat * diag 285 | tensor = np.reshape(tensor, newshape=[qudit_dimension] * 2 * nqudits) 286 | return tn.Node(tensor, name=name) 287 | -------------------------------------------------------------------------------- /mpsim/gates_test.py: -------------------------------------------------------------------------------- 1 | """Unit tests for gates.""" 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | from mpsim.gates import ( 7 | computational_basis_projector, 8 | is_projector, 9 | is_unitary, 10 | igate, 11 | hgate, 12 | xgate, 13 | ygate, 14 | zgate, 15 | cnot, 16 | cphase, 17 | haar_random_unitary 18 | ) 19 | 20 | 21 | def test_is_unitary(): 22 | """Tests that common qubit gates are unitary.""" 23 | for gate in (igate(), hgate(), xgate(), ygate(), zgate(), cnot()): 24 | assert is_unitary(gate) 25 | 26 | for exp in np.linspace(start=0, stop=2 * np.pi, num=100): 27 | assert is_unitary(cphase(exp)) 28 | 29 | 30 | def test_qubit_pi0_projector(): 31 | """Tests correctness for |0><0| on a qubit.""" 32 | pi0 = computational_basis_projector(state=0) 33 | correct_tensor = np.array([[1., 0.], [0., 0.]]) 34 | assert np.array_equal(pi0.tensor, correct_tensor) 35 | assert is_projector(pi0) 36 | assert not is_unitary(pi0) 37 | assert pi0.__str__() == "|0><0|" 38 | 39 | 40 | def test_qubit_pi1_projector(): 41 | """Tests correctness for |0><0| on a qubit.""" 42 | pi1 = computational_basis_projector(state=1) 43 | correct_tensor = np.array([[0., 0.], [0., 1.]]) 44 | assert np.array_equal(pi1.tensor, correct_tensor) 45 | assert is_projector(pi1) 46 | assert not is_unitary(pi1) 47 | assert pi1.__str__() == "|1><1|" 48 | 49 | 50 | def test_invalid_projectors(): 51 | """Tests exceptions are raised for invalid projectors.""" 52 | # State must be positive 53 | with pytest.raises(ValueError): 54 | computational_basis_projector(state=-1) 55 | 56 | # Dimension must be positive 57 | with pytest.raises(ValueError): 58 | computational_basis_projector(state=2, dim=-1) 59 | 60 | # State must be less than dimension 61 | with pytest.raises(ValueError): 62 | computational_basis_projector(state=10, dim=8) 63 | 64 | 65 | def test_qutrit_projectors(): 66 | """Tests correctness for projectors on qutrits.""" 67 | dim = 3 68 | for state in (0, 1, 2): 69 | projector = computational_basis_projector(state, dim) 70 | correct_tensor = np.zeros((dim, dim)) 71 | correct_tensor[state, state] = 1. 72 | assert np.array_equal(projector.tensor, correct_tensor) 73 | assert is_projector(projector) 74 | assert not is_unitary(projector) 75 | assert projector.__str__() == f"|{state}><{state}|" 76 | 77 | 78 | def test_haar_random_unitary(): 79 | """Tests single-qubit and two-qubit Haar random unitaries.""" 80 | for n in [2, 3, 4]: 81 | for d in [2, 3, 5]: 82 | gate = haar_random_unitary(nqudits=n, qudit_dimension=d, seed=1) 83 | assert is_unitary(gate) 84 | -------------------------------------------------------------------------------- /mpsim/mpsim_cirq/__init__.py: -------------------------------------------------------------------------------- 1 | from mpsim.mpsim_cirq.circuits import MPSimCircuit, MPSOperation 2 | from mpsim.mpsim_cirq.simulator import MPSimulator 3 | -------------------------------------------------------------------------------- /mpsim/mpsim_cirq/circuits.py: -------------------------------------------------------------------------------- 1 | """Defines mpsim circuits as extensions of Cirq circuits.""" 2 | 3 | from typing import Dict, List 4 | 5 | import numpy as np 6 | 7 | import cirq 8 | import tensornetwork as tn 9 | from mpsim.core import MPSOperation, CannotConvertToMPSOperation 10 | 11 | 12 | def mps_operation_from_gate_operation( 13 | gate_operation: cirq.GateOperation, 14 | qudit_to_index_map: Dict[cirq.Qid, int] 15 | ) -> MPSOperation: 16 | """Constructs an MPS Operation from a gate operation. 17 | 18 | Args: 19 | gate_operation: A valid cirq.GateOperation or any child class. 20 | qudit_to_index_map: Dictionary to map qubits to MPS indices. 21 | 22 | Raises: 23 | CannotConvertToMPSOperation 24 | If the gate operation does not have a _unitary_ method. 25 | """ 26 | num_qudits = len(gate_operation.qubits) 27 | qudit_dimension = 2 # TODO: Check if all Cirq ops are qubit ops 28 | qudit_indices = tuple( 29 | [qudit_to_index_map[qudit] for qudit in gate_operation.qubits] 30 | ) 31 | 32 | if not gate_operation._has_unitary_(): 33 | raise CannotConvertToMPSOperation( 34 | f"Cannot convert operation {gate_operation} into an MPS Operation" 35 | " because the operation does not have a unitary." 36 | ) 37 | 38 | tensor = gate_operation._unitary_() 39 | tensor = np.reshape( 40 | tensor, newshape=[qudit_dimension] * 2 * num_qudits 41 | ) 42 | node = tn.Node(tensor) 43 | return MPSOperation(node, qudit_indices, qudit_dimension) 44 | 45 | 46 | MPSOperation.from_gate_operation = mps_operation_from_gate_operation 47 | 48 | 49 | class MPSimCircuit(cirq.Circuit): 50 | """Defines MPS Circuits which extend cirq.Circuits and can be simulated by 51 | an MPS Simulator. 52 | """ 53 | def __init__( 54 | self, 55 | cirq_circuit: cirq.Circuit, 56 | device: cirq.devices = cirq.devices.UNCONSTRAINED_DEVICE, 57 | qubit_order: cirq.ops.QubitOrder = cirq.ops.QubitOrder.DEFAULT 58 | ) -> None: 59 | """Constructor for MPSimCircuit. 60 | 61 | Args: 62 | cirq_circuit: Cirq circuit to create an MPS Sim circuit from. 63 | device: Device the circuit runs on. 64 | qubit_order: Ordering of qubits. 65 | """ 66 | super().__init__(cirq_circuit, device=device) 67 | self._qudit_to_index_map = { 68 | qubit: i for i, qubit in enumerate(sorted(self.all_qubits())) 69 | } # TODO: Account for qubit order instead of always using sorted order. 70 | self._mps_operations = self._translate_to_mps_operations() 71 | 72 | def _resolve_parameters_(self, param_resolver: cirq.study.ParamResolver): 73 | """Returns a circuit with all parameters resolved by the param_resolver. 74 | 75 | Args: 76 | param_resolver: Defines values for parameters in the circuit. 77 | """ 78 | mpsim_circuit = super()._resolve_parameters_(param_resolver) 79 | mpsim_circuit.device = self.device 80 | return mpsim_circuit 81 | 82 | def _translate_to_mps_operations(self) -> List[MPSOperation]: 83 | """Appends all operations in a circuit to MPS Operations.""" 84 | all_mps_operations = [] 85 | for (moment_index, moment) in enumerate(self): 86 | for operation in moment: 87 | all_mps_operations.append( 88 | MPSOperation.from_gate_operation( 89 | operation, 90 | self._qudit_to_index_map 91 | ) 92 | ) 93 | return all_mps_operations 94 | -------------------------------------------------------------------------------- /mpsim/mpsim_cirq/circuits_test.py: -------------------------------------------------------------------------------- 1 | """Tests for MPSIM Circuits.""" 2 | 3 | import numpy as np 4 | 5 | import cirq 6 | 7 | import mpsim 8 | from mpsim import MPSOperation 9 | from mpsim.mpsim_cirq import MPSimCircuit 10 | 11 | 12 | def test_from_gate_operation_not_gate(): 13 | """Tests creating an MPS Operation from a one-qubit gate operation.""" 14 | qubit = cirq.LineQubit(0) 15 | operation = cirq.ops.X.on(qubit) 16 | qubit_to_index_map = {qubit: 0} 17 | 18 | mps_operation = MPSOperation.from_gate_operation( 19 | operation, qubit_to_index_map 20 | ) 21 | assert mps_operation.is_valid() 22 | assert mps_operation.is_single_qudit_operation() 23 | assert mps_operation.qudit_indices == (0,) 24 | assert np.allclose(mps_operation.tensor(), operation._unitary_()) 25 | 26 | 27 | def test_mps_operation_from_gate_operation_zz_gate(): 28 | """Tests creating an MPS Operation from a gate operation.""" 29 | qreg = cirq.LineQubit.range(2) 30 | operation = cirq.ops.ZZ.on(*qreg) 31 | qubit_to_indices_map = {qreg[0]: 0, qreg[1]: 1} 32 | 33 | mps_operation = MPSOperation.from_gate_operation( 34 | operation, qubit_to_indices_map 35 | ) 36 | assert mps_operation.is_valid() 37 | assert mps_operation.is_two_qudit_operation() 38 | assert mps_operation.qudit_indices == (0, 1) 39 | assert np.allclose(mps_operation.tensor(), operation._unitary_()) 40 | 41 | 42 | def test_mps_operation_from_gate_operation_nonlocal_cnot(): 43 | """Tests converting a non-local CNOT to an MPS Operation.""" 44 | qreg = cirq.LineQubit.range(3) 45 | operation = cirq.ops.CNOT.on(qreg[0], qreg[2]) 46 | qubit_to_indices_map = {qreg[i]: i for i in range(len(qreg))} 47 | 48 | mps_operation = MPSOperation.from_gate_operation( 49 | operation, qubit_to_indices_map 50 | ) 51 | assert mps_operation.is_valid() 52 | assert mps_operation.qudit_indices == (0, 2) 53 | assert np.allclose(mps_operation.tensor(), operation._unitary_()) 54 | 55 | 56 | def test_instantiate_empty_circuit(): 57 | """Tests instantiating an mpsim circuit from an empty Cirq circuit.""" 58 | cirq_circuit = cirq.Circuit() 59 | mpsim_circuit = MPSimCircuit(cirq_circuit) 60 | assert len(list(mpsim_circuit.all_qubits())) == 0 61 | assert len(list(mpsim_circuit.all_operations())) == 0 62 | 63 | 64 | # TODO: A single qubit MPS should be invalid. (No error raised here because no 65 | # MPS is ever created, only MPSOperations. 66 | # To fix: Update MPS to allow for single qubit MPS (to simulate oneq circuits). 67 | def test_single_qubit_circuit_unconstrained_device(): 68 | """Tests creating and MPSimCircuit and checks correctness of the 69 | MPS Operations in the circuit. 70 | """ 71 | qbit = cirq.LineQubit(0) 72 | gate_operations = [ 73 | cirq.ops.H.on(qbit), cirq.ops.Z.on(qbit), cirq.ops.H.on(qbit) 74 | ] 75 | cirq_circuit = cirq.Circuit(gate_operations) 76 | 77 | mpsim_circuit = MPSimCircuit(cirq_circuit) 78 | mps_operations = mpsim_circuit._mps_operations 79 | 80 | assert len(mps_operations) == len(gate_operations) 81 | for gate_op, mps_op in zip(gate_operations, mps_operations): 82 | assert np.allclose(gate_op._unitary_(), mps_op.tensor()) 83 | assert mps_op.qudit_indices == (0,) 84 | assert mps_op.qudit_dimension == 2 85 | 86 | 87 | def test_mpsim_circuit_qubit_order(): 88 | for _ in range(20): 89 | qreg = cirq.LineQubit.range(2) 90 | gate_operations = [ 91 | cirq.ops.H.on(qreg[0]), cirq.ops.CNOT.on(*qreg) 92 | ] 93 | cirq_circuit = cirq.Circuit(gate_operations) 94 | 95 | mpsim_circuit = MPSimCircuit(cirq_circuit) 96 | assert mpsim_circuit._qudit_to_index_map == { 97 | qreg[0]: 0, qreg[1]: 1 98 | } 99 | 100 | 101 | def test_two_qubit_circuit_unconstrained_device(): 102 | """Tests correctness for translating MPS Operations in a circuit 103 | which prepares a Bell state. 104 | """ 105 | qreg = cirq.LineQubit.range(2) 106 | gate_operations = [ 107 | cirq.ops.H.on(qreg[0]), cirq.ops.CNOT.on(*qreg) 108 | ] 109 | cirq_circuit = cirq.Circuit(gate_operations) 110 | 111 | mpsim_circuit = MPSimCircuit(cirq_circuit) 112 | mps_operations = mpsim_circuit._mps_operations 113 | 114 | assert len(mps_operations) == len(gate_operations) 115 | for gate_op, mps_op in zip(gate_operations, mps_operations): 116 | assert np.allclose(gate_op._unitary_(), mps_op.tensor()) 117 | assert mps_op.qudit_dimension == 2 118 | 119 | assert mps_operations[0].qudit_indices == (0,) 120 | assert mps_operations[1].qudit_indices == (0, 1) 121 | 122 | 123 | def test_convert_and_manually_simulate_circuit_two_qubits(): 124 | """Tests the following: 125 | 126 | 1. Converting a Cirq circuit to an MPSimCircuit. 127 | 2. Applying MPS Operations in the MPSimCircuit to 128 | an MPS starting in the all zero state, checking 129 | for corrections. 130 | """ 131 | # Define the Cirq circuit 132 | qreg = cirq.LineQubit.range(2) 133 | gate_operations = [ 134 | cirq.ops.H.on(qreg[0]), cirq.ops.CNOT.on(*qreg) 135 | ] 136 | cirq_circuit = cirq.Circuit(gate_operations) 137 | 138 | # Convert to an MPSimCircuit 139 | mpsim_circuit = MPSimCircuit(cirq_circuit) 140 | mps_operations = mpsim_circuit._mps_operations 141 | 142 | # Apply the MPSOperation's from the MPSimCircuit 143 | mps = mpsim.MPS(nqudits=2) 144 | for mps_op in mps_operations: 145 | mps._apply_mps_operation(mps_op) 146 | 147 | # Check correctness for the final wavefunction 148 | correct = 1 / np.sqrt(2) * np.array([1., 0., 0., 1.]) 149 | assert np.allclose(mps.wavefunction(), correct) 150 | 151 | 152 | def test_convert_and_manually_simulate_circuit_nonlocal_operations_ghz_state(): 153 | """Tests the following: 154 | 155 | 1. Converting a Cirq circuit with non-local operations to an MPSimCircuit. 156 | 2. Applying MPS Operations in the MPSimCircuit to 157 | an MPS starting in the all zero state, checking 158 | for corrections. 159 | """ 160 | # Define the Cirq circuit 161 | qreg = cirq.LineQubit.range(3) 162 | gate_operations = [ 163 | cirq.ops.H.on(qreg[0]), 164 | cirq.ops.CNOT.on(qreg[0], qreg[1]), 165 | cirq.ops.CNOT.on(qreg[0], qreg[-1]) 166 | ] 167 | cirq_circuit = cirq.Circuit(gate_operations) 168 | 169 | # Convert to an MPSimCircuit 170 | mpsim_circuit = MPSimCircuit(cirq_circuit) 171 | mps_operations = mpsim_circuit._mps_operations 172 | 173 | # Apply the MPSOperation's from the MPSimCircuit 174 | mps = mpsim.MPS(nqudits=3) 175 | for mps_op in mps_operations: 176 | mps._apply_mps_operation(mps_op) 177 | 178 | # Check correctness for the final wavefunction 179 | correct = np.zeros(shape=(8,)) 180 | correct[0] = correct[-1] = 1. / np.sqrt(2) 181 | assert np.allclose(mps.wavefunction(), correct) 182 | -------------------------------------------------------------------------------- /mpsim/mpsim_cirq/simulator.py: -------------------------------------------------------------------------------- 1 | """Defines MPSIM Simulator for Cirq circuits.""" 2 | 3 | from typing import Any, List, Union 4 | 5 | from cirq import Circuit, ops, protocols, study 6 | from cirq.sim import SimulatesFinalState 7 | 8 | from mpsim import MPS 9 | from mpsim.mpsim_cirq.circuits import ( 10 | MPSimCircuit, mps_operation_from_gate_operation 11 | ) 12 | 13 | 14 | class MPSimulator(SimulatesFinalState): 15 | 16 | def __init__(self, options: dict = {}): 17 | """Initializes an MPS Simulator. 18 | 19 | Args: 20 | options: Dictionary of options for the simulator. 21 | 22 | Valid options: 23 | "maxsvals" (int): Number of singular values to keep after each 24 | two qubit gate. 25 | 26 | "fraction" (float): Number of singular values to keep expressed 27 | as a fraction of the maximum bond dimension 28 | for the given tensor. 29 | """ 30 | self._options = options 31 | 32 | def simulate_sweep( 33 | self, 34 | program: Union[Circuit, MPSimCircuit], 35 | params: study.Sweepable, 36 | qubit_order: ops.QubitOrderOrList = ops.QubitOrder.DEFAULT, 37 | initial_state: Any = None, 38 | ) -> List[Any]: 39 | """Simulates the supplied Circuit. 40 | 41 | This method returns a result which allows access to the entire 42 | wave function. In contrast to simulate, this allows for sweeping 43 | over different parameter values. 44 | 45 | Args: 46 | program: The circuit to simulate. 47 | params: Parameters to run with the program. 48 | qubit_order: Determines the canonical ordering of the qubits. This 49 | is often used in specifying the initial state, i.e. the 50 | ordering of the computational basis states. 51 | initial_state: The initial state for the simulation. The form of 52 | this state depends on the simulation implementation. See 53 | documentation of the implementing class for details. 54 | Returns: 55 | List of SimulationTrialResults for this run, one for each 56 | possible parameter resolver. 57 | """ 58 | if not isinstance(program, (Circuit, MPSimCircuit)): 59 | raise ValueError( 60 | f"Program is of type {type(program)} but should be either " 61 | "a cirq.Circuit or mpsim.mpsim_cirq.MPSimCircuit." 62 | ) 63 | 64 | param_resolvers = study.to_resolvers(params) 65 | 66 | trial_results = [] 67 | for prs in param_resolvers: 68 | solved_circuit = protocols.resolve_parameters(program, prs) 69 | 70 | ordered_qubits = ops.QubitOrder.as_qubit_order( 71 | qubit_order).order_for( 72 | solved_circuit.all_qubits()) 73 | 74 | qubit_to_index_map = { 75 | qubit: index for index, qubit in enumerate(ordered_qubits) 76 | } 77 | 78 | mps = MPS(nqudits=len(solved_circuit.all_qubits())) 79 | # TODO: Account for an input ordering of operations to apply here 80 | operations = [ 81 | mps_operation_from_gate_operation( 82 | gate_operation, qubit_to_index_map 83 | ) 84 | for gate_operation in solved_circuit.all_operations() 85 | ] 86 | mps.apply(operations, **self._options) 87 | trial_results.append(mps) 88 | return trial_results 89 | -------------------------------------------------------------------------------- /mpsim/mpsim_cirq/simulator_test.py: -------------------------------------------------------------------------------- 1 | """Unit tests for MPSimulator.""" 2 | 3 | from copy import deepcopy 4 | import numpy as np 5 | import sympy 6 | 7 | import cirq 8 | 9 | import pytest 10 | 11 | from mpsim import MPS 12 | from mpsim.mpsim_cirq.circuits import MPSimCircuit 13 | from mpsim.mpsim_cirq.simulator import MPSimulator 14 | 15 | 16 | def test_empty_circuit_raises_error(): 17 | """Tests that an error is raised when an empty circuit is simulated.""" 18 | with pytest.raises(ValueError): 19 | MPSimulator().simulate(cirq.Circuit()) 20 | 21 | 22 | def test_simulate_identity_circuit(): 23 | """Tests simulating an identity circuit on three qubits.""" 24 | qreg = cirq.LineQubit.range(3) 25 | circ = cirq.Circuit( 26 | cirq.identity_each(qbit) for qbit in qreg 27 | ) 28 | mps = MPSimulator().simulate(circ) 29 | assert np.allclose( 30 | mps.wavefunction(), circ.final_wavefunction() 31 | ) 32 | 33 | 34 | def test_simulate_bell_state_cirq_circuit(): 35 | """Tests correctness for the final MPS wavefunction when simulating a 36 | Cirq Circuit which prepares a Bell state. 37 | """ 38 | # Define the circuit 39 | qreg = cirq.LineQubit.range(2) 40 | circ = cirq.Circuit( 41 | cirq.ops.H.on(qreg[0]), 42 | cirq.ops.CNOT(*qreg) 43 | ) 44 | 45 | # Do the simulation using the MPS Simulator 46 | sim = MPSimulator() 47 | res = sim.simulate(circ) 48 | assert isinstance(res, MPS) 49 | assert np.allclose( 50 | res.wavefunction(), np.array([1., 0., 0., 1.]) / np.sqrt(2) 51 | ) 52 | 53 | 54 | def test_simulate_bell_state_mpsim_circuit(): 55 | """Tests correctness for the final MPS wavefunction when simulating a 56 | Cirq Circuit which prepares a Bell state. 57 | """ 58 | # Define the circuit 59 | qreg = cirq.LineQubit.range(2) 60 | circ = cirq.Circuit( 61 | cirq.ops.H.on(qreg[0]), 62 | cirq.ops.CNOT(*qreg) 63 | ) 64 | 65 | # Convert to an MPSimCircuit 66 | mpsim_circ = MPSimCircuit(circ) 67 | 68 | # Do the simulation using the MPS Simulator 69 | sim = MPSimulator() 70 | res = sim.simulate(mpsim_circ) 71 | assert isinstance(res, MPS) 72 | assert np.allclose( 73 | res.wavefunction(), np.array([1., 0., 0., 1.]) / np.sqrt(2) 74 | ) 75 | 76 | 77 | def test_simulate_bell_state_cirq_circuit_with_truncation(): 78 | """Tests correctness for the final MPS wavefunction when simulating a 79 | Cirq Circuit which prepares a Bell state using only one singular value. 80 | """ 81 | # Define the circuit 82 | qreg = cirq.LineQubit.range(2) 83 | circ = cirq.Circuit( 84 | cirq.ops.H.on(qreg[0]), 85 | cirq.ops.CNOT(*qreg) 86 | ) 87 | 88 | # Do the simulation using the MPS Simulator 89 | sim = MPSimulator(options={"maxsvals": 1}) 90 | res = sim.simulate(circ) 91 | assert isinstance(res, MPS) 92 | assert np.allclose( 93 | res.wavefunction(), np.array([1., 0., 0., 0.]) / np.sqrt(2) 94 | ) 95 | 96 | 97 | def test_simulate_one_dimensional_supremacy_circuit(): 98 | """Tests simulating a one-dimensional supremacy circuit 99 | using the MPSimulator. 100 | """ 101 | # Get the circuit 102 | circuit = cirq.experiments.generate_boixo_2018_supremacy_circuits_v2_grid( 103 | n_rows=1, n_cols=5, cz_depth=10, seed=1 104 | ) 105 | 106 | # Do the simulation using the MPS Simulator 107 | sim = MPSimulator() 108 | res = sim.simulate(circuit) 109 | assert isinstance(res, MPS) 110 | assert np.isclose(res.norm(), 1.) 111 | 112 | 113 | def test_simulate_ghz_circuits(): 114 | """Tests simulating GHZ circuits on multiple qubits.""" 115 | for n in range(3, 10): 116 | qreg = cirq.LineQubit.range(n) 117 | circ = cirq.Circuit( 118 | [cirq.ops.H.on(qreg[0])], 119 | [cirq.ops.CNOT.on(qreg[0], qreg[i]) for i in range(1, n)] 120 | ) 121 | cirq_wavefunction = circ.final_wavefunction() 122 | mps_wavefunction = MPSimulator().simulate(circ).wavefunction() 123 | assert np.allclose(mps_wavefunction, cirq_wavefunction) 124 | 125 | 126 | def test_simulate_qft_circuit(): 127 | """Tests simulating the QFT circuit on multiple qubits.""" 128 | for n in range(3, 10): 129 | qreg = cirq.LineQubit.range(n) 130 | circ = cirq.Circuit() 131 | 132 | # Add the gates for the QFT 133 | for i in range(n - 1, -1, -1): 134 | circ.append(cirq.ops.H.on(qreg[i])) 135 | for j in range(i - 1, -1, -1): 136 | circ.append( 137 | cirq.ops.CZPowGate(exponent=2**(j - i)).on( 138 | qreg[j], qreg[i])) 139 | assert len(list(circ.all_operations())) == n * (n + 1) // 2 140 | 141 | # Check correctness 142 | cirq_wavefunction = circ.final_wavefunction() 143 | mps_wavefunction = MPSimulator().simulate(circ).wavefunction() 144 | assert np.allclose(mps_wavefunction, cirq_wavefunction) 145 | 146 | 147 | def test_two_qubit_parameterized_circuit_single_parameter(): 148 | """Tests a two-qubit circuit with a single parameter.""" 149 | theta_name, theta_value = "theta", 1.0 150 | theta = sympy.Symbol(name=theta_name) 151 | qreg = cirq.LineQubit.range(2) 152 | circ = cirq.Circuit( 153 | cirq.ry(theta).on(qreg[0]), 154 | cirq.CNOT.on(*qreg) 155 | ) 156 | 157 | sim = MPSimulator() 158 | mps = sim.simulate( 159 | circ, param_resolver=cirq.ParamResolver({theta_name: theta_value}) 160 | ) 161 | solved_circuit = cirq.Circuit( 162 | cirq.ry(theta_value).on(qreg[0]), 163 | cirq.CNOT.on(*qreg) 164 | ) 165 | assert np.allclose(mps.wavefunction(), solved_circuit.final_wavefunction()) 166 | 167 | 168 | def test_parameterized_single_qubit_gates(): 169 | """Tests several different single-qubit gates with parameters.""" 170 | rng = np.random.RandomState(seed=1) 171 | n = 4 172 | symbols = [sympy.Symbol(str(i)) for i in range(n)] 173 | qreg = cirq.LineQubit.range(n) 174 | 175 | num_tests = 20 176 | for _ in range(num_tests): 177 | values = list(rng.rand(n)) 178 | circ = cirq.Circuit( 179 | cirq.ops.HPowGate(exponent=symbols[0]).on(qreg[0]), 180 | cirq.ops.ZPowGate(exponent=symbols[1]).on(qreg[1]), 181 | cirq.ops.PhasedXPowGate(phase_exponent=symbols[2]).on(qreg[2]), 182 | cirq.ops.ry(rads=symbols[3]).on(qreg[3]), 183 | ) 184 | 185 | # Get the final wavefunction using the Cirq Simulator 186 | solved_circuit = cirq.Circuit( 187 | cirq.ops.HPowGate(exponent=values[0]).on(qreg[0]), 188 | cirq.ops.ZPowGate(exponent=values[1]).on(qreg[1]), 189 | cirq.ops.PhasedXPowGate(phase_exponent=values[2]).on(qreg[2]), 190 | cirq.ops.ry(rads=values[3]).on(qreg[3]), 191 | ) 192 | cirq_wavefunction = solved_circuit.final_wavefunction() 193 | 194 | # Get the final wavefunction using the MPS Simulator 195 | sim = MPSimulator() 196 | mps = sim.simulate(circ, dict(zip(symbols, values))) 197 | 198 | assert np.allclose(mps.wavefunction(), cirq_wavefunction) 199 | 200 | 201 | def test_parameterized_local_two_qubit_gates(): 202 | """Tests several different two-qubit local gates with parameters.""" 203 | rng = np.random.RandomState(seed=1) 204 | n = 4 205 | symbols = [sympy.Symbol(str(i)) for i in range(n // 2)] 206 | qreg = cirq.LineQubit.range(n) 207 | 208 | num_tests = 20 209 | for _ in range(num_tests): 210 | values = list(rng.rand(n // 2)) 211 | circ = cirq.Circuit( 212 | cirq.ops.H.on_each(*qreg), 213 | cirq.ops.CZPowGate(exponent=symbols[0]).on(qreg[0], qreg[1]), 214 | cirq.ops.ZZPowGate(exponent=symbols[1]).on(qreg[2], qreg[3]) 215 | ) 216 | 217 | # Get the final wavefunction using the Cirq Simulator 218 | solved_circuit = cirq.Circuit( 219 | cirq.ops.H.on_each(*qreg), 220 | cirq.ops.CZPowGate(exponent=values[0]).on(qreg[0], qreg[1]), 221 | cirq.ops.ZZPowGate(exponent=values[1]).on(qreg[2], qreg[3]) 222 | ) 223 | cirq_wavefunction = solved_circuit.final_wavefunction() 224 | 225 | # Get the final wavefunction using the MPS Simulator 226 | sim = MPSimulator() 227 | mps = sim.simulate(circ, dict(zip(symbols, values))) 228 | 229 | assert np.allclose(mps.wavefunction(), cirq_wavefunction) 230 | 231 | 232 | def test_parameterized_nonlocal_two_qubit_gates(): 233 | """Tests a non-local two-qubit gate with a parameter.""" 234 | rng = np.random.RandomState(seed=1) 235 | symbols = [sympy.Symbol("theta")] 236 | qreg = cirq.LineQubit.range(3) 237 | 238 | num_tests = 20 239 | for _ in range(num_tests): 240 | values = list(rng.rand(1)) 241 | circ = cirq.Circuit( 242 | cirq.ops.H.on(qreg[0]), 243 | cirq.ops.X.on(qreg[1]), 244 | cirq.ops.CZPowGate(exponent=symbols[0]).on(qreg[0], qreg[2]), 245 | ) 246 | 247 | # Get the final wavefunction using the Cirq Simulator 248 | solved_circuit = cirq.Circuit( 249 | cirq.ops.H.on(qreg[0]), 250 | cirq.ops.X.on(qreg[1]), 251 | cirq.ops.CZPowGate(exponent=values[0]).on(qreg[0], qreg[2]), 252 | ) 253 | cirq_wavefunction = solved_circuit.final_wavefunction() 254 | 255 | # Get the final wavefunction using the MPS Simulator 256 | sim = MPSimulator() 257 | mps = sim.simulate(circ, dict(zip(symbols, values))) 258 | 259 | assert np.allclose(mps.wavefunction(), cirq_wavefunction) 260 | 261 | 262 | def test_three_qubit_gate_raise_value_error(): 263 | """Tests that a ValueError is raised when attempting to simulate a circuit 264 | with a three-qubit gate in it. 265 | """ 266 | qreg = [cirq.GridQubit(x, 0) for x in range(3)] 267 | circ = cirq.Circuit( 268 | cirq.ops.TOFFOLI.on(*qreg) 269 | ) 270 | with pytest.raises(ValueError): 271 | MPSimulator().simulate(circ) 272 | 273 | 274 | @pytest.mark.parametrize("nqubits", [2, 4, 8]) 275 | def test_random_circuits(nqubits: int): 276 | """Tests several random circuits and checks the output wavefunction against 277 | the Cirq simulator. 278 | """ 279 | # Gates to randomly choose from 280 | gate_domain = { 281 | cirq.ops.X: 1, 282 | cirq.ops.Y: 1, 283 | cirq.ops.Z: 1, 284 | cirq.ops.H: 1, 285 | cirq.ops.S: 1, 286 | cirq.ops.T: 1, 287 | cirq.ops.CNOT: 2, 288 | cirq.ops.CZ: 2, 289 | cirq.ops.SWAP: 2, 290 | cirq.ops.CZPowGate(): 2, 291 | cirq.ops.ISWAP: 2, 292 | cirq.ops.FSimGate(theta=0.2, phi=0.3): 2, 293 | } 294 | 295 | np.random.seed(1) 296 | for _ in range(50): 297 | circuit = cirq.testing.random_circuit( 298 | qubits=nqubits, 299 | n_moments=25, 300 | op_density=0.999, 301 | gate_domain=gate_domain 302 | ) 303 | correct = circuit.final_wavefunction() 304 | mps = MPSimulator().simulate(circuit) 305 | assert np.allclose(mps.wavefunction(), correct) 306 | 307 | 308 | def test_custom_gates(): 309 | """Tests simulating a circuit with custom Cirq gates.""" 310 | class MyTwoQubitGate(cirq.TwoQubitGate): 311 | def __init__(self, matrix): 312 | super().__init__() 313 | self._matrix = deepcopy(matrix) 314 | 315 | def _unitary_(self): 316 | return deepcopy(self._matrix) 317 | 318 | def __repr__(self): 319 | return "Random" 320 | 321 | random = cirq.testing.random_unitary(dim=4, random_state=1) 322 | rgate = MyTwoQubitGate(random) 323 | 324 | qreg = cirq.LineQubit.range(2) 325 | circ = cirq.Circuit( 326 | rgate.on(qreg[0], qreg[1]) 327 | ) 328 | 329 | cirq_wavefunction = circ.final_wavefunction() 330 | mpsim_wavefunction = MPSimulator().simulate(circ).wavefunction() 331 | assert np.allclose(mpsim_wavefunction, cirq_wavefunction) 332 | 333 | 334 | def test_simulate_sweep(): 335 | """Tests simulate_sweep with a parameterized circuit.""" 336 | qreg = cirq.LineQubit.range(2) 337 | theta = sympy.Symbol("theta") 338 | circ = cirq.Circuit( 339 | cirq.rx(theta).on(qbit) for qbit in qreg 340 | ) 341 | num_params = 50 342 | param_resolvers = [ 343 | {"theta": theta} for theta in np.linspace(0, 2 * np.pi, num_params) 344 | ] 345 | 346 | sim = MPSimulator() 347 | allmps = sim.simulate_sweep( 348 | circ, param_resolvers 349 | ) 350 | assert len(allmps) == num_params 351 | all_wavefunctions = [mps.wavefunction() for mps in allmps] 352 | correct_wavefunctions = [ 353 | circ._resolve_parameters_(pr).final_wavefunction() 354 | for pr in param_resolvers 355 | ] 356 | for (mpsim_wf, cirq_wf) in zip(all_wavefunctions, correct_wavefunctions): 357 | assert np.allclose(mpsim_wf, cirq_wf) 358 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.18.1 2 | tensornetwork==0.2.1 3 | scipy>=1.4.1 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | with open('README.md') as f: 5 | long_description = f.read() 6 | 7 | with open('development_requirements.txt') as f: 8 | dev_requirements = f.read().splitlines() 9 | 10 | 11 | requirements = [ 12 | requirement.strip() for requirement in open("requirements.txt").readlines() 13 | ] 14 | 15 | description = ("A package for using matrix product states " 16 | "to simulate quantum circuits.") 17 | 18 | setup(name="mpsim", 19 | version="0.1.0", 20 | description=description, 21 | long_description=long_description, 22 | long_description_content_type="text/markdown", 23 | author="Ryan LaRose", 24 | author_email="rlarose@google.com", 25 | url="https://github.com/grmlarose/mps", 26 | license="MIT", 27 | python_requires=">=3.6", 28 | install_requires=requirements, 29 | extras_require={ 30 | "development": set(dev_requirements), 31 | "test": dev_requirements 32 | }, 33 | packages=find_packages() 34 | ) 35 | --------------------------------------------------------------------------------