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_random_gate(randomize_power: bool = True, use_clifford_only: bool = False, use_unitary_only: bool = True): """ Generate random gate. """ # Get available gates avail_gates = get_clifford_gates( ) if use_clifford_only else get_available_gates() # Add random matrices if not use_unitary_only: avail_gates = avail_gates + ('RANDOM_MATRIX', ) # Get random gate gate_name = np.random.choice(avail_gates) # Generate a random matrix if gate_name == 'RANDOM_MATRIX': # Get random number of qubits n_qubits = np.random.choice(range(1, 3)) # Get random matrix M = 2 * np.random.random( (2**n_qubits, 2**n_qubits)).astype('complex') - 1 M += 1j * (2 * np.random.random((2**n_qubits, 2**n_qubits)) - 1) M /= 2 # Normalize random matrix M /= np.sqrt(np.linalg.norm(np.linalg.eigvalsh(M.conj().T @ M))) # Get gate gate = MatrixGate(M) # Generate named gate else: gate = Gate(gate_name) # Apply random parameters if present if gate.provides('params'): gate._set_params(np.random.random(size=gate.n_params)) # Apply random power gate = gate**(2 * np.random.random() - 1 if randomize_power else 1) # Apply conjugation if supported if gate.provides('conj') and np.random.random() < 0.5: gate._conj() # Apply transposition if supported if gate.provides('T') and np.random.random() < 0.5: gate._T() # Convert to MatrixGate half of the times gate = gate if gate.name == 'MATRIX' or np.random.random( ) < 0.5 else MatrixGate(gate.matrix()) # Return gate return gate
def _GetPauliOperator(*ps): # Check if number of qubits is supported if f'_MATRIX_{len(ps)}' not in globals(): raise ValueError('Too many qubits') ps = ''.join(ps) if ps not in kwargs['P_cache']: kwargs['P_cache'][ps] = kron(*(Gate(g).matrix() for g in ps)) return kwargs['P_cache'][ps]
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 _GenerateLinearSystem(n_qubits): # Check if number of qubits is supported if f'_MATRIX_{n_qubits}' not in globals(): raise ValueError('Too many qubits') if n_qubits not in kwargs['LS_cache']: I = Gate('I').matrix().astype('complex128') X = Gate('X').matrix().astype('complex128') Y = Gate('Y').matrix().astype('complex128') Z = Gate('Z').matrix().astype('complex128') W = [I, X, Y, Z] for _ in range(n_qubits - 1): W = [kron(g1, g2) for g1 in W for g2 in [I, X, Y, Z]] W = np.linalg.inv(np.reshape(W, (2**(2 * n_qubits),) * 2).T) kwargs['LS_cache'][n_qubits] = W return kwargs['LS_cache'][n_qubits]
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)
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 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 pad(gate: Gate, qubits: iter[any], order: iter[any] = None, return_matrix_only: bool = False) -> {MatrixGate, np.ndarray}: """ Pad `gate` to act on `qubits`. More precisely, if `gate` is acting on a subset of `qubits`, extend `gate` with identities to act on all `qubits`. Parameters ---------- gate: Gate The gate to pad. qubits: iter[any] Qubits used to pad `gate`. If `gate.qubits` is not a subset of `qubits`, raise an error. order: iter[any], optional If provided, reorder qubits in the final gate accordingly to `qubits`. return_matrix_only: bool, optional If `True`, the matrix representing the state is returned instead of `MatrixGate` (default: `False`). Returns ------- MatrixGate The padded gate acting on `qubits`. """ from hybridq.gate import MatrixGate from hybridq.utils import sort # Convert qubits to tuple qubits = tuple(qubits) # Convert order to tuple if provided order = None if order is None else tuple(order) # Check that order is a permutation of qubits if order and sort(qubits) != sort(order): raise ValueError("'order' must be a permutation of 'qubits'") # 'gate' must have qubits and it must be a subset of 'qubits' if not gate.provides('qubits') or set(gate.qubits).difference(qubits): raise ValueError("'gate' must provide qubits and those " "qubits must be a subset of 'qubits'.") # Get matrix M = gate.matrix() # Pad matrix with identity if gate.n_qubits != len(qubits): M = np.kron(M, np.eye(2**(len(qubits) - gate.n_qubits))) # Get new qubits qubits = gate.qubits + tuple(set(qubits).difference(gate.qubits)) # Reorder if required if order and order != qubits: # Get new matrix M = MatrixGate(M, qubits=qubits).matrix(order=order) # Set new qubits qubits = order # Return gate return M if return_matrix_only else MatrixGate( M, qubits=qubits, tags=gate.tags if gate.provides('tags') else {})
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 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 GlobalPauliChannel(qubits: tuple[any, ...], s: {float, array, dict}, tags: dict[any, any] = None, name: str = 'GLOBAL_PAULI_CHANNEL', copy: bool = True, atol: float = 1e-8, methods: dict[any, any] = None, use_cache: bool = True) -> GlobalPauliChannel: """ Return a `GlobalPauliChannel`s acting on `qubits`. More precisely, each `LocalPauliChannel` has the form: rho -> E(rho) = \sum_{i1,i2,...}{j1,j2,...} s_{i1,i2...}{j1,j2,...} sigma_i1 sigma_i2 ... rho sigma_j1 sigma_j2 ... with `rho` being a density matrix and `sigma_i` being Pauli matrices. Parameters ---------- qubits: tuple[any, ...] Qubits the `LocalPauliChannel`s will act on. s: {float, array, dict} Weight for Pauli matrices. If `s` is a float, the diagonal of the matrix s_ij is set to `s`. Similarly, if `s` is a one dimensional array, then the diagonal of matrix s_ij is set to that array. If `s` is a `dict`, weights can be specified by using the tokens `I`, `X`, `Y` and `Z`. For instance, `dict(XYYZ=0.2)` will set the weight for `sigma_i1 == X`, `sigma_i2 == Y`, `sigma_j1 == Y` and `sigma_j2 == Z` to `0.2`. tags: dict[any, any] Tags to add to `LocalPauliChannel`s. name: str, optional Alternative name for `GlobalPauliChannel`. copy: bool, optional, If `copy == True`, then `s` is copied instead of passed by reference (default: `True`). atol: float, optional Use `atol` as absolute tollerance while checking. methods: dict[any, any] Add extra methods to the object. use_cache: bool, optional If `True`, extra memory is used to store a cached `Matrix`. """ from hybridq.utils import isintegral, kron from itertools import product from hybridq.gate import Gate # Get qubits qubits = tuple(qubits) # Define n_qubits n_qubits = len(qubits) # If 's' is a 'dict' if isinstance(s, dict): # Convert to upper s = {str(k).upper(): v for k, v in s.items()} # Check if tokens are valid if any(len(k) != 2 * n_qubits for k in s): raise ValueError("Keys in 's' must have twice a number of " "tokens which is twice the number of qubits") if any(set(k).difference('IXYZ') for k in s): raise ValueError("'s' contains non-valid tokens") # Get position def _get_position(k): return sum( (4**i * dict(I=0, X=1, Y=2, Z=3)[k]) for i, k in enumerate(k)) # Build matrix _s = np.zeros((4**n_qubits, 4**n_qubits)) for k, v in s.items(): # Get positions x, y = _get_position(k[:n_qubits]), _get_position(k[n_qubits:]) # Fill matrix _s[x, y] = v # assign s = _s # Otherwise, convert to array else: s = (np.array if copy else np.asarray)(s) # If a single float, return vector if s.ndim == 0: s = np.ones(4**n_qubits) * s # Otherwise, dimensions must be consistent elif s.ndim > 2 or set(s.shape) != {4**n_qubits}: raise ValueError("'s' must be either a vector of exactly " f"{4**n_qubits} elements, or a " f"{(4**n_qubits, 4**n_qubits)} matrix") # Get matrices Matrices = [ kron(*m) for m in product(*([[Gate(g, n_qubits=1).Matrix for g in 'IXYZ']] * n_qubits)) ] # Return gate return MatrixChannel(LMatrices=Matrices, qubits=qubits, s=s, tags=tags, name=name, copy=False, atol=atol, methods=methods, use_cache=use_cache)
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 decompose(gate: Gate, qubits: iter[any], return_matrices: bool = False, atol: float = 1e-8) -> SchmidtGate: """ Decompose `gate` using the Schmidt decomposition. Parameters ---------- gate: Gate `Gate` to decompose. qubits: iter[any] Subset of qubits used to decompose `gate`. return_matrices: bool, optional If `True`, return matrices instead of gates (default: `False`) atol: float Tollerance. Returns ------- d: tuple(list[float], tuple[Gate, ...], tuple[Gate, ...]) Decomposition of `gate`. See Also -------- `hybridq.utils.svd` """ from hybridq.gate import SchmidtGate from hybridq.utils import svd # Check qubits try: qubits = tuple(qubits) except: raise ValueError("'qubits' must be convertible to tuple.") # Get number of qubits in subset ns = len(qubits) # Get qubits not in subset alt_qubits = tuple(q for q in gate.qubits if q not in qubits) # Check is valid subset if set(qubits).difference(gate.qubits): raise ValueError("'qubits' must be a valid subset of `gate.qubits`.") # Get order axes = [gate.qubits.index(x) for x in qubits] axes += [x + gate.n_qubits for x in axes] # Get matrix and decompose it s, uh, vh = svd(np.reshape(gate.matrix(), (2,) * 2 * gate.n_qubits), axes, atol=atol) # Reshape uh = np.reshape(uh, (len(s), 2**ns, 2**ns)) vh = np.reshape(vh, (len(s), 2**(gate.n_qubits - ns), 2**(gate.n_qubits - ns))) # Return gates return (s, uh, vh) if return_matrices else SchmidtGate( gates=((Gate('MATRIX', qubits=qubits, U=x) for x in uh), (Gate('MATRIX', qubits=alt_qubits, U=x) for x in vh)), s=s)
def generate_OTOC(layout: dict[any, list[Coupling]], depth: int, sequence: list[any], one_qb_gates: iter[Gate], two_qb_gates: iter[Gate], butterfly_op: str, ancilla: Qubit, targets: list[Qubit], qubits_order: list[Qubit] = None) -> Circuit: # Get all qubits all_qubits = { q for s in sequence[:min(depth, len(sequence))] for gate in layout[s] for q in gate } # Get order of qubits qubits_order = sort(all_qubits) if qubits_order is None else qubits_order # Get list if single butterfly is provided butterfly_op = list(butterfly_op) # Check order of qubits if sort(all_qubits) != sort(qubits_order): raise ValueError( "'qubits_order' must be a valid permutation of all qubits.") # Check if butterfly op has valid strings if set(butterfly_op).difference(['I', 'X', 'Y', 'Z']): raise ValueError('Only {I, X, Y, Z} are valid butterfly operators') # Check if ancilla/targets are in layout if set(targets).union([ancilla]).difference(all_qubits): raise ValueError(f"Ancilla/Targets must be in layout.") # Check if targets are unique if len(set(targets)) != len(targets): raise ValueError('Targets must be unique.') # Check that ancilla is not in targets if ancilla in targets: raise ValueError('Ancilla must be different from targets') # Check if the number of targets corresponds to the number of butterfly ops if len(targets) != len(butterfly_op) + 1: raise ValueError( f"Number of butterfly operators does not match number " f"of targets (expected {len(targets)-1}, got {len(butterfly_op)})." ) # Check that there is a coupling between the ancilla qubit and the measurement qubit if next((False for s in sequence[:min(depth, len(sequence))] for w in layout[s] if sort(w) == sort([ancilla, targets[0]])), True): raise ValueError( f"No available two-qubit gate between ancilla {ancilla} " f"and qubit {targets[0]}.") # Initialize Circuit circ = Circuit() # Add initial layer of single qubit gates circ.extend([ Gate('SQRT_Y' if q != ancilla else 'SQRT_X', qubits=[q], tags={ 'depth': 0, 'sequence': 'initial' }) for q in sort(all_qubits) ]) # Add CZ between ancilla and first target qubit circ.append( Gate('CZ', [ancilla, targets[0]], tags={ 'depth': 0, 'sequence': 'first_control' })) # Generate U U = generate_U(layout=layout, qubits_order=qubits_order, depth=depth, sequence=sequence, one_qb_gates=one_qb_gates, two_qb_gates=two_qb_gates, exclude_qubits=[ancilla]).update_all_tags({'U': True}) # Add U to circuit circ += U # Add butterfly operator circ.extend([ Gate(_b, qubits=[_t], tags={ 'depth': depth - 1, 'sequence': 'butterfly' }) for _b, _t in zip(butterfly_op, targets[1:]) ]) # Add U* to circuit and update depth circ += Circuit( gate.update_tags({ 'depth': 2 * depth - gate.tags['depth'] - 1, 'U^-1': True }) for gate in U.inv().remove_all_tags(['U'])) # Add CZ between ancilla and first target qubit circ.append( Gate('CZ', [ancilla, targets[0]], tags={ 'depth': 2 * depth - 1, 'sequence': 'second_control' })) return circ
def merge(a: Gate, *bs) -> Gate: """ Merge two gates `a` and `b`. The merged `Gate` will be equivalent to apply ``` new_psi = bs.matrix() @ ... @ b.matrix() @ a.matrix() @ psi ``` with `psi` a quantum state. Parameters ---------- a, ...: Gate `Gate`s to merge. qubits_order: iter[any], optional If provided, qubits in new `Gate` will be sorted using `qubits_order`. Returns ------- Gate('MATRIX') The merged `Gate` """ # If no other gates are provided, return if len(bs) == 0: return a # Pop first gate b, bs = bs[0], bs[1:] # Check if any(not x.provides(['matrix', 'qubits']) or x.qubits is None for x in [a, b]): raise ValueError( "Both 'a' and 'b' must provides 'qubits' and 'matrix'.") # Get unitaries Ua, Ub = a.matrix(), b.matrix() # Get shared qubits shared_qubits = set(a.qubits).intersection(b.qubits) all_qubits = b.qubits + tuple(q for q in a.qubits if q not in b.qubits) # Get sizes n_a = len(a.qubits) n_b = len(b.qubits) n_ab = len(shared_qubits) n_c = len(all_qubits) if shared_qubits: from opt_einsum import get_symbol, contract # Build map _map_b_l = ''.join(get_symbol(x) for x in range(n_b)) _map_b_r = ''.join(get_symbol(x + n_b) for x in range(n_b)) _map_a_l = ''.join(_map_b_r[b.qubits.index(q)] if q in shared_qubits else get_symbol(x + 2 * n_b) for x, q in enumerate(a.qubits)) _map_a_r = ''.join(get_symbol(x + 2 * n_b + n_a) for x in range(n_a)) _map_c_l = ''.join(_map_b_l[b.qubits.index(q)] if q in b.qubits else _map_a_l[a.qubits.index(q)] for q in all_qubits) _map_c_r = ''.join( _map_b_r[b.qubits.index(q)] if q in b.qubits and q not in shared_qubits else _map_a_r[a.qubits.index(q)] for q in all_qubits) _map = _map_b_l + _map_b_r + ',' + _map_a_l + _map_a_r + '->' + _map_c_l + _map_c_r # Get matrix U = np.reshape( contract(_map, np.reshape(Ub, (2,) * 2 * n_b), np.reshape(Ua, (2,) * 2 * n_a)), (2**n_c, 2**n_c)) else: # Get matrix U = np.kron(Ub, Ua) # Get merged gate gate = Gate('MATRIX', qubits=all_qubits, U=U) # Iteratively call merge if len(bs) == 0: return gate else: return merge(gate, *bs)
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,