Ejemplo n.º 1
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
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
    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]
Ejemplo n.º 4
0
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
Ejemplo n.º 5
0
    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]
Ejemplo n.º 6
0
        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)
Ejemplo n.º 7
0
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))
Ejemplo n.º 8
0
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
Ejemplo n.º 9
0
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 {})
Ejemplo n.º 10
0
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()))
Ejemplo n.º 11
0
Archivo: qasm.py Proyecto: nasa/hybridq
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
Ejemplo n.º 12
0
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)
Ejemplo n.º 13
0
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
Ejemplo n.º 14
0
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)
Ejemplo n.º 15
0
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
Ejemplo n.º 16
0
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)
Ejemplo n.º 17
0
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,