def popright(circuit: list[BaseGate], pinned_qubits: list[any], atol: float = 1e-8, use_matrix_commutation: bool = True, max_n_qubits_matrix: int = 10, simplify: bool = True, verbose: bool = False) -> Circuit: """ Remove gates outside the lightcone created by pinned_qubits. """ # Initialize new circuit new_circuit = Circuit() # Insert gates, one by one for gate in tqdm(reversed(circuit), disable=not verbose, total=len(circuit), desc='Pop'): insert_from_left(new_circuit, gate, atol=atol, use_matrix_commutation=use_matrix_commutation, max_n_qubits_matrix=max_n_qubits_matrix, simplify=simplify, pop=True, pinned_qubits=pinned_qubits, inplace=True) # Return simplified circuit return new_circuit
def remove_swap(circuit: Circuit) -> tuple[Circuit, dict[any, any]]: """ Iteratively remove SWAP's from circuit by actually swapping qubits. The output map will have the form new_qubit -> old_qubit. """ # Initialize map _qubits_map = {q: q for q in circuit.all_qubits()} # Initialize circuit _circ = Circuit() # Get ideal SWAP _SWAP = Gate('SWAP').matrix() # For each gate in circuit .. for gate in circuit: # Check if gate is close to SWAP if gate.n_qubits == 2 and gate.qubits and np.allclose( gate.matrix(), _SWAP): # If true, swap qubits _q0 = next(k for k, v in _qubits_map.items() if v == gate.qubits[0]) _q1 = next(k for k, v in _qubits_map.items() if v == gate.qubits[1]) _qubits_map[_q0], _qubits_map[_q1] = _qubits_map[_q1], _qubits_map[ _q0] # Otherwise, remap qubits and append else: # Get the right qubits _qubits = [ next(k for k, v in _qubits_map.items() if v == q) for q in gate.qubits ] # Append to the new circuit _circ.append(gate.on(_qubits)) # Return circuit and map return _circ, _qubits_map
def add_depolarizing_noise(circuit: Circuit, probs: {float, list[float, ...], dict[any, float]}, where: {'before', 'after'} = 'after', verbose: bool = False): """ Given a `Circuit`, add global depolarizing noise after each instance of a `Gate`, with the same locality as the gate. Note, noise will not be added after an instance of `BaseChannel` circuit: Circuit The `Circuit` which will be modified. Note, a new `Circuit` is returned (this is not in place). probs: {float, list[float, ...], dict[any, float]} Depolarizing probabilities for `circuit`. If `probs` is a single `float`, the same probability is applied to all gates regardless the number of qubits. If `probs` is a list, the k-th value is used as the probability for all the k-qubit gates. If `probs` is a `dict`, `probs[k]` will be used as probability for k-qubit gates. If `probs[k]` is missing, the probability for a k-qubit gate will fallback to `probs[any]` (if provided). where: {'before', 'after', 'both'} Add noise either `'before'` or `'after'` every gate (default: `after`). verbose: bool, optional Verbose output. """ from hybridq.circuit import Circuit # Check 'where' if where not in ['before', 'after']: raise ValueError("'where' can only be either 'before' or 'after'") # Convert circuit circuit = Circuit(circuit) # Convert probs probs = channel.__get_params(keys=sorted(set(g.n_qubits for g in circuit)), args=probs, value_type=float) # Define how to add noise def _add_noise(g): # Get probability p = probs[g.n_qubits] # Get noise noise = channel.GlobalDepolarizingChannel(g.qubits, p) # Return gates return [g] if isinstance(g, BaseChannel) else ( [g, noise] if where == 'after' else [noise, g]) # Update circuit return SuperCircuit(g for w in tqdm( circuit, disable=not verbose, desc='Add depolarizing noise') for g in _add_noise(w))
def _prepare_state(state, qubits): c = Circuit() for q, s in zip(qubits, state): if s == '0': pass elif s == '1': c.append(Gate('X', [q])) elif s == '+': c.append(Gate('H', [q])) elif s == '-': c.extend([Gate('X', [q]), Gate('H', [q])]) else: raise ValueError(f"Unexpected token '{s}'") return c
def get_rqc(n_qubits: int, n_gates: int, *, indexes: list[int] = None, randomize_power: bool = True, use_clifford_only: bool = False, use_unitary_only: bool = True, use_random_indexes: bool = False, verbose: bool = False): """ Generate random quantum circuit. """ from tqdm.auto import tqdm # Initialize circuit circuit = Circuit() # If not provided, generate indexes indexes = get_indexes(n_qubits, use_random_indexes=use_random_indexes ) if indexes is None else list(indexes) # Check that size is correct assert (len(indexes) == n_qubits) # Get random gates gates = (get_random_gate(randomize_power=randomize_power, use_unitary_only=use_unitary_only, use_clifford_only=use_clifford_only) for _ in range(n_gates)) # Assign random qubits, and return circuit return Circuit( gate.on([ indexes[i] for i in np.random.choice(n_qubits, gate.n_qubits, replace=False) ]) for gate in tqdm(gates, disable=not verbose, total=n_gates, desc='Generating random circuit'))
def isclose(a: Circuit, b: Circuit, use_matrix_commutation: bool = True, max_n_qubits_matrix: int = 10, atol: float = 1e-8, verbose: bool = False) -> bool: """ Check if `a` is close to `b` within the absolute tollerance `atol`. Parameters ---------- circuit: Circuit[BaseGate] `Circuit` to compare with. use_matrix_commutation: bool, optional Use commutation rules. See `hybridq.circuit.utils.simplify`. max_n_qubits_matrix: int, optional Matrices are computes for gates with up to `max_n_qubits_matrix` qubits (default: 10). atol: float, optional Absolute tollerance. Returns ------- bool `True` if the two circuits are close within the absolute tollerance `atol`, and `False` otherwise. See Also -------- hybridq.circuit.utils.simplify Example ------- >>> c = Circuit(Gate('H', [q]) for q in range(10)) >>> c.isclose(Circuit(g**1.1 for g in c)) False >>> c.isclose(Circuit(g**1.1 for g in c), atol=1e-1) True """ # Get simplified circuit s = simplify(a + b.inv(), use_matrix_commutation=use_matrix_commutation, max_n_qubits_matrix=max_n_qubits_matrix, atol=atol, verbose=verbose) return not s or all( isidentity([g], atol=atol) for g in tqdm(s, disable=not verbose, desc='Check'))
def flatten(a: Circuit) -> Circuit: """ Return a flattened circuit. More precisely, `flatten` iteratively looks for gates that provide `flatten` in order to return a flattened circuit. Parameters ---------- a: Circuit Circuit to flatten. Returns ------- Circuit Flattened circuit. """ return Circuit( g for gs in a for g in (gs if gs.provides('flatten') else (gs,)))
def to_matrix_gate(circuit: iter[BaseGate], complex_type: any = 'complex64', **kwargs) -> BaseGate: """ Convert `circuit` to a matrix `BaseGate`. Parameters ---------- circuit: iter[BaseGate] Circuit to convert to `BaseGate`. complex_type: any, optional Float type to use while converting to `BaseGate`. Returns ------- Gate `BaseGate` representing `circuit`. Example ------- >>> # Define circuit >>> circuit = Circuit( >>> [Gate('X', qubits=[0])**1.2, >>> Gate('ISWAP', qubits=[0, 1])**2.3]) >>> >>> gate = utils.to_matrix_gate(circuit) >>> gate Gate(name=MATRIX, qubits=[0, 1], U=np.array(shape=(4, 4), dtype=complex64)) >>> gate.U array([[ 0.09549151-0.29389262j, 0. +0.j , 0.9045085 +0.29389262j, 0. +0.j ], [ 0.13342446-0.41063824j, -0.08508356+0.26186025j, -0.13342446-0.04335224j, -0.8059229 -0.26186025j], [-0.8059229 -0.26186025j, -0.13342446-0.04335224j, -0.08508356+0.26186025j, 0.13342446-0.41063824j], [ 0. +0.j , 0.9045085 +0.29389262j, 0. +0.j , 0.09549151-0.29389262j]], dtype=complex64) """ # Convert iterable to Circuit circuit = Circuit(circuit) return Gate('MATRIX', qubits=circuit.all_qubits(), U=matrix(circuit, complex_type=complex_type, **kwargs))
def popleft(circuit: list[BaseGate], pinned_qubits: list[any], atol: float = 1e-8, use_matrix_commutation: bool = True, simplify: bool = True, verbose: bool = False) -> Circuit: """ Remove gates outside the lightcone created by pinned_qubits (starting from the right). """ return Circuit( reversed( popright(list(reversed(circuit)), pinned_qubits=pinned_qubits, atol=atol, use_matrix_commutation=use_matrix_commutation, simplify=simplify, verbose=verbose)))
def simplify(circuit: list[BaseGate], atol: float = 1e-8, use_matrix_commutation: bool = True, max_n_qubits_matrix: int = 10, remove_id_gates: bool = True, verbose: bool = False) -> Circuit: """ Compress together gates up to the specified number of qubits. """ # Initialize new circuit new_circuit = Circuit() # Remove gates if required if remove_id_gates: rev_circuit = (g for g in reversed(circuit) if g.name != 'I' and ( not g.provides('matrix') or g.n_qubits > max_n_qubits_matrix or not isidentity([g], atol=atol))) else: rev_circuit = reversed(circuit) # Insert gates, one by one for gate in tqdm(rev_circuit, disable=not verbose, total=len(circuit), desc='Simplify'): insert_from_left(new_circuit, gate, atol=atol, use_matrix_commutation=use_matrix_commutation, max_n_qubits_matrix=max_n_qubits_matrix, simplify=True, pop=False, pinned_qubits=None, inplace=True) # Return simplified circuit return new_circuit
def expand_iswap(circuit: Circuit) -> Circuit: """ Expand ISWAP's by iteratively replacing with SWAP's, CZ's and Phases. """ from copy import deepcopy # Get ideal iSWAP _iSWAP = Gate('ISWAP').matrix() # Initialize circuit _circ = Circuit() # For each gate in circuit .. for gate in circuit: # Check if gate is close to SWAP if gate.n_qubits == 2 and gate.qubits and np.allclose( gate.matrix(), _iSWAP): # Get tags _tags = gate.tags if gate.provides('tags') else {} # Expand iSWAP _ext = [ Gate('SWAP', qubits=gate.qubits, tags=_tags), Gate('CZ', qubits=gate.qubits, tags=_tags), Gate('P', qubits=[gate.qubits[0]], tags=_tags), Gate('P', qubits=[gate.qubits[1]], tags=_tags), ] # Append to circuit _circ.extend(_ext if gate.power == 1 else ( g**-1 for g in reversed(_ext))) # Otherwise, just append else: _circ.append(deepcopy(gate)) # Return circuit return _circ
def __convert(circuit: iter, parallel: {bool, int} = False, verbose: bool = False) -> Circuit: from tqdm.auto import tqdm # Get lenght try: total = len(circuit) except: total = None # Flatten circuit circuit = tuple(g for w in circuit for g in (w if isinstance(w, tuple) else [w])) # Parallelize if requested if parallel: from hybridq.utils import isintegral from multiprocessing import Pool from time import sleep # Get number of parallel threads if isinstance(parallel, bool): from os import cpu_count parallel = cpu_count() elif isintegral(parallel) and parallel > 0: parallel = int(parallel) else: raise ValueError("'parallel' must be a positive integer") with Pool(parallel) as pool, tqdm(total=len(circuit), desc='Converting circuit', disable=not verbose) as pbar: # Get map _map = [pool.apply_async(__transform, (g, )) for g in circuit] # Wait till ready while 1: # Count number of complete c = sum(m.ready() for m in _map) # Update pbar pbar.n = c pbar.update() # Break if all complete if c == len(_map): break # Sleep for a while sleep(0.1) # Return circuit return Circuit(m.get() for m in _map) # Otherwise, transform one by one else: return Circuit( tqdm(map(__transform, circuit), total=total, desc='Converting circuit', disable=not verbose))
def matrix(circuit: iter[BaseGate], order: iter[any] = None, complex_type: any = 'complex64', max_compress: int = 4, verbose: bool = False) -> numpy.ndarray: """ Return matrix representing `circuit`. Parameters ---------- circuit: iter[BaseGate] Circuit to get the matrix from. order: iter[any], optional If specified, a matrix is returned following the order given by `order`. Otherwise, `circuit.all_qubits()` is used. max_compress: int, optional To reduce the computational cost, `circuit` is compressed prior to compute the matrix. complex_type: any, optional Complex type to use to compute the matrix. verbose: bool, optional Verbose output. Returns ------- numpy.ndarray Unitary matrix of `circuit`. Example ------- >>> # Define circuit >>> circuit = Circuit([Gate('CX', [1, 0])]) >>> # Show qubits [0, 1] >>> circuit.all_qubits() >>> # Get matrix without specifying any qubits order >>> # (therefore using circuit.all_qubits() == [0, 1]) >>> utils.matrix() array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j], [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j], [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j]], dtype=complex64) >>> # Get matrix with a specific order of qubits >>> utils.matrix(Circuit([Gate('CX', [1, 0])]), order=[1, 0]) array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j], [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j]], dtype=complex64) """ # Convert iterable to Circuit circuit = Circuit(circuit) # Check order if order is not None: # Conver to list order = list(order) if set(order).difference(circuit.all_qubits()): raise ValueError( "'order' must be a valid permutation of indexes in 'Circuit'.") # Compress circuit if max_compress > 0: return matrix(Circuit( to_matrix_gate(c, complex_type=complex_type, max_compress=0) for c in compress(circuit, max_n_qubits=max_compress)), order=order, complex_type=complex_type, max_compress=0, verbose=verbose) # Get qubits qubits = circuit.all_qubits() n_qubits = len(qubits) # Initialize matrix U = np.reshape(np.eye(2**n_qubits, order='C', dtype=complex_type), [2] * (2 * n_qubits)) for g in tqdm(circuit, disable=not verbose): # Get gate's qubits _qubits = g.qubits _n_qubits = len(_qubits) # Get map _map = [qubits.index(q) for q in _qubits] _map += [x for x in range(n_qubits) if x not in _map] # Reorder qubits qubits = [qubits[x] for x in _map] # Update U U = np.reshape( g.matrix().astype(complex_type) @ np.reshape( np.transpose(U, _map + list(range(n_qubits, 2 * n_qubits))), (2**_n_qubits, 2**(2 * n_qubits - _n_qubits))), (2,) * (2 * n_qubits)) # Get U U = np.reshape( np.transpose(U, argsort(qubits) + list(range(n_qubits, 2 * n_qubits))), (2**n_qubits, 2**n_qubits)) # Reorder if required if order and order != circuit.all_qubits(): qubits = circuit.all_qubits() U = np.reshape( np.transpose(np.reshape(U, (2,) * (2 * n_qubits)), [qubits.index(q) for q in order] + [n_qubits + qubits.index(q) for q in order]), (2**n_qubits, 2**n_qubits)) # Check U has the right type and order assert (U.dtype == np.dtype(complex_type)) assert (U.data.c_contiguous) # Return matrix return U
def expectation_value(circuit: Circuit, op: Circuit, initial_state: str, **kwargs) -> float: """ Compute the expectation value of `op` for the given `circuit`, using `initial_state` as initial state. Parameters ---------- circuit: Circuit Circuit to simulate. op: Circuit Operator used to compute the expectation value. `op` must be a valid `Circuit` containing only Pauli gates (that is, either `I`, `X`, `Y` or `Z` gates) acting on different qubits. initial_state: str Initial state used to compute the expectation value. Valid tokens for `initial_state` are: - `0`: qubit is set to `0` in the computational basis, - `1`: qubit is set to `1` in the computational basis, - `+`: qubit is set to `+` state in the computational basis, - `-`: qubit is set to `-` state in the computational basis. Returns ------- float [, dict[any, any]] The expectation value of the operator `op`. If `return_info=True`, information gathered during the simulation are also returned. Other Parameters ---------------- `expectation_value` uses all valid parameters for `update_pauli_string`. See Also -------- `update_pauli_string` Example ------- >>> # Define circuit >>> circuit = Circuit( >>> [Gate('X', qubits=[0])**1.2, >>> Gate('ISWAP', qubits=[0, 1])**2.3]) >>> >>> # Define operator >>> op = Circuit([Gate('Z', qubits=[1])]) >>> >>> # Get expectation value >>> clifford.expectation_value(circuit=circuit, >>> op=op, >>> initial_state='11', >>> float_type='float64') -0.6271482580325515 """ # ==== Set default parameters ==== def _db_init(): return [0] def _collect(db, ops, ph): # Compute expectation value given pauli string if next((False for x in ops if x in 'XY'), True): db[0] += ph def _merge(db, db_new): db[0] += db_new[0] def _prepare_state(state, qubits): c = Circuit() for q, s in zip(qubits, state): if s == '0': pass elif s == '1': c.append(Gate('X', [q])) elif s == '+': c.append(Gate('H', [q])) elif s == '-': c.extend([Gate('X', [q]), Gate('H', [q])]) else: raise ValueError(f"Unexpected token '{s}'") return c kwargs.setdefault('use_mpi', None) kwargs.setdefault('return_info', False) # If use_mpi==False, force the non-use of MPI if kwargs['use_mpi'] is None and _detect_mpi: # Warn that MPI is used because detected warn("MPI has been detected. Using MPI.") # Set MPI to true kwargs['use_mpi'] = True # Get MPI info if kwargs['use_mpi']: from mpi4py import MPI _mpi_comm = MPI.COMM_WORLD _mpi_size = _mpi_comm.Get_size() _mpi_rank = _mpi_comm.Get_rank() # ================================ # Prepare initial state if type(initial_state) == str: # Check if len(initial_state) != len(circuit.all_qubits()): raise ValueError( f"'initial_state' has the wrong number of qubits " f"(expected {len(circuit.all_qubits())}, got {len(initial_state)})." ) # Get state initial_state = _prepare_state(initial_state, circuit.all_qubits()) else: raise ValueError( f"'{type(initial_state)}' not supported for 'initial_state'.") # Get expectation value _res = update_pauli_string(initial_state + circuit, op, db_init=_db_init, collect=_collect, merge=_merge, mpi_merge=False, **kwargs) # Collect results if kwargs['use_mpi'] and _mpi_size > 1: _all_res = _mpi_comm.gather(_res, root=0) if _mpi_rank == 0: if kwargs['return_info']: infos = {} for _, _infos in _all_res: for _k, _v in _infos.items(): if _k not in infos: infos[_k] = [_v] else: infos[_k].append(_v) exp_value = sum(ev[0] for ev, _ in _all_res) else: exp_value = sum(ev[0] for ev in _all_res) else: exp_value = infos = None else: if kwargs['return_info']: exp_value = _res[0][0] infos = _res[1] else: exp_value = _res[0] # Return expectation value if kwargs['return_info']: return exp_value, infos else: return exp_value
def from_qasm(qasm_string: str) -> Circuit: """ Convert a QASM circuit to `Circuit`. Parameters ---------- qasm_string: str QASM circuit to convert to `Circuit`. Returns ------- Circuit QAMS circuit converted to `Circuit`. Notes ----- The QASM language used in HybridQ is compatible with the standard QASM. However, HybridQ introduces few extensions, which are recognized by the parser using ``#@`` at the beginning of the line (``#`` at the beginning of the line represent a general comment in QASM). At the moment, the following QAMS extensions are supported: * **qubits**, used to store `qubits_map`, * **power**, used to store the power of the gate, * **tags**, used to store the tags associated to the gate, * **U**, used to store the matrix reprentation of the gate if gate name is `MATRIX` If `Gate.qubits` are not specified, a single `.` is used to represent the missing qubits. If `Gate.params` are missing, parameters are just omitted. Example ------- >>> from hybridq.extras.qasm import from_qasm >>> qasm_str = \"\"\" >>> 1 >>> #@ qubits = >>> #@ { >>> #@ "0": "42" >>> #@ } >>> #@ tags = >>> #@ { >>> #@ "params": false, >>> #@ "qubits": false >>> #@ } >>> rx . >>> #@ tags = >>> #@ { >>> #@ "params": true, >>> #@ "qubits": false >>> #@ } >>> ry . 1.23 >>> #@ tags = >>> #@ { >>> #@ "params": false, >>> #@ "qubits": true >>> #@ } >>> #@ power = 1.23 >>> rz 0 >>> #@ U = >>> #@ [ >>> #@ [ >>> #@ "0.7071067811865475", >>> #@ "0.7071067811865475" >>> #@ ], >>> #@ [ >>> #@ "0.7071067811865475", >>> #@ "-0.7071067811865475" >>> #@ ] >>> #@ ] >>> matrix . >>> \"\"\" >>> from_qasm(qasm_str) Circuit([ Gate(name=RX, tags={'params': False, 'qubits': False}) Gate(name=RY, params=[1.23], tags={'params': True, 'qubits': False}) Gate(name=RZ, qubits=[42], tags={'params': False, 'qubits': True})**1.23 Gate(name=MATRIX, U=np.array(shape=(2, 2), dtype=float64)) ]) """ # Initialize circuit circuit = Circuit() # Initialize tags _extra = None _power = None _conj = False _T = False _tags = None _qubits_map = None _U = None for line in (line for line in qasm_string.split('\n') if line and (line[0] != '#' or line[:2] == '#@')): if line[:2] == '#@': # Strip line _line = re.sub(r'\s+', '', line) if '#@tags=' in _line: if _tags is not None: raise ValueError('Format error.') # Initialize tags _tags = line.split('=')[-1] _extra = 'tags' elif '#@U=' in _line: if _U is not None: raise ValueError('Format error.') # Initialize matrix _U = line.split('=')[-1] _extra = 'U' elif '#@power=' in _line: if _power is not None: raise ValueError('Format error.') # Initialize power _power = line.split('=')[-1] _extra = 'power' elif '#@conj' in _line: if _conj is not False: raise ValueError('Format error.') # Initialize conjugation _conj = True elif '#@T' in _line: if _T is not False: raise ValueError('Format error.') # Initialize transposition _T = True elif '#@qubits=' in _line: if _qubits_map is not None: raise ValueError('Format error.') # Initialize qubits _qubits_map = line.split('=')[-1] _extra = 'qubits' elif _extra: # Update tags if _extra == 'tags': _tags += line.replace('#@', '') # Update matrix elif _extra == 'U': _U += line.replace('#@', '') # Update power elif _extra == 'power': _power += line.replace('#@', '') # Update qubits_map elif _extra == 'qubits': _qubits_map += line.replace('#@', '') # Otherwise, error else: raise ValueError('Format error.') else: # Restart _extra _extra = None # Strip everything after the first # line = line.split('#')[0].split() # Make few guesses about format if len(line) == 1: if _isint(line[0]): warn( f"Skipping '{' '.join(line)}' (most likely the number of qubits in the circuit)." ) continue else: warn( f"Skipping '{' '.join(line)}' (format is not understood)." ) continue # Make few guesses about format if _isint(line[0]): warn( f"Skipping {line[0]} in '{' '.join(line)}' (most likely the circuit layer)." ) del (line[0]) # Make few guesses about format if not _isstring(line[0]): warn( f"Skipping '{' '.join(line)}' (format is not understood).") continue if line[0].upper() == 'MATRIX': # Remove name from line del (line[0]) # Add tags if not _U: raise ValueError('Format error.') # Set gate _U = np.real_if_close( np.array([[complex(y) for y in x] for x in json.loads(_U)])) gate = Gate('MATRIX', U=_U) # Set qubits if present if line[0] != '.': # Set qubits gate.on([int(x) for x in line], inplace=True) # Reset _U = None else: # Set position _p = 0 # Initialize gate with name gate = Gate(line[_p]) # Check if qubits are provided _p += 1 if line[_p] != '.': # Set qubits gate.on([int(x) for x in line[_p:_p + gate.n_qubits]], inplace=True) _p += gate.n_qubits else: # Skip qubits _p += 1 if _p != len(line): if not gate.provides('params') and ( _p + gate.n_params) != len(line): raise ValueError('Format error.') gate.set_params( [float(x) for x in line[_p:_p + gate.n_params]], inplace=True) _p += gate.n_params if len(line) != _p: print(line, gate) # Add tags if _tags: gate._set_tags(json.loads(_tags)) # Apply power if _power: gate._set_power(float(_power)) # Add conjugation if _conj: gate._conj() # Add transposition if _T: gate._T() # Append gate to circuit circuit.append(gate) # Reset _tags = None # Reset power _power = None # Reset conjugation _conj = False # Reset transposition _T = False # Remap qubits if _qubits_map is not None: def _int(x): try: return int(x) except: return x _qubits_map = json.loads(_qubits_map) _qubits_map = {int(x): _int(y) for x, y in _qubits_map.items()} for gate in circuit: if gate.provides('qubits') and gate.qubits is not None: gate._on([_qubits_map[x] for x in gate.qubits]) return circuit
def _simulate_tn(circuit: any, initial_state: any, final_state: any, optimize: any, backend: any, complex_type: any, tensor_only: bool, verbose: bool, **kwargs): import quimb.tensor as tn import cotengra as ctg # Get random leaves_prefix leaves_prefix = ''.join( np.random.choice(list('abcdefghijklmnopqrstuvwxyz'), size=20)) # Initialize info _sim_info = {} # Alias for tn if optimize == 'tn': optimize = 'cotengra' if isinstance(circuit, Circuit): # Get number of qubits qubits = circuit.all_qubits() n_qubits = len(qubits) # If initial/final state is None, set to all .'s initial_state = '.' * n_qubits if initial_state is None else initial_state final_state = '.' * n_qubits if final_state is None else final_state # Initial and final states must be valid strings for state, sname in [(initial_state, 'initial_state'), (final_state, 'final_state')]: # Get alphabet from string import ascii_letters # Check if string if not isinstance(state, str): raise ValueError(f"'{sname}' must be a valid string.") # Deprecated error if any(x in 'xX' for x in state): from hybridq.utils import DeprecationWarning from warnings import warn # Warn the user that '.' is used to represent open qubits warn( "Since '0.6.3', letters in the alphabet are used to " "trace selected qubits (including 'x' and 'X'). " "Instead, '.' is used to represent an open qubit.", DeprecationWarning) # Check only valid symbols are present if set(state).difference('01+-.' + ascii_letters): raise ValueError(f"'{sname}' contains invalid symbols.") # Check number of qubits if len(state) != n_qubits: raise ValueError(f"'{sname}' has the wrong number of qubits " f"(expected {n_qubits}, got {len(state)})") # Check memory if 2**(initial_state.count('.') + final_state.count('.')) > kwargs['max_largest_intermediate']: raise MemoryError("Memory for the given number of open qubits " "exceeds the 'max_largest_intermediate'.") # Compress circuit if kwargs['compress']: if verbose: print( f"Compress circuit (max_n_qubits={kwargs['compress']}): ", end='', file=stderr) _time = time() circuit = utils.compress( circuit, kwargs['compress']['max_n_qubits'] if isinstance( kwargs['compress'], dict) else kwargs['compress'], verbose=verbose, **({ k: v for k, v in kwargs['compress'].items() if k != 'max_n_qubits' } if isinstance(kwargs['compress'], dict) else {})) circuit = Circuit( utils.to_matrix_gate(c, complex_type=complex_type) for c in circuit) if verbose: print(f"Done! ({time()-_time:1.2f}s)", file=stderr) # Get tensor network representation of circuit tensor, tn_qubits_map = utils.to_tn(circuit, return_qubits_map=True, leaves_prefix=leaves_prefix) # Define basic MPS _mps = { '0': np.array([1, 0]), '1': np.array([0, 1]), '+': np.array([1, 1]) / np.sqrt(2), '-': np.array([1, -1]) / np.sqrt(2) } # Attach initial/final state for state, ext in [(initial_state, 'i'), (final_state, 'f')]: for s, q in ((s, q) for s, q in zip(state, qubits) if s in _mps): inds = [f'{leaves_prefix}_{tn_qubits_map[q]}_{ext}'] tensor &= tn.Tensor(_mps[s], inds=inds, tags=inds) # For each unique letter, apply trace for x in set(initial_state + final_state).difference(''.join(_mps) + '.'): # Get indexes inds = [ f'{leaves_prefix}_{tn_qubits_map[q]}_i' for s, q in zip(initial_state, qubits) if s == x ] inds += [ f'{leaves_prefix}_{tn_qubits_map[q]}_f' for s, q in zip(final_state, qubits) if s == x ] # Apply trace tensor &= tn.Tensor(np.reshape([1] + [0] * (2**len(inds) - 2) + [1], (2, ) * len(inds)), inds=inds) # Simplify if requested if kwargs['simplify_tn']: tensor.full_simplify_(kwargs['simplify_tn']).astype_(complex_type) else: # Otherwise, just convert to the given complex_type tensor.astype_(complex_type) # Get contraction from heuristic if optimize == 'cotengra' and kwargs['max_iterations'] > 0: # Create local client if MPI has been detected (not compatible with Dask at the moment) if _mpi_env and kwargs['parallel']: from distributed import Client, LocalCluster _client = Client(LocalCluster(processes=False)) else: _client = None # Set cotengra parameters cotengra_params = lambda: ctg.HyperOptimizer( methods=kwargs['methods'], max_time=kwargs['max_time'], max_repeats=kwargs['max_repeats'], minimize=kwargs['minimize'], progbar=verbose, parallel=kwargs['parallel'], **kwargs['cotengra']) # Get optimized path opt = cotengra_params() info = tensor.contract(all, optimize=opt, get='path-info') # Get target size tli = kwargs['target_largest_intermediate'] # Repeat for the requested number of iterations for _ in range(1, kwargs['max_iterations']): # Break if largest intermediate is equal or smaller than target if info.largest_intermediate <= tli: break # Otherwise, restart _opt = cotengra_params() _info = tensor.contract(all, optimize=_opt, get='path-info') # Store the best if kwargs['minimize'] == 'size': if _info.largest_intermediate < info.largest_intermediate or ( _info.largest_intermediate == info.largest_intermediate and _opt.best['flops'] < opt.best['flops']): info = _info opt = _opt else: if _opt.best['flops'] < opt.best['flops'] or ( _opt.best['flops'] == opt.best['flops'] and _info.largest_intermediate < info.largest_intermediate): info = _info opt = _opt # Close client if exists if _client: _client.shutdown() _client.close() # Just return tensor if required if tensor_only: if optimize == 'cotengra' and kwargs['max_iterations'] > 0: return tensor, (info, opt) else: return tensor else: # Set tensor tensor = circuit if len(optimize) == 2 and isinstance( optimize[0], PathInfo) and isinstance( optimize[1], ctg.hyper.HyperOptimizer): # Get info and opt from optimize info, opt = optimize # Set optimization optimize = 'cotengra' else: # Get tensor and path tensor = circuit # Print some info if verbose: print( f'Largest Intermediate: 2^{np.log2(float(info.largest_intermediate)):1.2f}', file=stderr) print( f'Max Largest Intermediate: 2^{np.log2(float(kwargs["max_largest_intermediate"])):1.2f}', file=stderr) print(f'Flops: 2^{np.log2(float(info.opt_cost)):1.2f}', file=stderr) if optimize == 'cotengra': # Get indexes _inds = tensor.outer_inds() # Get input indexes and output indexes _i_inds = sort([x for x in _inds if x[-2:] == '_i'], key=lambda x: int(x.split('_')[1])) _f_inds = sort([x for x in _inds if x[-2:] == '_f'], key=lambda x: int(x.split('_')[1])) # Get order _inds = [_inds.index(x) for x in _i_inds + _f_inds] # Get slice finder sf = ctg.SliceFinder(info, target_size=kwargs['max_largest_intermediate']) # Find slices with tqdm(kwargs['temperatures'], disable=not verbose, leave=False) as pbar: for _temp in pbar: pbar.set_description(f'Find slices (T={_temp})') ix_sl, cost_sl = sf.search(temperature=_temp) # Get slice contractor sc = sf.SlicedContractor([t.data for t in tensor]) # Update infos _sim_info.update({ 'flops': info.opt_cost, 'largest_intermediate': info.largest_intermediate, 'n_slices': cost_sl.nslices, 'total_flops': cost_sl.total_flops }) # Print some infos if verbose: print( f'Number of slices: 2^{np.log2(float(cost_sl.nslices)):1.2f}', file=stderr) print(f'Flops+Cuts: 2^{np.log2(float(cost_sl.total_flops)):1.2f}', file=stderr) if kwargs['max_n_slices'] and sc.nslices > kwargs['max_n_slices']: raise RuntimeError( f'Too many slices ({sc.nslices} > {kwargs["max_n_slices"]})') # Contract tensor _li = np.log2(float(info.largest_intermediate)) _mli = np.log2(float(kwargs["max_largest_intermediate"])) _tensor = sc.gather_slices((sc.contract_slice( i, backend=backend ) for i in tqdm( range(sc.nslices), desc=f'Contracting tensor (li=2^{_li:1.0f}, mli=2^{_mli:1.1f})', leave=False))) # Create map _map = ''.join([get_symbol(x) for x in range(len(_inds))]) _map += '->' _map += ''.join([get_symbol(x) for x in _inds]) # Reorder tensor tensor = contract(_map, _tensor) # Deprecated ## Reshape tensor #if _inds: # if _i_inds and _f_inds: # tensor = np.reshape(tensor, (2**len(_i_inds), 2**len(_f_inds))) # else: # tensor = np.reshape(tensor, # (2**max(len(_i_inds), len(_f_inds)),)) else: # Contract tensor tensor = tensor.contract(optimize=optimize, backend=backend) if hasattr(tensor, 'inds'): # Get input indexes and output indexes _i_inds = sort([x for x in tensor.inds if x[-2:] == '_i'], key=lambda x: int(x.split('_')[1])) _f_inds = sort([x for x in tensor.inds if x[-2:] == '_f'], key=lambda x: int(x.split('_')[1])) # Transpose tensor tensor.transpose(*(_i_inds + _f_inds), inplace=True) # Deprecated ## Reshape tensor #if _i_inds and _f_inds: # tensor = np.reshape(tensor, (2**len(_i_inds), 2**len(_f_inds))) #else: # tensor = np.reshape(tensor, # (2**max(len(_i_inds), len(_f_inds)),)) if kwargs['return_info']: return tensor, _sim_info else: return tensor
def _simulate_evolution(circuit: iter[Gate], initial_state: any, final_state: any, optimize: any, backend: any, complex_type: any, verbose: bool, **kwargs): """ Perform simulation of the circuit by using the direct evolution of the quantum state. """ if _detect_mpi: warn("Detected MPI but optimize='evolution' does not support MPI.") # Initialize info _sim_info = {} # Convert iterable to circuit circuit = Circuit(circuit) # Get number of qubits qubits = circuit.all_qubits() n_qubits = len(qubits) # Check if core libraries have been loaded properly if any(not x for x in [_swap_core, _dot_core, _to_complex_core, _log2_pack_size]): warn("Cannot find C++ HybridQ core. " "Falling back to optimize='evolution-einsum' instead.") optimize = 'einsum' # If the system is too small, fallback to einsum if optimize == 'hybridq' and n_qubits <= max(10, _log2_pack_size): warn("The system is too small to use optimize='evolution-hybridq'. " "Falling back to optimize='evolution-einsum'") optimize = 'einsum' if verbose: print(f'# Optimization: {optimize}', file=stderr) # Check memory if 2**n_qubits > kwargs['max_largest_intermediate']: raise MemoryError( "Memory for the given number of qubits exceeds the 'max_largest_intermediate'." ) # If final_state is specified, warn user if final_state is not None: warn( f"'final_state' cannot be specified in optimize='{optimize}'. Ignoring 'final_state'." ) # Initial state must be provided if initial_state is None: raise ValueError( "'initial_state' must be specified for optimize='evolution'.") # Convert complex_type to np.dtype complex_type = np.dtype(complex_type) # Print info if verbose: print(f"Compress circuit (max_n_qubits={kwargs['compress']}): ", end='', file=stderr) _time = time() # Compress circuit circuit = utils.compress( circuit, kwargs['compress']['max_n_qubits'] if isinstance( kwargs['compress'], dict) else kwargs['compress'], verbose=verbose, skip_compression=[pr.FunctionalGate], **({ k: v for k, v in kwargs['compress'].items() if k != 'max_n_qubits' } if isinstance(kwargs['compress'], dict) else {})) # Check that FunctionalGate's are not compressed assert (all(not isinstance(g, pr.FunctionalGate) if len(x) > 1 else True for x in circuit for g in x)) # Compress everything which is not a FunctionalGate circuit = Circuit(g for c in (c if any( isinstance(g, pr.FunctionalGate) for g in c) else [utils.to_matrix_gate(c, complex_type=complex_type)] for c in circuit) for g in c) # Get state initial_state = prepare_state(initial_state, complex_type=complex_type) if isinstance( initial_state, str) else initial_state if verbose: print(f"Done! ({time()-_time:1.2f}s)", file=stderr) if optimize == 'hybridq': if complex_type not in ['complex64', 'complex128']: warn( "optimize=evolution-hybridq only support ['complex64', 'complex128']. Using 'complex64'." ) complex_type = np.dtype('complex64') # Get float_type float_type = np.real(np.array(1, dtype=complex_type)).dtype # Get C float_type c_float_type = { np.dtype('float32'): ctypes.c_float, np.dtype('float64'): ctypes.c_double }[float_type] # Load libraries _apply_U = _dot_core[float_type] # Get swap core _swap = _swap_core[float_type] # Get to_complex core _to_complex = _to_complex_core[complex_type] # Get states _psi = aligned.empty(shape=(2, ) + initial_state.shape, dtype=float_type, order='C', alignment=32) # Split in real and imaginary part _psi_re = _psi[0] _psi_im = _psi[1] # Check alignment assert (_psi_re.ctypes.data % 32 == 0) assert (_psi_im.ctypes.data % 32 == 0) # Get C-pointers _psi_re_ptr = _psi_re.ctypes.data_as(ctypes.POINTER(c_float_type)) _psi_im_ptr = _psi_im.ctypes.data_as(ctypes.POINTER(c_float_type)) # Initialize np.copyto(_psi_re, np.real(initial_state)) np.copyto(_psi_im, np.imag(initial_state)) # Create index maps _map = {q: n_qubits - x - 1 for x, q in enumerate(qubits)} _inv_map = [q for q, _ in sort(_map.items(), key=lambda x: x[1])] # Set largest swap_size _max_swap_size = 0 # Start clock _ini_time = time() # Apply all gates for gate in tqdm(circuit, disable=not verbose): # FunctionalGate if isinstance(gate, pr.FunctionalGate): # Get order order = tuple( q for q, _ in sorted(_map.items(), key=lambda x: x[1])[::-1]) # Apply gate to state new_psi, new_order = gate.apply(psi=_psi, order=order) # Copy back if needed if new_psi is not _psi: # Align if needed _psi = aligned.asarray(new_psi, order='C', alignment=32, dtype=_psi.dtype) # Redefine real and imaginary part _psi_re = _psi[0] _psi_im = _psi[1] # Get C-pointers _psi_re_ptr = _psi_re.ctypes.data_as( ctypes.POINTER(c_float_type)) _psi_im_ptr = _psi_im.ctypes.data_as( ctypes.POINTER(c_float_type)) # This can be eventually fixed ... if any(x != y for x, y in zip(order, new_order)): raise RuntimeError("'order' has changed.") elif gate.provides(['qubits', 'matrix']): # Check if any qubits is withing the pack_size if any(q in _inv_map[:_log2_pack_size] for q in gate.qubits): #@@@ Alternative way to always use the smallest swap size #@@@ #@@@ # Get positions #@@@ _pos = np.fromiter((_map[q] for q in gate.qubits), #@@@ dtype=int) #@@@ # Get smallest swap size #@@@ _swap_size = 0 if np.all(_pos >= _log2_pack_size) else next( #@@@ k #@@@ for k in range(_log2_pack_size, 2 * #@@@ max(len(_pos), _log2_pack_size) + 1) #@@@ if sum(_pos < k) <= k - _log2_pack_size) #@@@ # Get new order #@@@ _order = [ #@@@ x for x, q in enumerate(_inv_map[:_swap_size]) #@@@ if q not in gate.qubits #@@@ ] #@@@ _order += [ #@@@ x for x, q in enumerate(_inv_map[:_swap_size]) #@@@ if q in gate.qubits #@@@ ] if len(gate.qubits) <= 4: # Get new order _order = [ x for x, q in enumerate(_inv_map[:8]) if q not in gate.qubits ] _order += [ x for x, q in enumerate(_inv_map[:8]) if q in gate.qubits ] else: # Get qubit indexes for gate _gate_idxs = [_inv_map.index(q) for q in gate.qubits] # Get new order _order = [ x for x in range(n_qubits) if x not in _gate_idxs ][:_log2_pack_size] _order += [x for x in _gate_idxs if x < max(_order)] # Get swap size _swap_size = len(_order) # Update max swap size if _swap_size > _max_swap_size: _max_swap_size = _swap_size # Update maps _inv_map[:_swap_size] = [ _inv_map[:_swap_size][x] for x in _order ] _map.update( {q: x for x, q in enumerate(_inv_map[:_swap_size])}) # Apply swap _order = np.array(_order, dtype='uint32') _swap( _psi_re_ptr, _order.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)), n_qubits, len(_order)) _swap( _psi_im_ptr, _order.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)), n_qubits, len(_order)) # Get positions _pos = np.array([_map[q] for q in reversed(gate.qubits)], dtype='uint32') # Get matrix _U = np.asarray(gate.matrix(), dtype=complex_type, order='C') # Apply matrix if _apply_U( _psi_re_ptr, _psi_im_ptr, _U.ctypes.data_as(ctypes.POINTER(c_float_type)), _pos.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)), n_qubits, len(_pos)): raise RuntimeError('something went wrong') else: raise RuntimeError(f"'{gate}' not supported") # Check maps are still consistent assert (all(_inv_map[_map[q]] == q for q in _map)) # Swap back to the correct order _order = np.array([_inv_map.index(q) for q in reversed(qubits)][:_max_swap_size], dtype='uint32') _swap(_psi_re_ptr, _order.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)), n_qubits, len(_order)) _swap(_psi_im_ptr, _order.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)), n_qubits, len(_order)) # Stop clock _end_time = time() # Copy the results if kwargs['return_numpy_array']: _complex_psi = np.empty(_psi.shape[1:], dtype=complex_type) _to_complex( _psi_re_ptr, _psi_im_ptr, _complex_psi.ctypes.data_as(ctypes.POINTER(c_float_type)), 2**n_qubits) _psi = _complex_psi # Update info _sim_info['runtime (s)'] = _end_time - _ini_time elif optimize.split('-')[0] == 'einsum': optimize = '-'.join(optimize.split('-')[1:]) if not optimize: optimize = 'auto' # Split circuits to separate FunctionalGate's circuit = utils.compress( circuit, max_n_qubits=len(qubits), skip_compression=[pr.FunctionalGate], **({ k: v for k, v in kwargs['compress'].items() if k != 'max_n_qubits' } if isinstance(kwargs['compress'], dict) else {})) # Check that FunctionalGate's are not compressed assert (all( not isinstance(g, pr.FunctionalGate) if len(x) > 1 else True for x in circuit for g in x)) # Prepare initial_state _psi = initial_state # Initialize time _ini_time = time() for circuit in circuit: # Check assert (all(not isinstance(g, pr.FunctionalGate) for g in circuit) or len(circuit) == 1) # Apply gate if functional if len(circuit) == 1 and isinstance(circuit[0], pr.FunctionalGate): # Apply gate to state _psi, qubits = circuit[0].apply(psi=_psi, order=qubits) else: # Get gates and corresponding qubits _qubits, _gates = zip( *((c.qubits, np.reshape(c.matrix().astype(complex_type), (2, ) * (2 * len(c.qubits)))) for c in circuit)) # Initialize map _map = {q: get_symbol(x) for x, q in enumerate(qubits)} _count = n_qubits _path = ''.join((_map[q] for q in qubits)) # Generate map for _qs in _qubits: # Initialize local paths _path_in = _path_out = '' # Add incoming legs for _q in _qs: _path_in += _map[_q] # Add outcoming legs for _q in _qs: _map[_q] = get_symbol(_count) _count += 1 _path_out += _map[_q] # Update path _path = _path_out + _path_in + ',' + _path # Make sure that qubits order is preserved _path += '->' + ''.join([_map[q] for q in qubits]) # Contracts _psi = contract(_path, *reversed(_gates), _psi, backend=backend, optimize=optimize) # Block JAX until result is ready (for a more precise runtime) if backend == 'jax' and kwargs['block_until_ready']: _psi.block_until_ready() # Stop time _end_time = time() # Update info _sim_info['runtime (s)'] = _end_time - _ini_time else: raise ValueError(f"optimize='{optimize}' not implemented.") if verbose: print(f'# Runtime (s): {_sim_info["runtime (s)"]:1.2f}', file=stderr) # Return state if kwargs['return_info']: return _psi, _sim_info else: return _psi
def add_dephasing_noise(circuit: Circuit, probs: {float, list[float, ...], dict[any, float]}, pauli_indexes: {int, list[int, ...], dict[any, int]} = 3, where: {'before', 'after'} = 'after', verbose: bool = False): """ Given a `Circuit`, add dephasing noise after each instance of a `Gate`, which acts independently on the qubits of the gate. Note, noise will not be added after an instance of `BaseChannel` circuit: Circuit The `Circuit` which will be modified. Note, a new `Circuit` is returned (this is not in place). probs: {float, list[float, ...], dict[any, float]} Dephasing probabilities for `circuit`. If `probs` is a single `float`, the same probability is applied to all qubits. If `probs` is a list, the k-th value is used as the probability for the k-th qubit. If `probs` is a `dict`, `probs[q]` will be used as probability for the qubit `q`. If `probs[q]` is missing, the probability for a qubit `q` will fallback to `probs[any]` (if provided). pauli_indexes: {int, list[int, ...], dict[any, int]} Pauli indexes representing the dephasing axis (Pauli matrix). If `pauli_indexes` is a single `int`, the same axis is applied to all qubits. If `pauli_indexes` is a list, the k-th value is used as the axis for the k-th qubit. If `pauli_indexes` is a `dict`, `pauli_indexes[q]` will be used as axis for the qubit `q`. If `pauli_indexes[q]` is missing, the axis for a qubit `q` will fallback to `pauli_indexes[any]` (if provided). where: {'before', 'after', 'both'} Add noise either `'before'` or `'after'` every gate (default: `after`). verbose: bool, optional Verbose output. """ from hybridq.circuit import Circuit # Check 'where' if where not in ['before', 'after']: raise ValueError("'where' can only be either 'before' or 'after'") # Convert circuit circuit = Circuit(circuit) # Get all qubits qubits = circuit.all_qubits() # Convert gammas and probs probs = channel.__get_params(qubits, probs, value_type=float) pauli_indexes = channel.__get_params(qubits, pauli_indexes, value_type=int) # Define how to add noise def _add_noise(g): # Get gammas and probs _probs = {q: probs[q] for q in g.qubits} _axis = {q: pauli_indexes[q] for q in g.qubits} # Get noise noise = channel.LocalDephasingChannel(g.qubits, p=_probs, pauli_index=_axis) # Return gates return [g] if isinstance( g, BaseChannel) else ((g, ) + noise if where == 'after' else noise + (g, )) # Update circuit return SuperCircuit(g for w in tqdm( circuit, disable=not verbose, desc='Add amplitude damping noise') for g in _add_noise(w))
def to_nx(circuit: iter[BaseGate], add_final_nodes: bool = True, node_tags: dict = None, edge_tags: dict = None, return_qubits_map: bool = False, leaves_prefix: str = 'q') -> networkx.Graph: """ Return graph representation of circuit. `to_nx` is deterministic, so it can be reused elsewhere. Parameters ---------- circuit: iter[BaseGate] Circuit to get graph representation from. add_final_nodes: bool, optional Add final nodes for each qubit to the graph representation of `circuit`. node_tags: dict, optional Add specific tags to nodes. edge_tags: dict, optional Add specific tags to edges. return_qubits_map: bool, optional Return map associated to the Circuit qubits. leaves_prefix: str, optional Specify prefix to use for leaves. Returns ------- networkx.Graph Graph representing `circuit`. Example ------- >>> import networkx as nx >>> >>> # Define circuit >>> circuit = Circuit( >>> [Gate('X', qubits=[0])**1.2, >>> Gate('ISWAP', qubits=[0, 1])**2.3], Gate('H', [1])) >>> >>> # Draw graph >>> nx.draw_planar(utils.to_nx(circuit)) .. image:: ../../images/circuit_nx.png """ import networkx as nx # Initialize if node_tags is None: node_tags = {} if edge_tags is None: edge_tags = {} # Check if node is a leaf def _is_leaf(node): return type(node) == str and node[:len(leaves_prefix)] == leaves_prefix # Convert iterable to Circuit circuit = Circuit(circuit) # Get graph graph = nx.DiGraph() # Get qubits qubits = circuit.all_qubits() # Get qubits_map qubits_map = {q: i for i, q in enumerate(qubits)} # Check that no qubits is 'confused' as leaf if any(_is_leaf(q) for q in qubits): raise ValueError( f"No qubits must start with 'leaves_prefix'={leaves_prefix}.") # Add first layer for q in qubits: graph.add_node(f'{leaves_prefix}_{qubits_map[q]}_i', qubits=[q], **node_tags) # Last leg last_leg = {q: f'{leaves_prefix}_{qubits_map[q]}_i' for q in qubits} # Build network for x, gate in enumerate(circuit): # Add node graph.add_node(x, circuit=Circuit([gate]), qubits=sort(gate.qubits), **node_tags) # Add edges (time directed) graph.add_edges_from([(last_leg[q], x) for q in gate.qubits], **edge_tags) # Update last_leg last_leg.update({q: x for q in gate.qubits}) # Add last indexes if required if add_final_nodes: for q in qubits: graph.add_node(f'{leaves_prefix}_{qubits_map[q]}_f', qubits=[q], **node_tags) graph.add_edges_from([(x, f'{leaves_prefix}_{qubits_map[q]}_f') for q, x in last_leg.items()], **edge_tags) if return_qubits_map: return graph, qubits_map else: return graph
def insert_from_left(circuit: iter[BaseGate], gate: BaseGate, atol: float = 1e-8, *, use_matrix_commutation: bool = True, max_n_qubits_matrix: int = 10, simplify: bool = True, pop: bool = False, pinned_qubits: list[any] = None, inplace: bool = False) -> Circuit: """ Add a gate to circuit starting from the left, commuting with existing gates if necessary. Parameters ---------- circuit: Circuit `gate` will be added to `circuit`. gate: Gate Gate to add to `circuit`. atol: float, optional Absolute tollerance while simplifying. use_matrix_commutation: bool, optional Use matrix commutation while simplifying `circuit`. max_n_qubits_matrix: int, optional Matrices are computes for gates with up to `max_n_qubits_matrix` qubits (default: 10). simplify: bool, optional Simplify `circuit` while adding `gate` (default: `True`). pop: bool, optional Remove `gate` if it commutes with all gates in `circuit` (default: `False`). pinned_qubits: list[any], optional If `pop` is `True`, remove gates unless `gate` share qubits with `pinned_qubits` (default: `None`). inplace: bool, optional If `True`, add `gate` to `circuit` in-place (default: `False`) Returns ------- Circuit Circuit with `gate` added to it. """ from copy import deepcopy # Copy circuit if required if not inplace: circuit = Circuit(circuit, copy=True) # Get qubits if gate.provides('qubits') and gate.qubits is not None: _qubits = set(gate.qubits) else: # If gate has not qubits, just append to the left circuit.insert(0, deepcopy(gate)) return circuit # Iterate over all the gates for _p, _g in enumerate(circuit): # Remove if gate simplifies with _g try: if simplify and gate.inv().isclose(_g, atol=atol): del (circuit[_p]) return circuit except: pass # Otherwise, check if gate can commute with _g. If not, insert gate # and exit loop. _commute = False try: if _g.n_qubits <= max_n_qubits_matrix: _commute |= not _qubits.intersection(_g.qubits) _commute |= use_matrix_commutation and gate.commutes_with( _g, atol=atol) except: pass finally: if not _commute: circuit.insert(_p, deepcopy(gate)) return circuit # If commutes with everything, just append at the end if not pop or _qubits.intersection(pinned_qubits): circuit.append(deepcopy(gate)) # Return circuit return circuit
def update_pauli_string(circuit: Circuit, pauli_string: {Circuit, dict[str, float]}, phase: float = 1, parallel: {bool, int} = False, return_info: bool = False, use_mpi: bool = None, compress: int = 4, simplify: bool = True, remove_id_gates: bool = True, float_type: any = 'float32', verbose: bool = False, **kwargs) -> defaultdict: """ Evolve density matrix accordingly to `circuit` using `pauli_string` as initial product state. The evolved density matrix will be represented as a set of different Pauli strings, each of them with a different phase, such that their sum corresponds to the evolved density matrix. The number of branches depends on the number of non-Clifford gates in `circuit`. Parameters ---------- circuit: Circuit Circuit to use to evolve `pauli_string`. pauli_string: {Circuit, dict[str, float]} Pauli string to be evolved. `pauli_string` must be a `Circuit` composed of single qubit Pauli `Gate`s (that is, either `Gate('I')`, `Gate('X')`, `Gate('Y')` or `Gate('Z')`), each one acting on every qubit of `circuit`. If a dictionary is provided, every key of `pauli_string` must be a valid Pauli string. The size of each Pauli string must be equal to the number of qubits in `circuit`. Values in `pauli_string` will be used as inital phase for the given string. phase: float, optional Initial phase for `pauli_string`. atol: float, optional Discard all Pauli strings that have an absolute amplitude smaller than `atol`. parallel: int, optional Parallelize simulation (where possible). If `True`, the number of available cpus is used. Otherwise, a `parallel` number of threads is used. return_info: bool Return extra information collected during the evolution. use_mpi: bool, optional Use `MPI` if available. Unless `use_mpi=False`, `MPI` will be used if detected (for instance, if `mpiexec` is used to called HybridQ). If `use_mpi=True`, force the use of `MPI` (in case `MPI` is not automatically detected). compress: int, optional Compress `Circuit` using `utils.compress` prior the simulation. simplify: bool, optional Simplify `Circuit` using `utils.simplify` prior the simulation. remove_id_gates: bool, optional Remove `ID` gates prior the simulation. float_type: any, optional Float type to use for the simulation. verbose: bool, optional Verbose output. Returns ------- dict[str, float] [, dict[any, any]] If `return_info=False`, `update_pauli_string` returns a `dict` of Pauli strings and the corresponding amplitude. The full density matrix can be reconstructed by resumming over all the Pauli string, weighted with the corresponding amplitude. If `return_info=True`, information gathered during the simulation are also returned. Other Parameters ---------------- eps: float, optional (default: auto) Do not branch if the branch weight for the given non-Clifford operation is smaller than `eps`. `atol=1e-7` if `float_type=float32`, otherwise `atol=1e-8` if `float_type=float64`. atol: float, optional (default: auto) Remove elements from final state if such element as an absolute amplitude smaller than `atol`. `atol=1e-8` if `float_type=float32`, otherwise `atol=1e-12` if `float_type=float64`. branch_atol: float, optional Stop branching if the branch absolute amplitude is smaller than `branch_atol`. If not specified, it will be equal to `atol`. max_breadth_first_branches: int (default: auto) Max number of branches to collect using breadth first search. The number of branches collect during the breadth first phase will be split among the different threads (or nodes if using `MPI`). n_chunks: int (default: auto) Number of chunks to divide the branches obtained during the breadth first phase. The default value is twelve times the number of threads. max_virtual_memory: float (default: 80) Max virtual memory (%) that can be using during the simulation. If the used virtual memory is above `max_virtual_memory`, `update_pauli_string` will raise an error. sleep_time: float (default: 0.1) Completition of parallel processes is checked every `sleep_time` seconds. Example ------- >>> from hybridq.circuit import utils >>> import numpy as np >>> >>> # Define circuit >>> circuit = Circuit( >>> [Gate('X', qubits=[0])**1.2, >>> Gate('ISWAP', qubits=[0, 1])**2.3]) >>> >>> # Define Pauli string >>> pauli_string = Circuit([Gate('Z', qubits=[1])]) >>> >>> # Get density matrix decomposed in Pauli strings >>> dm = clifford.update_pauli_string(circuit=circuit, >>> pauli_string=pauli_string, >>> float_type='float64') >>> >>> dm defaultdict(<function hybridq.circuit.simulation.clifford.update_pauli_string.<locals>._db_init.<locals>.<lambda>()>, {'IZ': 0.7938926261462365, 'YI': -0.12114687473997318, 'ZI': -0.166744368113685, 'ZX': 0.2377641290737882, 'YX': -0.3272542485937367, 'XY': -0.40450849718747345}) >>> # Reconstruct density matrix >>> U = sum(phase * np.kron(Gate(g1).matrix(), >>> Gate(g2).matrix()) for (g1, g2), phase in dm.items()) >>> >>> U array([[ 0.62714826+0.j , 0.23776413+0.j , 0. +0.12114687j, 0. +0.73176275j], [ 0.23776413+0.j , -0.96063699+0.j , 0. -0.07725425j, 0. +0.12114687j], [ 0. -0.12114687j, 0. +0.07725425j, 0.96063699+0.j , -0.23776413+0.j ], [ 0. -0.73176275j, 0. -0.12114687j, -0.23776413+0.j , -0.62714826+0.j ]]) >>> np.allclose(utils.matrix(circuit + pauli_string + circuit.inv()), >>> U, >>> atol=1e-8) True >>> U[0b11, 0b11] (-0.6271482580325515+0j) """ # ==== Set default parameters ==== # If use_mpi==False, force the non-use of MPI if use_mpi is None and _detect_mpi: # Warn that MPI is used because detected warn("MPI has been detected. Using MPI.") # Set MPI to true use_mpi = True # If parallel==True, use number of cpus if type(parallel) is bool: parallel = cpu_count() if parallel else 1 else: parallel = int(parallel) if parallel <= 0: warn("'parallel' must be a positive integer. Setting parallel=1") parallel = 1 # utils.globalize may not work properly on MacOSX systems .. for now, let's # disable parallelization for MacOSX if parallel > 1: from platform import system from warnings import warn if system() == 'Darwin': warn( "'utils.globalize' may not work on MacOSX. Disabling parallelization." ) parallel = 1 # Fix atol if 'atol' in kwargs: atol = kwargs['atol'] del (kwargs['atol']) else: float_type = np.dtype(float_type) if float_type == np.float64: atol = 1e-12 elif float_type == np.float32: atol = 1e-8 else: raise ValueError(f'Unsupported array dtype: {float_type}') # Fix branch_atol if 'branch_atol' in kwargs: branch_atol = kwargs['branch_atol'] del (kwargs['branch_atol']) else: branch_atol = atol # Fix eps if 'eps' in kwargs: eps = kwargs['eps'] del (kwargs['eps']) else: float_type = np.dtype(float_type) if float_type == np.float64: eps = 1e-8 elif float_type == np.float32: eps = 1e-7 else: raise ValueError(f'Unsupported array dtype: {float_type}') # Set default db initialization def _db_init(): return defaultdict(int) # Set default transform def _transform(ps): # Join bitstring return ''.join({_X: 'X', _Y: 'Y', _Z: 'Z', _I: 'I'}[op] for op in ps) # Set default collect def _collect(db, ps, ph): # Update final paulis db[ps] += ph # Remove elements close to zero if abs(db[ps]) < atol: del (db[ps]) # Set default merge def _merge(db, db_new, use_tuple=False): # Update final paulis for ps, ph in db_new if use_tuple else db_new.items(): # Collect results kwargs['collect'](db, ps, ph) kwargs.setdefault('max_breadth_first_branches', min(4 * 12 * parallel, 2**14)) kwargs.setdefault('n_chunks', 12 * parallel) kwargs.setdefault('max_virtual_memory', 80) kwargs.setdefault('sleep_time', 0.1) kwargs.setdefault('collect', _collect) kwargs.setdefault('transform', _transform) kwargs.setdefault('merge', _merge) kwargs.setdefault('db_init', _db_init) # Get MPI info if use_mpi: from mpi4py import MPI _mpi_comm = MPI.COMM_WORLD _mpi_size = _mpi_comm.Get_size() _mpi_rank = _mpi_comm.Get_rank() kwargs.setdefault('max_breadth_first_branches_mpi', min(_mpi_size * 2**9, 2**14)) kwargs.setdefault('mpi_chunk_max_size', 2**20) kwargs.setdefault('mpi_merge', True) # Get complex_type from float_type complex_type = (np.array([1], dtype=float_type) + 1j * np.array([1], dtype=float_type)).dtype # Local verbose _verbose = verbose and (not use_mpi or _mpi_rank == 0) # =========== CHECKS ============= if type(pauli_string) == Circuit: from collections import Counter # Initialize error message _err_msg = "'pauli_string' must contain only I, X, Y and Z gates acting on different qubits." # Check qubits match with circuit if any(g.n_qubits != 1 or not g.qubits for g in pauli_string) or set( pauli_string.all_qubits()).difference( circuit.all_qubits()) or set( Counter(gate.qubits[0] for gate in pauli_string).values()).difference( [1]): raise ValueError(_err_msg) # Get ideal paulis _ig = list(map(lambda n: Gate(n).matrix(), 'IXYZ')) # Get the correct pauli def _get_pauli(gate): # Get matrix U = gate.matrix() # Get right pauli p = next( (p for x, p in enumerate('IXYZ') if np.allclose(_ig[x], U)), None) # If not found, raise error if not p: raise ValueError(_err_msg) # Otherwise, return pauli return Gate(p, qubits=gate.qubits) # Reconstruct paulis pauli_string = Circuit(map(_get_pauli, pauli_string)) else: # Check that all strings only have I,X,Y,Z tokens _n_qubits = len(circuit.all_qubits()) if any( set(p).difference('IXYZ') or len(p) != _n_qubits for p in pauli_string): raise ValueError( f"'pauli_string' must contain only I, X, Y and Z gates acting on different qubits." ) # ================================ # Start pre-processing time _prep_time = time() # Get qubits _qubits = circuit.all_qubits() # Remove ID gates if remove_id_gates: circuit = Circuit(gate for gate in circuit if gate.name != 'I') # Simplify circuit if simplify: # Get qubits to pin if type(pauli_string) == Circuit: # Pinned qubits _pinned_qubits = pauli_string.all_qubits() else: # Find qubits to pin _pinned_qubits = set.union( *({q for q, g in zip(_qubits, p) if g != 'I'} for p in pauli_string)) # Simplify circuit = utils.simplify(circuit, remove_id_gates=remove_id_gates, verbose=_verbose) circuit = utils.popright(utils.simplify(circuit), pinned_qubits=set(_pinned_qubits).intersection( circuit.all_qubits()), verbose=_verbose) # Compress circuit circuit = Circuit( utils.to_matrix_gate(c, complex_type=complex_type) for c in tqdm(utils.compress(circuit, max_n_qubits=compress), disable=not _verbose, desc=f"Compress ({int(compress)})")) # Pad missing qubits circuit += Circuit( Gate('MATRIX', [q], U=np.eye(2)) for q in set(_qubits).difference(circuit.all_qubits())) # Get qubits map qubits_map = kwargs['qubits_map'] if 'qubits_map' in kwargs else { q: x for x, q in enumerate(circuit.all_qubits()) } # Pre-process circuit _LS_cache = {} _P_cache = {} circuit = [ g for gate in tqdm(reversed(circuit), total=len(circuit), disable=not _verbose, desc='Pre-processing') for g in _process_gate(gate, LS_cache=_LS_cache, P_cache=_P_cache) ] _LS_cache.clear() _P_cache.clear() del (_LS_cache) del (_P_cache) # Get maximum number of qubits and parameters _max_n_qubits = max(max(len(gate[1]) for gate in circuit), 2) _max_n_params = max(len(gate[2]) for gate in circuit) # Get qubits qubits = np.array([ np.pad([qubits_map[q] for q in gate[1]], (0, _max_n_qubits - len(gate[1]))) for gate in circuit ], dtype='int32') # Get parameters params = np.round( np.array([ np.pad(gate[2], (0, _max_n_params - len(gate[2]))) for gate in circuit ], dtype=float_type), -int(np.floor(np.log10(atol))) if atol < 1 else 0) # Remove -0 params[np.abs(params) == 0] = 0 # Quick check assert (all('_' + gate[0] in globals() for gate in circuit)) # Get gates gates = np.array([globals()['_' + gate[0]] for gate in circuit], dtype='int') # Compute expected number of paths _log2_n_expected_branches = 0 for _idx in np.where(np.isin(gates, _MATRIX_SET))[0]: _nq = (gates[_idx] // _gate_mul) + 1 _p = params[_idx][:4**(2 * _nq)] _log2_n_expected_branches += np.sum( np.log2( np.sum(np.abs(np.reshape(_p, (4**_nq, 4**_nq))) > eps, axis=1))) / 4**_nq # Check assert (len(gates) == len(qubits) and len(gates) == len(params)) # Initialize branches if type(pauli_string) == Circuit: # Convert Pauli string _pauli_string = np.array([_I] * len(qubits_map), dtype='int') for gate in pauli_string: if gate.name != 'I': _pauli_string[qubits_map[gate.qubits[0]]] = { 'X': _X, 'Y': _Y, 'Z': _Z }[gate.name] # Initialize branches branches = [(_pauli_string, phase, 0)] else: # Initialize branches branches = [(np.array([{ 'I': _I, 'X': _X, 'Y': _Y, 'Z': _Z }[g] for g in p], dtype='int'), phase, 0) for p, phase in pauli_string.items() if abs(phase) > atol] # Initialize final Pauli strings db = kwargs['db_init']() # Define update function _update = partial_func(_update_pauli_string, gates, qubits, params, eps=eps, atol=branch_atol) # End pre-processing time _prep_time = time() - _prep_time # Initialize infos _info_init = lambda: { 'n_explored_branches': 0, 'largest_n_branches_in_memory': 0, 'peak_virtual_memory (GB)': virtual_memory().used / 2**30, 'average_virtual_memory (GB)': (virtual_memory().used / 2**30, 1), 'n_threads': parallel, 'n_cpus': cpu_count(), 'eps': eps, 'atol': atol, 'branch_atol': branch_atol, 'float_type': str(float_type), 'log2_n_expected_branches': _log2_n_expected_branches } infos = _info_init() infos['memory_baseline (GB)'] = virtual_memory().used / 2**30 if not use_mpi or _mpi_rank == 0: infos['n_explored_branches'] = 1 infos['largest_n_branches_in_memory'] = 1 # Start clock _init_time = time() # Scatter first batch of branches to different MPI nodes if use_mpi and _mpi_size > 1: if _mpi_rank == 0: # Explore branches (breadth-first search) branches = _breadth_first_search( _update, db, branches, max_n_branches=kwargs['max_breadth_first_branches_mpi'], infos=infos, verbose=verbose, mpi_rank=_mpi_rank, **kwargs) # Distribute branches branches = _mpi_comm.scatter( [list(x) for x in distribute(_mpi_size, branches)], root=0) # Explore branches (breadth-first search) branches = _breadth_first_search( _update, db, branches, max_n_branches=kwargs['max_breadth_first_branches'], infos=infos, verbose=verbose if not use_mpi or _mpi_rank == 0 else False, **kwargs) # If there are remaining branches, use depth-first search if branches: _depth_first_search(_update, db, branches, parallel=parallel, infos=infos, info_init=_info_init, verbose=verbose, mpi_rank=_mpi_rank if use_mpi else 0, mpi_size=_mpi_size if use_mpi else 1, **kwargs) # Update infos infos['average_virtual_memory (GB)'] = infos['average_virtual_memory (GB)'][ 0] / infos['average_virtual_memory (GB)'][1] - infos[ 'memory_baseline (GB)'] infos['peak_virtual_memory (GB)'] -= infos['memory_baseline (GB)'] # Update branching time infos['branching_time (s)'] = time() - _init_time # Collect results if use_mpi and _mpi_size > 1 and kwargs['mpi_merge']: for _k in infos: infos[_k] = [infos[_k]] # Initialize pbar if _mpi_rank == 0: pbar = tqdm(total=int(np.ceil(np.log2(_mpi_size))), disable=not verbose, desc='Collect results') # Initialize tag and size _tag = 0 _size = _mpi_size while _size > 1: # Update progressbar if _mpi_rank == 0: pbar.set_description( f'Collect results (Mem={virtual_memory().percent}%)') # Get shift _shift = (_size // 2) + (_size % 2) if _mpi_rank < (_size // 2): # Get infos _infos = _mpi_comm.recv(source=_mpi_rank + _shift, tag=_tag) # Update infos for _k in infos: infos[_k].extend(_infos[_k]) # Get number of chunks _n_chunks = _mpi_comm.recv(source=_mpi_rank + _shift, tag=_tag + 1) if _n_chunks > 1: # Initialize _process with tqdm(range(_n_chunks), desc='Get db', leave=False, disable=_mpi_rank != 0) as pbar: for _ in pbar: # Receive db _db = _mpi_comm.recv(source=_mpi_rank + _shift, tag=_tag + 2) # Merge datasets kwargs['merge'](db, _db, use_tuple=True) # Update description pbar.set_description( f'Get db (Mem={virtual_memory().percent}%)') # Clear dataset _db.clear() else: # Receive db _db = _mpi_comm.recv(source=_mpi_rank + _shift, tag=_tag + 2) # Merge datasets kwargs['merge'](db, _db) # Clear dataset _db.clear() elif _shift <= _mpi_rank < _size: # Remove default_factory because pickle is picky regarding local objects db.default_factory = None # Send infos _mpi_comm.send(infos, dest=_mpi_rank - _shift, tag=_tag) # Compute chunks _n_chunks = kwargs['mpi_chunk_max_size'] _n_chunks = (len(db) // _n_chunks) + ( (len(db) % _n_chunks) != 0) # Send number of chunks _mpi_comm.send(_n_chunks, dest=_mpi_rank - _shift, tag=_tag + 1) if _n_chunks > 1: # Split db in chunks for _db in chunked(db.items(), kwargs['mpi_chunk_max_size']): _mpi_comm.send(_db, dest=_mpi_rank - _shift, tag=_tag + 2) else: # Send db _mpi_comm.send(db, dest=_mpi_rank - _shift, tag=_tag + 2) # Reset db and infos db.clear() infos.clear() # update size _tag += 3 _size = _shift _mpi_comm.barrier() # Update progressbar if _mpi_rank == 0: pbar.set_description( f'Collect results (Mem={virtual_memory().percent}%)') pbar.update() # Update runtime if not use_mpi or _mpi_rank == 0 or not kwargs['mpi_merge']: infos['runtime (s)'] = time() - _init_time infos['pre-processing (s)'] = _prep_time # Check that all the others dbs/infos (excluding rank==0) has been cleared up if use_mpi and _mpi_rank > 0 and kwargs['mpi_merge']: assert (not len(db) and not len(infos)) if return_info: return db, infos else: return db
def to_qasm(circuit: Circuit, qubits_map: dict[any, int] = None) -> str: """ Convert a `Circuit` to QASM language. Parameters ---------- circuit: Circuit `Circuit` to convert to QASM language. qubits_map: dict[any, int], optional If provided, `qubits_map` map qubit indexes in `Circuit` to qubit indexes in QASM. Otherwise, indexes are assigned to QASM qubits by using `Circuit.all_qubits()` order. Returns ------- str String representing the QAMS circuit. Notes ----- The QASM language used in HybridQ is compatible with the standard QASM. However, HybridQ introduces few extensions, which are recognized by the parser using ``#@`` at the beginning of the line (``#`` at the beginning of the line represent a general comment in QASM). At the moment, the following QAMS extensions are supported: * **qubits**, used to store `qubits_map`, * **power**, used to store the power of the gate, * **tags**, used to store the tags associated to the gate, * **U**, used to store the matrix reprentation of the gate if gate name is `MATRIX` If `Gate.qubits` are not specified, a single `.` is used to represent the missing qubits. If `Gate.params` are missing, parameters are just omitted. Example ------- >>> from hybridq.extras.qasm import to_qasm >>> print(to_qasm(Circuit(Gate('H', [q]) for q in range(10)))) 10 #@ qubits = #@ { #@ "0": "0", #@ "1": "1", #@ "2": "2", #@ "3": "3", #@ "4": "4", #@ "5": "5", #@ "6": "6", #@ "7": "7", #@ "8": "8", #@ "9": "9" #@ } h 0 h 1 h 2 h 3 h 4 h 5 h 6 h 7 h 8 h 9 >>> c = Circuit() >>> c.append(Gate('RX', tags={'params': False, 'qubits': False})) >>> c.append(Gate('RY', params=[1.23], tags={'params': True, 'qubits': False})) >>> c.append(Gate('RZ', qubits=[42], tags={'params': False, 'qubits': True})**1.23) >>> c.append(Gate('MATRIX', U=Gate('H').matrix())) >>> print(to_qasm(c)) 1 #@ qubits = #@ { #@ "0": "42" #@ } #@ tags = #@ { #@ "params": false, #@ "qubits": false #@ } rx . #@ tags = #@ { #@ "params": true, #@ "qubits": false #@ } ry . 1.23 #@ tags = #@ { #@ "params": false, #@ "qubits": true #@ } #@ power = 1.23 rz 0 #@ U = #@ [ #@ [ #@ "0.7071067811865475", #@ "0.7071067811865475" #@ ], #@ [ #@ "0.7071067811865475", #@ "-0.7071067811865475" #@ ] #@ ] matrix . """ # Initialize output _out = '' # Get qubits map if qubits_map is None: qubits_map = { q: x for x, q in enumerate( circuit.all_qubits(ignore_missing_qubits=True)) } # Get inverse map inv_qubits_map = {x: str(q) for q, x in qubits_map.items()} # Dump number of qubits _out += f'{len(qubits_map)}\n' # Dump map _out += '#@ qubits = \n' _out += '\n'.join( ['#@ ' + x for x in json.dumps(inv_qubits_map, indent=2).split('\n')]) _out += '\n' for gate in circuit: # Store matrix if gate.name == 'MATRIX': _out += '#@ U = \n' _out += '\n'.join('#@ ' + x for x in json.dumps([[str(y) for y in x] for x in gate.Matrix], indent=2).split('\n')) _out += '\n' # Dump tags if gate.provides('tags') and gate.tags is not None: _out += '#@ tags = \n' _out += '\n'.join([ '#@ ' + x for x in json.dumps(gate.tags, indent=2).split('\n') ]) _out += '\n' # Dump power if gate.provides('power') and gate.power != 1: _out += f'#@ power = {gate.power}\n' # Dump conjugation if gate.provides('is_conjugated') and gate.is_conjugated(): _out += f'#@ conj\n' # Dump transposition if gate.provides('is_transposed') and gate.is_transposed(): _out += f'#@ T\n' # Dump name _out += gate.name.lower() # Dump gates if gate.provides('qubits') and gate.qubits is not None: _out += ' ' + ' '.join((str(qubits_map[x]) for x in gate.qubits)) else: _out += ' .' # Dump params if gate.provides('params'): if gate.params is not None: _out += ' ' + ' '.join((str(x) for x in gate.params)) else: raise ValueError('Not yet implemented.') # Add newline _out += '\n' return _out
def expectation_value(state: Array, op: Circuit, qubits_order: iter[any], complex_type: any = 'complex64', backend: any = 'numpy', verbose: bool = False, **kwargs) -> complex: """ Compute expectation value of an operator given a quantum state. Parameters ---------- state: Array Quantum state to use to compute the expectation value of the operator `op`. op: Circuit Quantum operator to use to compute the expectation value. qubits_order: iter[any] Order of qubits used to map `Circuit.qubits` to `state`. complex_type: any, optional Complex type to use to compute the expectation value. backend: any, optional Backend used to compute the quantum state. Backend must have `tensordot`, `transpose` and `einsum` methods. verbose: bool, optional Verbose output. Returns ------- complex The expectation value of the operator `op` given `state`. Other Parameters ---------------- `expectation_value` accepts all valid parameters for `simulate`. See Also -------- `simulate` Example ------- >>> op = Circuit([ >>> Gate('H', qubits=[32]), >>> Gate('CX', qubits=[32, 42]), >>> Gate('RX', qubits=[12], params=[1.32]) >>> ]) >>> expectation_value( >>> state=prepare_state('+0-'), >>> op=op, >>> qubits_order=[12, 42, 32], >>> ) array(0.55860883-0.43353909j) """ # Fix remove_id_gates kwargs['remove_id_gates'] = False # Get number of qubits state = np.asarray(state) # Get number of qubits n_qubits = state.ndim # Convert qubits_order to a list qubits_order = list(qubits_order) # Check lenght of qubits_order if len(qubits_order) != n_qubits: raise ValueError( "'qubits_order' must have the same number of qubits of 'state'.") # Check that qubits in op are a subset of qubits_order if set(op.all_qubits()).difference(qubits_order): raise ValueError("'op' has qubits not included in 'qubits_order'.") # Add Gate('I') to op for not used qubits op = op + [ Gate('I', qubits=[q]) for q in set(qubits_order).difference(op.all_qubits()) ] # Simulate op with given state _state = simulate(op, initial_state=state, optimize='evolution', complex_type=complex_type, backend=backend, verbose=verbose, **kwargs) # Return expectation value return np.real_if_close(np.sum(_state * state.conj()))
def simulate(circuit: SuperCircuit, initial_state: any, final_state: any = None, optimize: any = 'evolution', parallel: {bool, int} = False, verbose: bool = False, **kwargs): """ Frontend to simulate `rho` using different optimization models and backends. Parameters ---------- circuit: Circuit Circuit to simulate. initial_state: {str, Circuit, array_like} Initial density matrix to evolve. final_state: {str, Circuit, array_like} Final density matrix to project to. optimize: any Method to use to perform the simulation. The available methods are: - `evolution`: Evolve the density matrix using state vector evolution - `tn`: Evolve the density matrix using tensor contraction - `clifford`: Evolve the density matrix using Clifford expansion See Also -------- `hybridq.circuit.simulation` and `hybridq.circuit.simulation.clifford` """ from hybridq.circuit import Circuit # Convert circuit to list circuit = list(circuit) if optimize == 'clifford': from hybridq.circuit.simulation.clifford import update_pauli_string from hybridq.gate import BaseGate # Check that all gates are BaseGate if any(not isinstance(gate, BaseGate) for gate in circuit): raise NotImplementedError( "'optimize=clifford' only supports 'BaseGate's") # Final state cannot be provided if optimize='clifford' if final_state is not None: raise ValueError( "'final_state' cannot be provided if optimize='clifford'.") # Get circuit circuit = Circuit(circuit) # Try to convert to circuit try: initial_state = Circuit(initial_state) except: pass # Return Clifford expandion return update_pauli_string(circuit=circuit, pauli_string=initial_state, parallel=parallel, verbose=verbose, **kwargs) else: from hybridq.circuit.simulation import simulate from hybridq.utils import sort # Convert to SuperCircuit circuit = SuperCircuit(circuit) # Get left/right qubits l_qubits, r_qubits = circuit.all_qubits() # Convert SuperCircuit to Circuit circuit = __convert(circuit, parallel=parallel, verbose=verbose) # Get number of qubits nl = len(l_qubits) nr = len(r_qubits) # Check states def _get_state(state, name): # If None, return None if state is None: return None # Check if string elif isinstance(state, str): # If single char, extend to full size state = state * (nl + nr) if len(state) == 1 else state # Check that state has the right number of chars if not (len(state) == (nl + nr) or (l_qubits == r_qubits and len(state) == nl)): raise ValueError( f"'{name}' has the wrong number of qubits.") # Extend if needed state = state + state if len(state) == nl else state # Return return state # Check if Circuit elif isinstance(state, Circuit): from hybridq.circuit.utils import matrix # Check that qubits are consistent if l_qubits != r_qubits or sort(l_qubits) != sort( state.all_qubits()): raise ValueError( f"Qubits in '{name}' are not consistent with 'circuit'." ) # Get matrix U = matrix(state, order=l_qubits) # Swap input/output return np.transpose(np.reshape(U, (2, ) * 2 * nl), list(range(nl, 2 * nl)) + list(range(nl))) else: # Try to convert to numpy array state = np.asarray(state) # At the moment, only 2-dimensional qubits are allowed if set(state.shape) != {2}: raise NotImplementedError( "Only 2-dimensional qubits are allowed.") # Check if the number of dimensions matches if not (state.ndim == (nl + nr) or (l_qubits == r_qubits and state.ndim == nl)): raise ValueError( f"'{name}' has the wrong number of qubits.") # Extend if needed if state.ndim == nl: state = np.reshape(np.kron(state.ravel(), state.ravel()), (2, ) * 2 * nl) # Return state return state # Get states initial_state = _get_state(initial_state, 'initial_state') final_state = _get_state(final_state, 'final_state') # Return results return simulate(circuit=circuit, initial_state=initial_state, final_state=final_state, optimize=optimize, parallel=parallel, verbose=verbose, **kwargs)
def simulate(circuit: {Circuit, TensorNetwork}, initial_state: any = None, final_state: any = None, optimize: any = 'evolution', backend: any = 'numpy', complex_type: any = 'complex64', tensor_only: bool = False, simplify: {bool, dict} = True, remove_id_gates: bool = True, use_mpi: bool = None, atol: float = 1e-8, verbose: bool = False, **kwargs) -> any: """ Frontend to simulate `Circuit` using different optimization models and backends. Parameters ---------- circuit: {Circuit, TensorNetwork} Circuit to simulate. initial_state: any, optional Initial state to use. final_state: any, optional Final state to use (only valid for `optimize='tn'`). optimize: any, optional Optimization to use. At the moment, HybridQ supports two optimizations: `optimize='evolution'` (equivalent to `optimize='evolution-hybridq'`) and `optimize='tn'` (equivalent to `optimize='cotengra'`). `optimize='evolution'` takes an `initial_state` (it can either be a string, which is processed using ```prepare_state``` or an `Array`) and evolve the quantum state accordingly to `Circuit`. Alternatives are: - `optimize='evolution-hybridq'`: use internal `C++` implementation for quantum state evolution that uses vectorization instructions (such as AVX instructions for Intel processors). This optimization method is best suitable for `CPU`s. - `optimize='evolution-einsum'`: use `einsum` to perform the evolution of the quantum state (via `opt_einsum`). It is possible to futher specify optimization for `opt_einsum` by using `optimize='evolution-einsum-opt'` where `opt` is one of the available optimization in `opt_einsum.contract` (default: `auto`). This optimization is best suitable for `GPU`s and `TPU`s (using `backend='jax'`). `optimize='tn'` (or, equivalently, `optimize='cotengra'`) performs the tensor contraction of `Circuit` given an `initial_state` and a `final_state` (both must be a `str`). Valid tokens for both `initial_state` and `final_state` are: - `0`: qubit is set to `0` in the computational basis, - `1`: qubit is set to `1` in the computational basis, - `+`: qubit is set to `+` state in the computational basis, - `-`: qubit is set to `-` state in the computational basis, - `.`: qubit is left uncontracted. Before the actual contraction, `cotengra` is called to identify an optimal contraction. Such contraction is then used to perform the tensor contraction. If `Circuit` is a `TensorNetwork`, `optimize` must be a valid contraction (see `tensor_only` parameter). backend: any, optional Backend used to perform the simulation. Backend must have `tensordot`, `transpose` and `einsum` methods. complex_type: any, optional Complex type to use for the simulation. tensor_only: bool, optional If `True` and `optimize=None`, `simulate` will return a `TensorNetwork` representing `Circuit`. Otherwise, if `optimize='cotengra'`, `simulate` will return the `tuple` ```(TensorNetwork```, ```ContractionInfo)```. ```TensorNetwork``` and and ```ContractionInfo``` can be respectively used as values for `circuit` and `optimize` to perform the actual contraction. simplify: {bool, dict}, optional Circuit is simplified before the simulation using `circuit.utils.simplify`. If non-empty `dict` is provided, `simplify` is passed as arguments for `circuit.utils.simplity`. remove_id_gates: bool, optional Identity gates are removed before to perform the simulation. If `False`, identity gates are kept during the simulation. use_mpi: bool, optional Use `MPI` if available. Unless `use_mpi=False`, `MPI` will be used if detected (for instance, if `mpiexec` is used to called HybridQ). If `use_mpi=True`, force the use of `MPI` (in case `MPI` is not automatically detected). atol: float, optional Use `atol` as absolute tollerance. verbose: bool, optional Verbose output. Returns ------- Output of `simulate` depends on the chosen parameters. Other Parameters ---------------- parallel: int (default: False) Parallelize simulation (where possible). If `True`, the number of available cpus is used. Otherwise, a `parallel` number of threads is used. compress: {int, dict} (default: auto) Select level of compression for ```circuit.utils.compress```, which is run on `Circuit` prior to perform the simulation. If non-empty `dict` is provided, `compress` is passed as arguments for `circuit.utils.compress`. If `optimize=evolution`, `compress` is set to `4` by default. Otherwise, if `optimize=tn`, `compress` is set to `2` by default. allow_sampling: bool (default: False) If `True`, `Gate`s that provide the method `sample` will not be sampled. sampling_seed: int (default: None) If provided, `numpy.random` state will be saved before sampling and `sampling_seed` will be used to sample `Gate`s. `numpy.random` state will be restored after sampling. block_until_ready: bool (default: True) When `backend='jax'`, wait till the results are ready before returning. return_numpy_array: bool (default: True) When `optimize='hybridq'` and `return_numpy_array` is `False, a `tuple` of two `np.ndarray` is returned, corresponding to the real and imaginary part of the quantu state. If `True`, the real and imaginary part are copied to a single `np.ndarray` of complex numbers. return_info: bool (default: False) Return extra information collected during the simulation. simplify_tn: str (default: 'RC') Simplification to apply to `TensorNetwork`. Available simplifications as specified in `quimb.tensor.TensorNetwork.full_simplify`. max_largest_intermediate: int (default: 2**26) Largest intermediate which is allowed during simulation. If `optimize='evolution'`, `simulate` will raise an error if the largest intermediate is larger than `max_largest_intermediate`. If `optimize='tn'`, slicing will be applied to fit the contraction in memory. target_largest_intermediate: int (default: 0) Stop `cotengra` if a contraction having the largest intermediate smaller than `target_largest_intermediate` is found. max_iterations: int (default: 1) Number of `cotengra` iterations to find optimal contration. max_time: int (default: 120) Maximum number of seconds allowed to `cotengra` to find optimal contraction for each iteration. max_repeats: int (default: 16) Number of `cotengra` steps to find optimal contraction for each iteration. temperatures: list[float] (default: [1.0, 0.1, 0.01]) Temperatures used by `cotengra` to find optimal slicing of the tensor network. max_n_slices: int (default: None) If specified, `simulate` will raise an error if the number of slices to fit the tensor contraction in memory is larger than `max_n_slices`. minimize: str (default: 'combo') Cost function to minimize while looking for the best contraction (see `cotengra` for more information). methods: list[str] (default: ['kahypar', 'greedy']) Heuristics used by `cotengra` to find optimal contraction. cotengra: dict[any, any] (default: {}) Extra parameters to pass to `cotengra`. """ # Set defaults kwargs.setdefault('allow_sampling', False) kwargs.setdefault('sampling_seed', None) # Convert simplify simplify = simplify if isinstance(simplify, bool) else dict(simplify) # Checks if tensor_only and type(optimize) == str and 'evolution' in optimize: raise ValueError( f"'tensor_only' is not support for optimize={optimize}") # Try to convert to circuit try: circuit = Circuit(circuit) except: pass # Simplify circuit if isinstance(circuit, Circuit): # Flatten circuit circuit = utils.flatten(circuit) # If 'sampling_seed' is provided, use it if kwargs['sampling_seed'] is not None: # Store numpy.random state __state = np.random.get_state() # Set seed np.random.seed(int(kwargs['sampling_seed'])) # If stochastic gates are present, randomly sample from them circuit = Circuit(g.sample() if isinstance(g, pr.StochasticGate) and kwargs['allow_sampling'] else g for g in circuit) # Restore numpy.random state if kwargs['sampling_seed'] is not None: np.random.set_state(__state) # Get qubits qubits = circuit.all_qubits() n_qubits = len(qubits) # Prepare state def _prepare_state(state): if isinstance(state, str): if len(state) == 1: state *= n_qubits if len(state) != n_qubits: raise ValueError( "Wrong number of qubits for initial/final state.") return state else: # Convert to np.ndarray state = np.asarray(state) # For now, it only supports "qubits" ... if any(x != 2 for x in state.shape): raise ValueError( "Only qubits of dimension 2 are supported.") # Check number of qubits is consistent if state.ndim != n_qubits: raise ValueError( "Wrong number of qubits for initial/final state.") return state # Prepare initial/final state initial_state = None if initial_state is None else _prepare_state( initial_state) final_state = None if final_state is None else _prepare_state( final_state) # Strip Gate('I') if remove_id_gates: circuit = Circuit(gate for gate in circuit if gate.name != 'I') # Simplify circuit if simplify: circuit = utils.simplify( circuit, remove_id_gates=remove_id_gates, atol=atol, verbose=verbose, **(simplify if isinstance(simplify, dict) else {})) # Stop if qubits have changed if circuit.all_qubits() != qubits: raise ValueError( "Active qubits have changed after simplification. Forcing stop." ) # Simulate if type(optimize) == str and 'evolution' in optimize: # Set default parameters optimize = '-'.join(optimize.split('-')[1:]) if not optimize: optimize = 'hybridq' kwargs.setdefault('compress', 4) kwargs.setdefault('max_largest_intermediate', 2**26) kwargs.setdefault('return_info', False) kwargs.setdefault('block_until_ready', True) kwargs.setdefault('return_numpy_array', True) return _simulate_evolution(circuit, initial_state, final_state, optimize, backend, complex_type, verbose, **kwargs) else: # Set default parameters kwargs.setdefault('compress', 2) kwargs.setdefault('simplify_tn', 'RC') kwargs.setdefault('max_iterations', 1) try: import kahypar as __kahypar__ kwargs.setdefault('methods', ['kahypar', 'greedy']) except ModuleNotFoundError: warn("Cannot find module kahypar. Remove it from defaults.") kwargs.setdefault('methods', ['greedy']) except ImportError: warn("Cannot import module kahypar. Remove it from defaults.") kwargs.setdefault('methods', ['greedy']) kwargs.setdefault('max_time', 120) kwargs.setdefault('max_repeats', 16) kwargs.setdefault('minimize', 'combo') kwargs.setdefault('target_largest_intermediate', 0) kwargs.setdefault('max_largest_intermediate', 2**26) kwargs.setdefault('temperatures', [1.0, 0.1, 0.01]) kwargs.setdefault('parallel', None) kwargs.setdefault('cotengra', {}) kwargs.setdefault('max_n_slices', None) kwargs.setdefault('return_info', False) # If use_mpi==False, force the non-use of MPI if not (use_mpi == False) and (use_mpi or _detect_mpi): # Warn that MPI is used because detected if not use_mpi: warn("MPI has been detected. Using MPI.") from hybridq.circuit.simulation.simulation_mpi import _simulate_tn_mpi return _simulate_tn_mpi(circuit, initial_state, final_state, optimize, backend, complex_type, tensor_only, verbose, **kwargs) else: if _detect_mpi and use_mpi == False: warn( "MPI has been detected but use_mpi==False. Not using MPI as requested." ) return _simulate_tn(circuit, initial_state, final_state, optimize, backend, complex_type, tensor_only, verbose, **kwargs)
def compress(circuit: iter[BaseGate], max_n_qubits: int = 2, *, exclude_qubits: iter[any] = None, use_matrix_commutation: bool = True, max_n_qubits_matrix: int = 10, skip_compression: iter[{type, str}] = None, skip_commutation: iter[{type, str}] = None, atol: float = 1e-8, verbose: bool = False) -> list[Circuit]: """ Compress gates together up to the specified number of qubits. `compress` is deterministic, so it can be reused elsewhere. Parameters ---------- circuit: iter[BaseGate] Circuit to compress. max_n_qubits: int, optional Maximum number of qubits that a compressed gate may have. exclude_qubits: list[any], optional Exclude gates which act on `exclude_qubits` to be compressed. use_matrix_commutation: bool, optional If `True`, use commutation to maximize compression. max_n_qubits_matrix: int, optional Limit the size of matrices when checking for commutation. skip_compression: iter[{type, str}], optional If `BaseGate` is either an instance of any types in `skip_compression`, it provides any methods in `skip_compression`, or `BaseGate` name will match any names in `skip_compression`, `BaseGate` will not be compressed. However, if `use_matrix_commutation` is `True`, commutation will be checked against `BaseGate`. skip_commutation: iter[{type, str}], optional If `BaseGate` is either an instance of any types in `skip_commutation`, it provides any methods in `skip_commutation`, or `BaseGate` name will match any names in `skip_commutation`, `BaseGate` will not be checked against commutation. atol: float Absolute tollerance for commutation. verbose: bool, optional Verbose output. Returns ------- list[Circuit] A list of `Circuit`s, with each `Circuit` representing a compressed `BaseGate`. See Also -------- hybridq.gate.commutes_with Example ------- >>> # Define circuit >>> circuit = Circuit( >>> [Gate('X', qubits=[0])**1.2, >>> Gate('ISWAP', qubits=[0, 1])**2.3, >>> Gate('ISWAP', qubits=[0, 2])**2.3]) >>> >>> # Compress circuit up to 1-qubit gates >>> utils.compress(circuit, 1) [Circuit([ Gate(name=X, qubits=[0])**1.2 ]), Circuit([ Gate(name=ISWAP, qubits=[0, 1])**2.3 ]), Circuit([ Gate(name=ISWAP, qubits=[0, 2])**2.3 ])] >>> # Compress circuit up to 2-qubit gates >>> utils.compress(circuit, 2) [Circuit([ Gate(name=X, qubits=[0])**1.2 Gate(name=ISWAP, qubits=[0, 1])**2.3 ]), Circuit([ Gate(name=ISWAP, qubits=[0, 2])**2.3 ])] >>> # Compress circuit up to 3-qubit gates >>> utils.compress(circuit, 3) [Circuit([ Gate(name=X, qubits=[0])**1.2 Gate(name=ISWAP, qubits=[0, 1])**2.3 Gate(name=ISWAP, qubits=[0, 2])**2.3 ])] """ # If max_n_qubits <= 0, split every gate if max_n_qubits <= 0: return [Circuit([g]) for g in circuit] # Initialize skip_compression and skip_commutation skip_compression = tuple() if skip_compression is None else tuple( skip_compression) skip_commutation = tuple() if skip_commutation is None else tuple( skip_commutation) def _check_skip(gate, x): if isinstance(x, type): return isinstance(gate, x) elif isinstance(x, str): return gate.name == x.upper() or gate.provides(x) else: raise ValueError(f"'{x}' not supported.") # Initialize exclude_qubits exclude_qubits = set([] if exclude_qubits is None else exclude_qubits) # Convert to Circuit circuit = Circuit(circuit) # Initialize compressed circuit new_circuit = [] # For every gate in circuit .. for gate in tqdm(circuit, disable=not verbose, desc=f'Compress ({max_n_qubits})'): # Initialize matrix gate _gate = None # Initialize _compress gate_properties = dict(compress=True, commute=True) # Initialize index _merge_to = len(new_circuit) # If gate does not provide qubits or qubits is None, # then gate is not compressible if not gate.provides('qubits') or gate.qubits is None: gate_properties['compress'] = False gate_properties['commute'] = False # Otherwise ... else: # Get qubits _q = set(gate.qubits) # Get Matrix gate if possible try: _gate = to_matrix_gate( [gate], max_compress=0) if use_matrix_commutation and len( _q) <= max_n_qubits_matrix else None except: _gate = None # Check if gate must skip compression if any(_check_skip(gate, t) for t in skip_compression ) or _q.intersection(exclude_qubits): gate_properties['compress'] = False # Check if gate must skip commutation if any(_check_skip(gate, t) for t in skip_commutation): gate_properties['commute'] = False # Check for each existing layer for i, (_circ, _circ_gate, _circ_properties) in reversed(list(enumerate(new_circuit))): # If _circ does not provide any qubits, just break try: # Get circuit qubits _cq = set(_circ.all_qubits()) except: break # Check if both gate and _circ can be compressed if gate_properties['compress'] and _circ_properties['compress']: # Check if it can be merged if len(_q.union(_cq)) <= max(max_n_qubits, len(_cq), len(_q)): _merge_to = i # Check commutation if use_matrix_commutation and gate_properties[ 'commute'] and _circ_properties['commute']: # Check if gate and _circ share any qubit if not _q.intersection(_cq): continue # Check if gate and _circ commute using matrix try: if _gate.commutes_with(_circ_gate): continue except: pass # Otherwise, just break break # If it possible to merge the gate to an existing layer if _merge_to < len(new_circuit): # Get layer _nc = new_circuit[_merge_to] # Update circuit _nc[0].append(gate) # Update matrix try: _nc[1] = to_matrix_gate( [_nc[1], _gate], max_compress=0) if use_matrix_commutation and len( set(_gate.qubits).union( _nc[1].qubits)) <= max_n_qubits_matrix else None except: _nc[1] = None # Update properties for k in ['compress', 'commute']: _nc[2][k] &= gate_properties[k] # Otherwise, create a new layer else: new_circuit.append([Circuit([gate]), _gate, gate_properties]) # Return only circuits return [c for c, _, _ in new_circuit]
def to_tn(circuit: iter[BaseGate], complex_type: any = 'complex64', return_qubits_map: bool = False, leaves_prefix: str = 'q_') -> quimb.tensor.TensorNetwork: """ Return `quimb.tensor.TensorNetwork` representing `circuit`. `to_tn` is deterministic, so it can be reused elsewhere. Parameters ---------- circuit: iter[BaseGate] Circuit to get `quimb.tensor.TensorNetwork` representation from. complex_type: any, optional Complex type to use while getting the `quimb.tensor.TensorNetwork` representation. return_qubits_map: bool, optional Return map associated to the Circuit qubits. leaves_prefix: str, optional Specify prefix to use for leaves. Returns ------- quimb.tensor.TensorNetwork Tensor representing `circuit`. Example ------- >>> # Define circuit >>> circuit = Circuit( >>> [Gate('X', qubits=[0])**1.2, >>> Gate('ISWAP', qubits=[0, 1])**2.3], Gate('H', [1])) >>> >>> # Draw graph >>> utils.to_tn(circuit).graph() .. image:: ../../images/circuit_tn.png """ import quimb.tensor as tn # Convert iterable to Circuit circuit = Circuit(circuit) # Get all qubits all_qubits = circuit.all_qubits() # Get qubits map qubits_map = {q: i for i, q in enumerate(all_qubits)} # Get last_tag last_tag = {q: 'i' for q in all_qubits} # Node generator def _get_node(t, gate): # Get matrix U = np.reshape(gate.matrix().astype(complex_type), [2] * (2 * len(gate.qubits))) # Get indexes inds = [f'{leaves_prefix}_{qubits_map[q]}_{t}' for q in gate.qubits] + [ f'{leaves_prefix}_{qubits_map[q]}_{last_tag[q]}' for q in gate.qubits ] # Update last_tag for q in gate.qubits: last_tag[q] = t # Return node return tn.Tensor( U.astype(complex_type), inds=inds, tags=[f'{leaves_prefix}_{qubits_map[q]}' for q in gate.qubits] + [f'gate-idx_{t}']) # Get list of tensors tensor = [_get_node(t, gate) for t, gate in enumerate(circuit)] # Generate new output map output_map = { f'{leaves_prefix}_{qubits_map[q]}_{t}': f'{leaves_prefix}_{qubits_map[q]}_f' for q, t in last_tag.items() } # Rename output legs for node in tensor: node.reindex(output_map, inplace=True) # Return tensor network if return_qubits_map: return tn.TensorNetwork(tensor), qubits_map else: return tn.TensorNetwork(tensor)
if __name__ == '__main__': # Set number of qubits n_qubits = 23 # Set number of gates n_gates = 2000 # Generate RQC print('# Generate RQC: ', end='') circuit = get_rqc(n_qubits, n_gates, use_random_indexes=True) print('Done!') # Compress circuit print('# Compress RQC: ', end='') circuit = Circuit( utils.to_matrix_gate(c) for c in utils.compress(circuit, 2)) print('Done!') # Get final state print('# Compute quantum evolution: ', end='') psi = simulate(circuit, optimize='evolution', initial_state='0' * n_qubits, verbose=True) print('Done!') # Print final state for i in np.random.choice(2**n_qubits, size=20, replace=False): x = bin(i)[2:].zfill(n_qubits) print(f'|{x}> = {psi[tuple(map(int, x))]:+1.5f}') print('...')
def _simulate_tn_mpi(circuit: Circuit, initial_state: any, final_state: any, optimize: any, backend: any, complex_type: any, tensor_only: bool, verbose: bool, **kwargs): import quimb.tensor as tn import cotengra as ctg # Get MPI _mpi_comm = MPI.COMM_WORLD _mpi_size = _mpi_comm.Get_size() _mpi_rank = _mpi_comm.Get_rank() # Set default parameters kwargs.setdefault('compress', 2) kwargs.setdefault('simplify_tn', 'RC') kwargs.setdefault('max_iterations', 1) kwargs.setdefault('methods', ['kahypar', 'greedy']) kwargs.setdefault('max_time', 120) kwargs.setdefault('max_repeats', 16) kwargs.setdefault('minimize', 'combo') kwargs.setdefault('target_largest_intermediate', 0) kwargs.setdefault('max_largest_intermediate', 2**26) kwargs.setdefault('temperatures', [1.0, 0.1, 0.01]) kwargs.setdefault('parallel', None) kwargs.setdefault('cotengra', {}) kwargs.setdefault('max_n_slices', None) kwargs.setdefault('return_info', False) # Get random leaves_prefix leaves_prefix = ''.join( np.random.choice(list('abcdefghijklmnopqrstuvwxyz'), size=20)) # Initialize info _sim_info = {} # Alias for tn if optimize == 'tn': optimize = 'cotengra' if isinstance(circuit, Circuit): if not kwargs['parallel']: kwargs['parallel'] = 1 else: # If number of threads not provided, just use half of the number of available cpus if isinstance(kwargs['parallel'], bool) and kwargs['parallel'] == True: kwargs['parallel'] = cpu_count() // 2 if optimize is not None and kwargs['parallel'] and kwargs[ 'max_iterations'] == 1: warn("Parallelization for MPI works for multiple iterations only. " "For a better performance, use: 'max_iterations' > 1") # Get number of qubits qubits = circuit.all_qubits() n_qubits = len(qubits) # If initial/final state is None, set to all .'s initial_state = '.' * n_qubits if initial_state is None else initial_state final_state = '.' * n_qubits if final_state is None else final_state # Initial and final states must be valid strings for state, sname in [(initial_state, 'initial_state'), (final_state, 'final_state')]: # Get alphabet from string import ascii_letters # Check if string if not isinstance(state, str): raise ValueError(f"'{sname}' must be a valid string.") # Deprecated error if any(x in 'xX' for x in state): from warnings import warn # Define new DeprecationWarning (to always print the warning # signal) class DeprecationWarning(Warning): pass # Warn the user that '.' is used to represent open qubits warn( "Since '0.6.3', letters in the alphabet are used to " "trace selected qubits (including 'x' and 'X'). " "Instead, '.' is used to represent an open qubit.", DeprecationWarning) # Check only valid symbols are present if set(state).difference('01+-.' + ascii_letters): raise ValueError(f"'{sname}' contains invalid symbols.") # Check number of qubits if len(state) != n_qubits: raise ValueError(f"'{sname}' has the wrong number of qubits " f"(expected {n_qubits}, got {len(state)})") # Check memory if 2**(initial_state.count('.') + final_state.count('.')) > kwargs['max_largest_intermediate']: raise MemoryError("Memory for the given number of open qubits " "exceeds the 'max_largest_intermediate'.") # Compress circuit if kwargs['compress']: if verbose: print( f"Compress circuit (max_n_qubits={kwargs['compress']}): ", end='', file=stderr) _time = time() circuit = utils.compress( circuit, kwargs['compress']['max_n_qubits'] if isinstance( kwargs['compress'], dict) else kwargs['compress'], verbose=verbose, **({ k: v for k, v in kwargs['compress'].items() if k != 'max_n_qubits' } if isinstance(kwargs['compress'], dict) else {})) circuit = Circuit( utils.to_matrix_gate(c, complex_type=complex_type) for c in circuit) if verbose: print(f"Done! ({time()-_time:1.2f}s)", file=stderr) # Get tensor network representation of circuit tensor, tn_qubits_map = utils.to_tn(circuit, return_qubits_map=True, leaves_prefix=leaves_prefix) # Define basic MPS _mps = { '0': np.array([1, 0]), '1': np.array([0, 1]), '+': np.array([1, 1]) / np.sqrt(2), '-': np.array([1, -1]) / np.sqrt(2) } # Attach initial/final state for state, ext in [(initial_state, 'i'), (final_state, 'f')]: for s, q in ((s, q) for s, q in zip(state, qubits) if s in _mps): inds = [f'{leaves_prefix}_{tn_qubits_map[q]}_{ext}'] tensor &= tn.Tensor(_mps[s], inds=inds, tags=inds) # For each unique letter, apply trace for x in set(initial_state + final_state).difference(''.join(_mps) + '.'): # Get indexes inds = [ f'{leaves_prefix}_{tn_qubits_map[q]}_i' for s, q in zip(initial_state, qubits) if s == x ] inds += [ f'{leaves_prefix}_{tn_qubits_map[q]}_f' for s, q in zip(final_state, qubits) if s == x ] # Apply trace tensor &= tn.Tensor(np.reshape([1] + [0] * (2**len(inds) - 2) + [1], (2, ) * len(inds)), inds=inds) # Simplify if requested if kwargs['simplify_tn']: tensor.full_simplify_(kwargs['simplify_tn']).astype_(complex_type) else: # Otherwise, just convert to the given complex_type tensor.astype_(complex_type) # Get contraction from heuristic if optimize == 'cotengra' and kwargs['max_iterations'] > 0: # Set cotengra parameters def cotengra_params(): # Get HyperOptimizer q = ctg.HyperOptimizer(methods=kwargs['methods'], max_time=kwargs['max_time'], max_repeats=kwargs['max_repeats'], minimize=kwargs['minimize'], progbar=False, parallel=False, **kwargs['cotengra']) # For some optlib, HyperOptimizer._retrieve_params is not # pickeable. Let's fix the problem by hand. q._retrieve_params = __FunctionWrap(q._retrieve_params) # Return HyperOptimizer return q # Get target size tli = kwargs['target_largest_intermediate'] with Pool(kwargs['parallel']) as pool: # Sumbit jobs _opts = [ cotengra_params() for _ in range(kwargs['max_iterations']) ] _map = [ pool.apply_async(tensor.contract, (all, ), dict(optimize=_opt, get='path-info')) for _opt in _opts ] with tqdm(total=len(_map), disable=not verbose, desc='Collecting contractions') as pbar: _old_completed = 0 while 1: # Count number of completed _completed = 0 for _w in _map: _completed += _w.ready() if _w.ready() and not _w.successful(): _w.get() # Update pbar pbar.update(_completed - _old_completed) _old_completed = _completed if _completed == len(_map): break # Wait sleep(1) # Collect results _infos = [_w.get() for _w in _map] if kwargs['minimize'] == 'size': opt, info = sort( zip(_opts, _infos), key=lambda w: (w[1].largest_intermediate, w[0].best['flops']))[0] else: opt, info = sort( zip(_opts, _infos), key=lambda w: (w[0].best['flops'], w[1].largest_intermediate))[0] if optimize == 'cotengra': # Gather best contractions _cost = _mpi_comm.gather( (info.largest_intermediate, info.opt_cost, _mpi_rank), root=0) if _mpi_rank == 0: if kwargs['minimize'] == 'size': _best_rank = sort(_cost, key=lambda x: (x[0], x[1]))[0][-1] else: _best_rank = sort(_cost, key=lambda x: (x[1], x[0]))[0][-1] else: _best_rank = None _best_rank = _mpi_comm.bcast(_best_rank, root=0) if hasattr(opt, '_pool'): del (opt._pool) # Distribute opt/info tensor, info, opt = _mpi_comm.bcast((tensor, info, opt), root=_best_rank) # Just return tensor if required if tensor_only: if optimize == 'cotengra' and kwargs['max_iterations'] > 0: return tensor, (info, opt) else: return tensor else: # Set tensor tensor = circuit if len(optimize) == 2 and isinstance( optimize[0], PathInfo) and isinstance( optimize[1], ctg.hyper.HyperOptimizer): # Get info and opt from optimize info, opt = optimize # Set optimization optimize = 'cotengra' else: # Get tensor and path tensor = circuit # Print some info if verbose and _mpi_rank == 0: print( f'Largest Intermediate: 2^{np.log2(float(info.largest_intermediate)):1.2f}', file=stderr) print( f'Max Largest Intermediate: 2^{np.log2(float(kwargs["max_largest_intermediate"])):1.2f}', file=stderr) print(f'Flops: 2^{np.log2(float(info.opt_cost)):1.2f}', file=stderr) if optimize == 'cotengra': if _mpi_rank == 0: # Get indexes _inds = tensor.outer_inds() # Get input indexes and output indexes _i_inds = sort([x for x in _inds if x[-2:] == '_i'], key=lambda x: int(x.split('_')[1])) _f_inds = sort([x for x in _inds if x[-2:] == '_f'], key=lambda x: int(x.split('_')[1])) # Get order _inds = [_inds.index(x) for x in _i_inds + _f_inds] # Get slice finder sf = ctg.SliceFinder( info, target_size=kwargs['max_largest_intermediate'], allow_outer=False) # Find slices with tqdm(kwargs['temperatures'], disable=not verbose, leave=False) as pbar: for _temp in pbar: pbar.set_description(f'Find slices (T={_temp})') ix_sl, cost_sl = sf.search(temperature=_temp) # Get slice contractor sc = sf.SlicedContractor([t.data for t in tensor]) # Make sure that no open qubits are sliced assert (not { ix: i for i, ix in enumerate(sc.output) if ix in sc.sliced }) # Print some infos if verbose: print( f'Number of slices: 2^{np.log2(float(cost_sl.nslices)):1.2f}', file=stderr) print( f'Flops+Cuts: 2^{np.log2(float(cost_sl.total_flops)):1.2f}', file=stderr) # Update infos _sim_info.update({ 'flops': info.opt_cost, 'largest_intermediate': info.largest_intermediate, 'n_slices': cost_sl.nslices, 'total_flops': cost_sl.total_flops }) # Get slices slices = list(range(cost_sl.nslices + 1)) + [None] * ( _mpi_size - cost_sl.nslices) if cost_sl.nslices < _mpi_size else [ cost_sl.nslices / _mpi_size * i for i in range(_mpi_size) ] + [cost_sl.nslices] if not np.alltrue( [int(x) == x for x in slices if x is not None]) or not np.alltrue([ slices[i] < slices[i + 1] for i in range(_mpi_size) if slices[i] is not None and slices[i + 1] is not None ]): raise RuntimeError('Something went wrong') # Convert all to integers slices = [int(x) if x is not None else None for x in slices] else: sc = slices = None # Distribute slicer and slices sc, slices = _mpi_comm.bcast((sc, slices), root=0) _n_slices = max(x for x in slices if x) if kwargs['max_n_slices'] and _n_slices > kwargs['max_n_slices']: raise RuntimeError( f'Too many slices ({_n_slices} > {kwargs["max_n_slices"]})') # Contract slices _tensor = None if slices[_mpi_rank] is not None and slices[_mpi_rank + 1] is not None: for i in tqdm(range(slices[_mpi_rank], slices[_mpi_rank + 1]), desc='Contracting slices', disable=not verbose, leave=False): if _tensor is None: _tensor = np.copy(sc.contract_slice(i, backend=backend)) else: _tensor += sc.contract_slice(i, backend=backend) # Gather tensors if _mpi_rank != 0: _mpi_comm.send(_tensor, dest=0, tag=11) elif _mpi_rank == 0: for i in tqdm(range(1, _mpi_size), desc='Collecting tensors', disable=not verbose): _p_tensor = _mpi_comm.recv(source=i, tag=11) if _p_tensor is not None: _tensor += _p_tensor if _mpi_rank == 0: # Create map _map = ''.join([get_symbol(x) for x in range(len(_inds))]) _map += '->' _map += ''.join([get_symbol(x) for x in _inds]) # Reorder tensor tensor = contract(_map, _tensor) # Deprecated ## Reshape tensor #if _inds: # if _i_inds and _f_inds: # tensor = np.reshape(tensor, # (2**len(_i_inds), 2**len(_f_inds))) # else: # tensor = np.reshape(tensor, # (2**max(len(_i_inds), len(_f_inds)),)) else: tensor = None else: if _mpi_rank == 0: # Contract tensor tensor = tensor.contract(optimize=optimize, backend=backend) if hasattr(tensor, 'inds'): # Get input indexes and output indexes _i_inds = sort([x for x in tensor.inds if x[-2:] == '_i'], key=lambda x: int(x.split('_')[1])) _f_inds = sort([x for x in tensor.inds if x[-2:] == '_f'], key=lambda x: int(x.split('_')[1])) # Transpose tensor tensor.transpose(*(_i_inds + _f_inds), inplace=True) # Deprecated ## Reshape tensor #if _i_inds and _f_inds: # tensor = np.reshape(tensor, # (2**len(_i_inds), 2**len(_f_inds))) #else: # tensor = np.reshape(tensor, # (2**max(len(_i_inds), len(_f_inds)),)) else: tensor = None if kwargs['return_info']: return tensor, _sim_info else: return tensor
if __name__ == '__main__': from mpi4py import MPI _mpi_comm = MPI.COMM_WORLD _mpi_size = _mpi_comm.Get_size() _mpi_rank = _mpi_comm.Get_rank() # Generate random circuit if _mpi_rank == 0: # Get random circuit c = get_rqc(20, 40, use_random_indexes=True) # Get random observable b = Circuit( Gate(np.random.choice(list('XYZ')), [q]) for q in c.all_qubits()[:2]) else: c = b = None # Broadcast circuits c = _mpi_comm.bcast(c, root=0) b = _mpi_comm.bcast(b, root=0) # Compute expectation value with mpi res1 = clifford.expectation_value( c, b, initial_state='+' * len(c.all_qubits()), return_info=True,