Пример #1
0
def popright(circuit: list[BaseGate],
             pinned_qubits: list[any],
             atol: float = 1e-8,
             use_matrix_commutation: bool = True,
             max_n_qubits_matrix: int = 10,
             simplify: bool = True,
             verbose: bool = False) -> Circuit:
    """
  Remove gates outside the lightcone created by pinned_qubits.
  """

    # Initialize new circuit
    new_circuit = Circuit()

    # Insert gates, one by one
    for gate in tqdm(reversed(circuit),
                     disable=not verbose,
                     total=len(circuit),
                     desc='Pop'):
        insert_from_left(new_circuit,
                         gate,
                         atol=atol,
                         use_matrix_commutation=use_matrix_commutation,
                         max_n_qubits_matrix=max_n_qubits_matrix,
                         simplify=simplify,
                         pop=True,
                         pinned_qubits=pinned_qubits,
                         inplace=True)

    # Return simplified circuit
    return new_circuit
Пример #2
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
Пример #3
0
def add_depolarizing_noise(circuit: Circuit,
                           probs: {float, list[float, ...], dict[any, float]},
                           where: {'before', 'after'} = 'after',
                           verbose: bool = False):
    """
    Given a `Circuit`, add global depolarizing noise after each instance
    of a `Gate`, with the same locality as the gate.
    Note, noise will not be added after an instance of `BaseChannel`

    circuit: Circuit
        The `Circuit` which will be modified. Note, a new `Circuit` is
        returned (this is not in place).
    probs: {float, list[float, ...], dict[any, float]}
        Depolarizing probabilities for `circuit`. If `probs` is a single `float`,
        the same probability is applied to all gates regardless the number of
        qubits. If `probs` is a list, the k-th value is used as the probability
        for all the k-qubit gates. If `probs` is a `dict`, `probs[k]` will be
        used as probability for k-qubit gates. If `probs[k]` is missing, the
        probability for a k-qubit gate will fallback to `probs[any]` (if
        provided).
    where: {'before', 'after', 'both'}
        Add noise either `'before'` or `'after'` every gate (default: `after`).
    verbose: bool, optional
        Verbose output.
    """
    from hybridq.circuit import Circuit

    # Check 'where'
    if where not in ['before', 'after']:
        raise ValueError("'where' can only be either 'before' or 'after'")

    # Convert circuit
    circuit = Circuit(circuit)

    # Convert probs
    probs = channel.__get_params(keys=sorted(set(g.n_qubits for g in circuit)),
                                 args=probs,
                                 value_type=float)

    # Define how to add noise
    def _add_noise(g):
        # Get probability
        p = probs[g.n_qubits]

        # Get noise
        noise = channel.GlobalDepolarizingChannel(g.qubits, p)

        # Return gates
        return [g] if isinstance(g, BaseChannel) else (
            [g, noise] if where == 'after' else [noise, g])

    # Update circuit
    return SuperCircuit(g for w in tqdm(
        circuit, disable=not verbose, desc='Add depolarizing noise')
                        for g in _add_noise(w))
Пример #4
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
Пример #5
0
def get_rqc(n_qubits: int,
            n_gates: int,
            *,
            indexes: list[int] = None,
            randomize_power: bool = True,
            use_clifford_only: bool = False,
            use_unitary_only: bool = True,
            use_random_indexes: bool = False,
            verbose: bool = False):
    """
  Generate random quantum circuit.
  """
    from tqdm.auto import tqdm

    # Initialize circuit
    circuit = Circuit()

    # If not provided, generate indexes
    indexes = get_indexes(n_qubits, use_random_indexes=use_random_indexes
                          ) if indexes is None else list(indexes)

    # Check that size is correct
    assert (len(indexes) == n_qubits)

    # Get random gates
    gates = (get_random_gate(randomize_power=randomize_power,
                             use_unitary_only=use_unitary_only,
                             use_clifford_only=use_clifford_only)
             for _ in range(n_gates))

    # Assign random qubits, and return circuit
    return Circuit(
        gate.on([
            indexes[i]
            for i in np.random.choice(n_qubits, gate.n_qubits, replace=False)
        ]) for gate in tqdm(gates,
                            disable=not verbose,
                            total=n_gates,
                            desc='Generating random circuit'))
Пример #6
0
def isclose(a: Circuit,
            b: Circuit,
            use_matrix_commutation: bool = True,
            max_n_qubits_matrix: int = 10,
            atol: float = 1e-8,
            verbose: bool = False) -> bool:
    """
    Check if `a` is close to `b` within the absolute tollerance
    `atol`.

    Parameters
    ----------
    circuit: Circuit[BaseGate]
        `Circuit` to compare with.
    use_matrix_commutation: bool, optional
        Use commutation rules. See `hybridq.circuit.utils.simplify`.
    max_n_qubits_matrix: int, optional
        Matrices are computes for gates with up to `max_n_qubits_matrix` qubits
        (default: 10).
    atol: float, optional
        Absolute tollerance.

    Returns
    -------
    bool
        `True` if the two circuits are close within the absolute tollerance
        `atol`, and `False` otherwise.

    See Also
    --------
    hybridq.circuit.utils.simplify

    Example
    -------
    >>> c = Circuit(Gate('H', [q]) for q in range(10))
    >>> c.isclose(Circuit(g**1.1 for g in c))
    False
    >>> c.isclose(Circuit(g**1.1 for g in c), atol=1e-1)
    True
    """

    # Get simplified circuit
    s = simplify(a + b.inv(),
                 use_matrix_commutation=use_matrix_commutation,
                 max_n_qubits_matrix=max_n_qubits_matrix,
                 atol=atol,
                 verbose=verbose)

    return not s or all(
        isidentity([g], atol=atol)
        for g in tqdm(s, disable=not verbose, desc='Check'))
Пример #7
0
def flatten(a: Circuit) -> Circuit:
    """
    Return a flattened circuit. More precisely, `flatten` iteratively looks for
    gates that provide `flatten` in order to return a flattened circuit.

    Parameters
    ----------
    a: Circuit
        Circuit to flatten.

    Returns
    -------
    Circuit
        Flattened circuit.
    """
    return Circuit(
        g for gs in a for g in (gs if gs.provides('flatten') else (gs,)))
Пример #8
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))
Пример #9
0
def popleft(circuit: list[BaseGate],
            pinned_qubits: list[any],
            atol: float = 1e-8,
            use_matrix_commutation: bool = True,
            simplify: bool = True,
            verbose: bool = False) -> Circuit:
    """
  Remove gates outside the lightcone created by pinned_qubits (starting from the right).
  """

    return Circuit(
        reversed(
            popright(list(reversed(circuit)),
                     pinned_qubits=pinned_qubits,
                     atol=atol,
                     use_matrix_commutation=use_matrix_commutation,
                     simplify=simplify,
                     verbose=verbose)))
Пример #10
0
def simplify(circuit: list[BaseGate],
             atol: float = 1e-8,
             use_matrix_commutation: bool = True,
             max_n_qubits_matrix: int = 10,
             remove_id_gates: bool = True,
             verbose: bool = False) -> Circuit:
    """
    Compress together gates up to the specified number of qubits.
    """

    # Initialize new circuit
    new_circuit = Circuit()

    # Remove gates if required
    if remove_id_gates:
        rev_circuit = (g for g in reversed(circuit) if g.name != 'I' and (
            not g.provides('matrix') or g.n_qubits > max_n_qubits_matrix or
            not isidentity([g], atol=atol)))
    else:
        rev_circuit = reversed(circuit)

    # Insert gates, one by one
    for gate in tqdm(rev_circuit,
                     disable=not verbose,
                     total=len(circuit),
                     desc='Simplify'):
        insert_from_left(new_circuit,
                         gate,
                         atol=atol,
                         use_matrix_commutation=use_matrix_commutation,
                         max_n_qubits_matrix=max_n_qubits_matrix,
                         simplify=True,
                         pop=False,
                         pinned_qubits=None,
                         inplace=True)

    # Return simplified circuit
    return new_circuit
Пример #11
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
Пример #12
0
def __convert(circuit: iter,
              parallel: {bool, int} = False,
              verbose: bool = False) -> Circuit:
    from tqdm.auto import tqdm

    # Get lenght
    try:
        total = len(circuit)
    except:
        total = None

    # Flatten circuit
    circuit = tuple(g for w in circuit
                    for g in (w if isinstance(w, tuple) else [w]))

    # Parallelize if requested
    if parallel:
        from hybridq.utils import isintegral
        from multiprocessing import Pool
        from time import sleep

        # Get number of parallel threads
        if isinstance(parallel, bool):
            from os import cpu_count
            parallel = cpu_count()
        elif isintegral(parallel) and parallel > 0:
            parallel = int(parallel)
        else:
            raise ValueError("'parallel' must be a positive integer")

        with Pool(parallel) as pool, tqdm(total=len(circuit),
                                          desc='Converting circuit',
                                          disable=not verbose) as pbar:
            # Get map
            _map = [pool.apply_async(__transform, (g, )) for g in circuit]

            # Wait till ready
            while 1:
                # Count number of complete
                c = sum(m.ready() for m in _map)

                # Update pbar
                pbar.n = c
                pbar.update()

                # Break if all complete
                if c == len(_map):
                    break

                # Sleep for a while
                sleep(0.1)

            # Return circuit
            return Circuit(m.get() for m in _map)

    # Otherwise, transform one by one
    else:
        return Circuit(
            tqdm(map(__transform, circuit),
                 total=total,
                 desc='Converting circuit',
                 disable=not verbose))
Пример #13
0
def matrix(circuit: iter[BaseGate],
           order: iter[any] = None,
           complex_type: any = 'complex64',
           max_compress: int = 4,
           verbose: bool = False) -> numpy.ndarray:
    """
    Return matrix representing `circuit`.

    Parameters
    ----------
    circuit: iter[BaseGate]
        Circuit to get the matrix from.
    order: iter[any], optional
        If specified, a matrix is returned following the order given by
        `order`. Otherwise, `circuit.all_qubits()` is used.
    max_compress: int, optional
        To reduce the computational cost, `circuit` is compressed prior to
        compute the matrix.
    complex_type: any, optional
        Complex type to use to compute the matrix.
    verbose: bool, optional
        Verbose output.

    Returns
    -------
    numpy.ndarray
        Unitary matrix of `circuit`.

    Example
    -------
    >>> # Define circuit
    >>> circuit = Circuit([Gate('CX', [1, 0])])
    >>> # Show qubits
    [0, 1]
    >>> circuit.all_qubits()
    >>> # Get matrix without specifying any qubits order
    >>> # (therefore using circuit.all_qubits() == [0, 1])
    >>> utils.matrix()
    array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
           [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
           [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],
           [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j]], dtype=complex64)
    >>> # Get matrix with a specific order of qubits
    >>> utils.matrix(Circuit([Gate('CX', [1, 0])]), order=[1, 0])
    array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
           [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
           [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j],
           [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j]], dtype=complex64)
    """

    # Convert iterable to Circuit
    circuit = Circuit(circuit)

    # Check order
    if order is not None:
        # Conver to list
        order = list(order)
        if set(order).difference(circuit.all_qubits()):
            raise ValueError(
                "'order' must be a valid permutation of indexes in 'Circuit'.")

    # Compress circuit
    if max_compress > 0:
        return matrix(Circuit(
            to_matrix_gate(c, complex_type=complex_type, max_compress=0)
            for c in compress(circuit, max_n_qubits=max_compress)),
                      order=order,
                      complex_type=complex_type,
                      max_compress=0,
                      verbose=verbose)

    # Get qubits
    qubits = circuit.all_qubits()
    n_qubits = len(qubits)

    # Initialize matrix
    U = np.reshape(np.eye(2**n_qubits, order='C', dtype=complex_type),
                   [2] * (2 * n_qubits))

    for g in tqdm(circuit, disable=not verbose):

        # Get gate's qubits
        _qubits = g.qubits
        _n_qubits = len(_qubits)

        # Get map
        _map = [qubits.index(q) for q in _qubits]
        _map += [x for x in range(n_qubits) if x not in _map]

        # Reorder qubits
        qubits = [qubits[x] for x in _map]

        # Update U
        U = np.reshape(
            g.matrix().astype(complex_type) @ np.reshape(
                np.transpose(U, _map + list(range(n_qubits, 2 * n_qubits))),
                (2**_n_qubits, 2**(2 * n_qubits - _n_qubits))),
            (2,) * (2 * n_qubits))

    # Get U
    U = np.reshape(
        np.transpose(U,
                     argsort(qubits) + list(range(n_qubits, 2 * n_qubits))),
        (2**n_qubits, 2**n_qubits))

    # Reorder if required
    if order and order != circuit.all_qubits():
        qubits = circuit.all_qubits()
        U = np.reshape(
            np.transpose(np.reshape(U, (2,) * (2 * n_qubits)),
                         [qubits.index(q) for q in order] +
                         [n_qubits + qubits.index(q) for q in order]),
            (2**n_qubits, 2**n_qubits))

    # Check U has the right type and order
    assert (U.dtype == np.dtype(complex_type))
    assert (U.data.c_contiguous)

    # Return matrix
    return U
Пример #14
0
def expectation_value(circuit: Circuit, op: Circuit, initial_state: str,
                      **kwargs) -> float:
    """
    Compute the expectation value of `op` for the given `circuit`, using
    `initial_state` as initial state.

    Parameters
    ----------
    circuit: Circuit
        Circuit to simulate.
    op: Circuit
        Operator used to compute the expectation value. `op` must be a valid
        `Circuit` containing only Pauli gates (that is, either `I`, `X`, `Y` or
        `Z` gates) acting on different qubits.
    initial_state: str
        Initial state used to compute the expectation value. Valid tokens for
        `initial_state` are:

        - `0`: qubit is set to `0` in the computational basis,
        - `1`: qubit is set to `1` in the computational basis,
        - `+`: qubit is set to `+` state in the computational basis,
        - `-`: qubit is set to `-` state in the computational basis.

    Returns
    -------
    float [, dict[any, any]]
        The expectation value of the operator `op`. If `return_info=True`,
        information gathered during the simulation are also returned.

    Other Parameters
    ----------------
    `expectation_value` uses all valid parameters for `update_pauli_string`.

    See Also
    --------
    `update_pauli_string`

    Example
    -------
    >>> # Define circuit
    >>> circuit = Circuit(
    >>>     [Gate('X', qubits=[0])**1.2,
    >>>      Gate('ISWAP', qubits=[0, 1])**2.3])
    >>>
    >>> # Define operator
    >>> op = Circuit([Gate('Z', qubits=[1])])
    >>>
    >>> # Get expectation value
    >>> clifford.expectation_value(circuit=circuit,
    >>>                            op=op,
    >>>                            initial_state='11',
    >>>                            float_type='float64')
    -0.6271482580325515
    """

    # ==== Set default parameters ====

    def _db_init():
        return [0]

    def _collect(db, ops, ph):
        # Compute expectation value given pauli string
        if next((False for x in ops if x in 'XY'), True):
            db[0] += ph

    def _merge(db, db_new):
        db[0] += db_new[0]

    def _prepare_state(state, qubits):
        c = Circuit()
        for q, s in zip(qubits, state):
            if s == '0':
                pass
            elif s == '1':
                c.append(Gate('X', [q]))
            elif s == '+':
                c.append(Gate('H', [q]))
            elif s == '-':
                c.extend([Gate('X', [q]), Gate('H', [q])])
            else:
                raise ValueError(f"Unexpected token '{s}'")
        return c

    kwargs.setdefault('use_mpi', None)
    kwargs.setdefault('return_info', False)

    # If use_mpi==False, force the non-use of MPI
    if kwargs['use_mpi'] is None and _detect_mpi:

        # Warn that MPI is used because detected
        warn("MPI has been detected. Using MPI.")

        # Set MPI to true
        kwargs['use_mpi'] = True

    # Get MPI info
    if kwargs['use_mpi']:
        from mpi4py import MPI
        _mpi_comm = MPI.COMM_WORLD
        _mpi_size = _mpi_comm.Get_size()
        _mpi_rank = _mpi_comm.Get_rank()

    # ================================

    # Prepare initial state
    if type(initial_state) == str:
        # Check
        if len(initial_state) != len(circuit.all_qubits()):
            raise ValueError(
                f"'initial_state' has the wrong number of qubits "
                f"(expected {len(circuit.all_qubits())}, got {len(initial_state)})."
            )
        # Get state
        initial_state = _prepare_state(initial_state, circuit.all_qubits())
    else:
        raise ValueError(
            f"'{type(initial_state)}' not supported for 'initial_state'.")

    # Get expectation value
    _res = update_pauli_string(initial_state + circuit,
                               op,
                               db_init=_db_init,
                               collect=_collect,
                               merge=_merge,
                               mpi_merge=False,
                               **kwargs)

    # Collect results
    if kwargs['use_mpi'] and _mpi_size > 1:
        _all_res = _mpi_comm.gather(_res, root=0)
        if _mpi_rank == 0:
            if kwargs['return_info']:
                infos = {}
                for _, _infos in _all_res:
                    for _k, _v in _infos.items():
                        if _k not in infos:
                            infos[_k] = [_v]
                        else:
                            infos[_k].append(_v)
                exp_value = sum(ev[0] for ev, _ in _all_res)
            else:
                exp_value = sum(ev[0] for ev in _all_res)
        else:
            exp_value = infos = None
    else:
        if kwargs['return_info']:
            exp_value = _res[0][0]
            infos = _res[1]
        else:
            exp_value = _res[0]

    # Return expectation value
    if kwargs['return_info']:
        return exp_value, infos
    else:
        return exp_value
Пример #15
0
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
Пример #16
0
def _simulate_tn(circuit: any, initial_state: any, final_state: any,
                 optimize: any, backend: any, complex_type: any,
                 tensor_only: bool, verbose: bool, **kwargs):
    import quimb.tensor as tn
    import cotengra as ctg

    # Get random leaves_prefix
    leaves_prefix = ''.join(
        np.random.choice(list('abcdefghijklmnopqrstuvwxyz'), size=20))

    # Initialize info
    _sim_info = {}

    # Alias for tn
    if optimize == 'tn':
        optimize = 'cotengra'

    if isinstance(circuit, Circuit):

        # Get number of qubits
        qubits = circuit.all_qubits()
        n_qubits = len(qubits)

        # If initial/final state is None, set to all .'s
        initial_state = '.' * n_qubits if initial_state is None else initial_state
        final_state = '.' * n_qubits if final_state is None else final_state

        # Initial and final states must be valid strings
        for state, sname in [(initial_state, 'initial_state'),
                             (final_state, 'final_state')]:
            # Get alphabet
            from string import ascii_letters

            # Check if string
            if not isinstance(state, str):
                raise ValueError(f"'{sname}' must be a valid string.")

            # Deprecated error
            if any(x in 'xX' for x in state):
                from hybridq.utils import DeprecationWarning
                from warnings import warn

                # Warn the user that '.' is used to represent open qubits
                warn(
                    "Since '0.6.3', letters in the alphabet are used to "
                    "trace selected qubits (including 'x' and 'X'). "
                    "Instead, '.' is used to represent an open qubit.",
                    DeprecationWarning)

            # Check only valid symbols are present
            if set(state).difference('01+-.' + ascii_letters):
                raise ValueError(f"'{sname}' contains invalid symbols.")

            # Check number of qubits
            if len(state) != n_qubits:
                raise ValueError(f"'{sname}' has the wrong number of qubits "
                                 f"(expected {n_qubits}, got {len(state)})")

        # Check memory
        if 2**(initial_state.count('.') +
               final_state.count('.')) > kwargs['max_largest_intermediate']:
            raise MemoryError("Memory for the given number of open qubits "
                              "exceeds the 'max_largest_intermediate'.")

        # Compress circuit
        if kwargs['compress']:
            if verbose:
                print(
                    f"Compress circuit (max_n_qubits={kwargs['compress']}): ",
                    end='',
                    file=stderr)
                _time = time()

            circuit = utils.compress(
                circuit,
                kwargs['compress']['max_n_qubits'] if isinstance(
                    kwargs['compress'], dict) else kwargs['compress'],
                verbose=verbose,
                **({
                    k: v
                    for k, v in kwargs['compress'].items()
                    if k != 'max_n_qubits'
                } if isinstance(kwargs['compress'], dict) else {}))

            circuit = Circuit(
                utils.to_matrix_gate(c, complex_type=complex_type)
                for c in circuit)
            if verbose:
                print(f"Done! ({time()-_time:1.2f}s)", file=stderr)

        # Get tensor network representation of circuit
        tensor, tn_qubits_map = utils.to_tn(circuit,
                                            return_qubits_map=True,
                                            leaves_prefix=leaves_prefix)

        # Define basic MPS
        _mps = {
            '0': np.array([1, 0]),
            '1': np.array([0, 1]),
            '+': np.array([1, 1]) / np.sqrt(2),
            '-': np.array([1, -1]) / np.sqrt(2)
        }

        # Attach initial/final state
        for state, ext in [(initial_state, 'i'), (final_state, 'f')]:
            for s, q in ((s, q) for s, q in zip(state, qubits) if s in _mps):
                inds = [f'{leaves_prefix}_{tn_qubits_map[q]}_{ext}']
                tensor &= tn.Tensor(_mps[s], inds=inds, tags=inds)

        # For each unique letter, apply trace
        for x in set(initial_state + final_state).difference(''.join(_mps) +
                                                             '.'):
            # Get indexes
            inds = [
                f'{leaves_prefix}_{tn_qubits_map[q]}_i'
                for s, q in zip(initial_state, qubits) if s == x
            ]
            inds += [
                f'{leaves_prefix}_{tn_qubits_map[q]}_f'
                for s, q in zip(final_state, qubits) if s == x
            ]

            # Apply trace
            tensor &= tn.Tensor(np.reshape([1] + [0] * (2**len(inds) - 2) +
                                           [1], (2, ) * len(inds)),
                                inds=inds)

        # Simplify if requested
        if kwargs['simplify_tn']:
            tensor.full_simplify_(kwargs['simplify_tn']).astype_(complex_type)
        else:
            # Otherwise, just convert to the given complex_type
            tensor.astype_(complex_type)

        # Get contraction from heuristic
        if optimize == 'cotengra' and kwargs['max_iterations'] > 0:

            # Create local client if MPI has been detected (not compatible with Dask at the moment)
            if _mpi_env and kwargs['parallel']:

                from distributed import Client, LocalCluster
                _client = Client(LocalCluster(processes=False))

            else:

                _client = None

            # Set cotengra parameters
            cotengra_params = lambda: ctg.HyperOptimizer(
                methods=kwargs['methods'],
                max_time=kwargs['max_time'],
                max_repeats=kwargs['max_repeats'],
                minimize=kwargs['minimize'],
                progbar=verbose,
                parallel=kwargs['parallel'],
                **kwargs['cotengra'])

            # Get optimized path
            opt = cotengra_params()
            info = tensor.contract(all, optimize=opt, get='path-info')

            # Get target size
            tli = kwargs['target_largest_intermediate']

            # Repeat for the requested number of iterations
            for _ in range(1, kwargs['max_iterations']):

                # Break if largest intermediate is equal or smaller than target
                if info.largest_intermediate <= tli:
                    break

                # Otherwise, restart
                _opt = cotengra_params()
                _info = tensor.contract(all, optimize=_opt, get='path-info')

                # Store the best
                if kwargs['minimize'] == 'size':

                    if _info.largest_intermediate < info.largest_intermediate or (
                            _info.largest_intermediate
                            == info.largest_intermediate
                            and _opt.best['flops'] < opt.best['flops']):
                        info = _info
                        opt = _opt

                else:

                    if _opt.best['flops'] < opt.best['flops'] or (
                            _opt.best['flops'] == opt.best['flops']
                            and _info.largest_intermediate <
                            info.largest_intermediate):
                        info = _info
                        opt = _opt

            # Close client if exists
            if _client:

                _client.shutdown()
                _client.close()

        # Just return tensor if required
        if tensor_only:
            if optimize == 'cotengra' and kwargs['max_iterations'] > 0:
                return tensor, (info, opt)
            else:
                return tensor

    else:

        # Set tensor
        tensor = circuit

        if len(optimize) == 2 and isinstance(
                optimize[0], PathInfo) and isinstance(
                    optimize[1], ctg.hyper.HyperOptimizer):

            # Get info and opt from optimize
            info, opt = optimize

            # Set optimization
            optimize = 'cotengra'

        else:

            # Get tensor and path
            tensor = circuit

    # Print some info
    if verbose:
        print(
            f'Largest Intermediate: 2^{np.log2(float(info.largest_intermediate)):1.2f}',
            file=stderr)
        print(
            f'Max Largest Intermediate: 2^{np.log2(float(kwargs["max_largest_intermediate"])):1.2f}',
            file=stderr)
        print(f'Flops: 2^{np.log2(float(info.opt_cost)):1.2f}', file=stderr)

    if optimize == 'cotengra':

        # Get indexes
        _inds = tensor.outer_inds()

        # Get input indexes and output indexes
        _i_inds = sort([x for x in _inds if x[-2:] == '_i'],
                       key=lambda x: int(x.split('_')[1]))
        _f_inds = sort([x for x in _inds if x[-2:] == '_f'],
                       key=lambda x: int(x.split('_')[1]))

        # Get order
        _inds = [_inds.index(x) for x in _i_inds + _f_inds]

        # Get slice finder
        sf = ctg.SliceFinder(info,
                             target_size=kwargs['max_largest_intermediate'])

        # Find slices
        with tqdm(kwargs['temperatures'], disable=not verbose,
                  leave=False) as pbar:
            for _temp in pbar:
                pbar.set_description(f'Find slices (T={_temp})')
                ix_sl, cost_sl = sf.search(temperature=_temp)

        # Get slice contractor
        sc = sf.SlicedContractor([t.data for t in tensor])

        # Update infos
        _sim_info.update({
            'flops': info.opt_cost,
            'largest_intermediate': info.largest_intermediate,
            'n_slices': cost_sl.nslices,
            'total_flops': cost_sl.total_flops
        })

        # Print some infos
        if verbose:
            print(
                f'Number of slices: 2^{np.log2(float(cost_sl.nslices)):1.2f}',
                file=stderr)
            print(f'Flops+Cuts: 2^{np.log2(float(cost_sl.total_flops)):1.2f}',
                  file=stderr)

        if kwargs['max_n_slices'] and sc.nslices > kwargs['max_n_slices']:
            raise RuntimeError(
                f'Too many slices ({sc.nslices} > {kwargs["max_n_slices"]})')

        # Contract tensor
        _li = np.log2(float(info.largest_intermediate))
        _mli = np.log2(float(kwargs["max_largest_intermediate"]))
        _tensor = sc.gather_slices((sc.contract_slice(
            i, backend=backend
        ) for i in tqdm(
            range(sc.nslices),
            desc=f'Contracting tensor (li=2^{_li:1.0f}, mli=2^{_mli:1.1f})',
            leave=False)))

        # Create map
        _map = ''.join([get_symbol(x) for x in range(len(_inds))])
        _map += '->'
        _map += ''.join([get_symbol(x) for x in _inds])

        # Reorder tensor
        tensor = contract(_map, _tensor)

        # Deprecated
        ## Reshape tensor
        #if _inds:
        #    if _i_inds and _f_inds:
        #        tensor = np.reshape(tensor, (2**len(_i_inds), 2**len(_f_inds)))
        #    else:
        #        tensor = np.reshape(tensor,
        #                            (2**max(len(_i_inds), len(_f_inds)),))

    else:

        # Contract tensor
        tensor = tensor.contract(optimize=optimize, backend=backend)

        if hasattr(tensor, 'inds'):

            # Get input indexes and output indexes
            _i_inds = sort([x for x in tensor.inds if x[-2:] == '_i'],
                           key=lambda x: int(x.split('_')[1]))
            _f_inds = sort([x for x in tensor.inds if x[-2:] == '_f'],
                           key=lambda x: int(x.split('_')[1]))

            # Transpose tensor
            tensor.transpose(*(_i_inds + _f_inds), inplace=True)

            # Deprecated
            ## Reshape tensor
            #if _i_inds and _f_inds:
            #    tensor = np.reshape(tensor, (2**len(_i_inds), 2**len(_f_inds)))
            #else:
            #    tensor = np.reshape(tensor,
            #                        (2**max(len(_i_inds), len(_f_inds)),))

    if kwargs['return_info']:
        return tensor, _sim_info
    else:
        return tensor
Пример #17
0
def _simulate_evolution(circuit: iter[Gate], initial_state: any,
                        final_state: any, optimize: any, backend: any,
                        complex_type: any, verbose: bool, **kwargs):
    """
    Perform simulation of the circuit by using the direct evolution of the quantum state.
    """

    if _detect_mpi:
        warn("Detected MPI but optimize='evolution' does not support MPI.")

    # Initialize info
    _sim_info = {}

    # Convert iterable to circuit
    circuit = Circuit(circuit)

    # Get number of qubits
    qubits = circuit.all_qubits()
    n_qubits = len(qubits)

    # Check if core libraries have been loaded properly
    if any(not x for x in
           [_swap_core, _dot_core, _to_complex_core, _log2_pack_size]):
        warn("Cannot find C++ HybridQ core. "
             "Falling back to optimize='evolution-einsum' instead.")
        optimize = 'einsum'

    # If the system is too small, fallback to einsum
    if optimize == 'hybridq' and n_qubits <= max(10, _log2_pack_size):
        warn("The system is too small to use optimize='evolution-hybridq'. "
             "Falling back to optimize='evolution-einsum'")
        optimize = 'einsum'

    if verbose:
        print(f'# Optimization: {optimize}', file=stderr)

    # Check memory
    if 2**n_qubits > kwargs['max_largest_intermediate']:
        raise MemoryError(
            "Memory for the given number of qubits exceeds the 'max_largest_intermediate'."
        )

    # If final_state is specified, warn user
    if final_state is not None:
        warn(
            f"'final_state' cannot be specified in optimize='{optimize}'. Ignoring 'final_state'."
        )

    # Initial state must be provided
    if initial_state is None:
        raise ValueError(
            "'initial_state' must be specified for optimize='evolution'.")

    # Convert complex_type to np.dtype
    complex_type = np.dtype(complex_type)

    # Print info
    if verbose:
        print(f"Compress circuit (max_n_qubits={kwargs['compress']}): ",
              end='',
              file=stderr)
        _time = time()

    # Compress circuit
    circuit = utils.compress(
        circuit,
        kwargs['compress']['max_n_qubits'] if isinstance(
            kwargs['compress'], dict) else kwargs['compress'],
        verbose=verbose,
        skip_compression=[pr.FunctionalGate],
        **({
            k: v
            for k, v in kwargs['compress'].items() if k != 'max_n_qubits'
        } if isinstance(kwargs['compress'], dict) else {}))

    # Check that FunctionalGate's are not compressed
    assert (all(not isinstance(g, pr.FunctionalGate) if len(x) > 1 else True
                for x in circuit for g in x))

    # Compress everything which is not a FunctionalGate
    circuit = Circuit(g for c in (c if any(
        isinstance(g, pr.FunctionalGate)
        for g in c) else [utils.to_matrix_gate(c, complex_type=complex_type)]
                                  for c in circuit) for g in c)

    # Get state
    initial_state = prepare_state(initial_state,
                                  complex_type=complex_type) if isinstance(
                                      initial_state, str) else initial_state

    if verbose:
        print(f"Done! ({time()-_time:1.2f}s)", file=stderr)

    if optimize == 'hybridq':

        if complex_type not in ['complex64', 'complex128']:
            warn(
                "optimize=evolution-hybridq only support ['complex64', 'complex128']. Using 'complex64'."
            )
            complex_type = np.dtype('complex64')

        # Get float_type
        float_type = np.real(np.array(1, dtype=complex_type)).dtype

        # Get C float_type
        c_float_type = {
            np.dtype('float32'): ctypes.c_float,
            np.dtype('float64'): ctypes.c_double
        }[float_type]

        # Load libraries
        _apply_U = _dot_core[float_type]

        # Get swap core
        _swap = _swap_core[float_type]

        # Get to_complex core
        _to_complex = _to_complex_core[complex_type]

        # Get states
        _psi = aligned.empty(shape=(2, ) + initial_state.shape,
                             dtype=float_type,
                             order='C',
                             alignment=32)
        # Split in real and imaginary part
        _psi_re = _psi[0]
        _psi_im = _psi[1]

        # Check alignment
        assert (_psi_re.ctypes.data % 32 == 0)
        assert (_psi_im.ctypes.data % 32 == 0)

        # Get C-pointers
        _psi_re_ptr = _psi_re.ctypes.data_as(ctypes.POINTER(c_float_type))
        _psi_im_ptr = _psi_im.ctypes.data_as(ctypes.POINTER(c_float_type))

        # Initialize
        np.copyto(_psi_re, np.real(initial_state))
        np.copyto(_psi_im, np.imag(initial_state))

        # Create index maps
        _map = {q: n_qubits - x - 1 for x, q in enumerate(qubits)}
        _inv_map = [q for q, _ in sort(_map.items(), key=lambda x: x[1])]

        # Set largest swap_size
        _max_swap_size = 0

        # Start clock
        _ini_time = time()

        # Apply all gates
        for gate in tqdm(circuit, disable=not verbose):

            # FunctionalGate
            if isinstance(gate, pr.FunctionalGate):
                # Get order
                order = tuple(
                    q
                    for q, _ in sorted(_map.items(), key=lambda x: x[1])[::-1])

                # Apply gate to state
                new_psi, new_order = gate.apply(psi=_psi, order=order)

                # Copy back if needed
                if new_psi is not _psi:
                    # Align if needed
                    _psi = aligned.asarray(new_psi,
                                           order='C',
                                           alignment=32,
                                           dtype=_psi.dtype)

                    # Redefine real and imaginary part
                    _psi_re = _psi[0]
                    _psi_im = _psi[1]

                    # Get C-pointers
                    _psi_re_ptr = _psi_re.ctypes.data_as(
                        ctypes.POINTER(c_float_type))
                    _psi_im_ptr = _psi_im.ctypes.data_as(
                        ctypes.POINTER(c_float_type))

                # This can be eventually fixed ...
                if any(x != y for x, y in zip(order, new_order)):
                    raise RuntimeError("'order' has changed.")

            elif gate.provides(['qubits', 'matrix']):

                # Check if any qubits is withing the pack_size
                if any(q in _inv_map[:_log2_pack_size] for q in gate.qubits):

                    #@@@ Alternative way to always use the smallest swap size
                    #@@@
                    #@@@ # Get positions
                    #@@@ _pos = np.fromiter((_map[q] for q in gate.qubits),
                    #@@@                    dtype=int)

                    #@@@ # Get smallest swap size
                    #@@@ _swap_size = 0 if np.all(_pos >= _log2_pack_size) else next(
                    #@@@     k
                    #@@@     for k in range(_log2_pack_size, 2 *
                    #@@@                    max(len(_pos), _log2_pack_size) + 1)
                    #@@@     if sum(_pos < k) <= k - _log2_pack_size)

                    #@@@ # Get new order
                    #@@@ _order = [
                    #@@@     x for x, q in enumerate(_inv_map[:_swap_size])
                    #@@@     if q not in gate.qubits
                    #@@@ ]
                    #@@@ _order += [
                    #@@@     x for x, q in enumerate(_inv_map[:_swap_size])
                    #@@@     if q in gate.qubits
                    #@@@ ]

                    if len(gate.qubits) <= 4:

                        # Get new order
                        _order = [
                            x for x, q in enumerate(_inv_map[:8])
                            if q not in gate.qubits
                        ]
                        _order += [
                            x for x, q in enumerate(_inv_map[:8])
                            if q in gate.qubits
                        ]

                    else:

                        # Get qubit indexes for gate
                        _gate_idxs = [_inv_map.index(q) for q in gate.qubits]

                        # Get new order
                        _order = [
                            x for x in range(n_qubits) if x not in _gate_idxs
                        ][:_log2_pack_size]
                        _order += [x for x in _gate_idxs if x < max(_order)]

                    # Get swap size
                    _swap_size = len(_order)

                    # Update max swap size
                    if _swap_size > _max_swap_size:
                        _max_swap_size = _swap_size

                    # Update maps
                    _inv_map[:_swap_size] = [
                        _inv_map[:_swap_size][x] for x in _order
                    ]
                    _map.update(
                        {q: x
                         for x, q in enumerate(_inv_map[:_swap_size])})

                    # Apply swap
                    _order = np.array(_order, dtype='uint32')
                    _swap(
                        _psi_re_ptr,
                        _order.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)),
                        n_qubits, len(_order))
                    _swap(
                        _psi_im_ptr,
                        _order.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)),
                        n_qubits, len(_order))

                # Get positions
                _pos = np.array([_map[q] for q in reversed(gate.qubits)],
                                dtype='uint32')

                # Get matrix
                _U = np.asarray(gate.matrix(), dtype=complex_type, order='C')

                # Apply matrix
                if _apply_U(
                        _psi_re_ptr, _psi_im_ptr,
                        _U.ctypes.data_as(ctypes.POINTER(c_float_type)),
                        _pos.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)),
                        n_qubits, len(_pos)):

                    raise RuntimeError('something went wrong')

            else:
                raise RuntimeError(f"'{gate}' not supported")

        # Check maps are still consistent
        assert (all(_inv_map[_map[q]] == q for q in _map))

        # Swap back to the correct order
        _order = np.array([_inv_map.index(q)
                           for q in reversed(qubits)][:_max_swap_size],
                          dtype='uint32')
        _swap(_psi_re_ptr,
              _order.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)), n_qubits,
              len(_order))
        _swap(_psi_im_ptr,
              _order.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)), n_qubits,
              len(_order))

        # Stop clock
        _end_time = time()

        # Copy the results
        if kwargs['return_numpy_array']:
            _complex_psi = np.empty(_psi.shape[1:], dtype=complex_type)
            _to_complex(
                _psi_re_ptr, _psi_im_ptr,
                _complex_psi.ctypes.data_as(ctypes.POINTER(c_float_type)),
                2**n_qubits)
            _psi = _complex_psi

        # Update info
        _sim_info['runtime (s)'] = _end_time - _ini_time

    elif optimize.split('-')[0] == 'einsum':

        optimize = '-'.join(optimize.split('-')[1:])
        if not optimize:
            optimize = 'auto'

        # Split circuits to separate FunctionalGate's
        circuit = utils.compress(
            circuit,
            max_n_qubits=len(qubits),
            skip_compression=[pr.FunctionalGate],
            **({
                k: v
                for k, v in kwargs['compress'].items() if k != 'max_n_qubits'
            } if isinstance(kwargs['compress'], dict) else {}))

        # Check that FunctionalGate's are not compressed
        assert (all(
            not isinstance(g, pr.FunctionalGate) if len(x) > 1 else True
            for x in circuit for g in x))

        # Prepare initial_state
        _psi = initial_state

        # Initialize time
        _ini_time = time()
        for circuit in circuit:

            # Check
            assert (all(not isinstance(g, pr.FunctionalGate) for g in circuit)
                    or len(circuit) == 1)

            # Apply gate if functional
            if len(circuit) == 1 and isinstance(circuit[0], pr.FunctionalGate):

                # Apply gate to state
                _psi, qubits = circuit[0].apply(psi=_psi, order=qubits)

            else:
                # Get gates and corresponding qubits
                _qubits, _gates = zip(
                    *((c.qubits,
                       np.reshape(c.matrix().astype(complex_type), (2, ) *
                                  (2 * len(c.qubits)))) for c in circuit))

                # Initialize map
                _map = {q: get_symbol(x) for x, q in enumerate(qubits)}
                _count = n_qubits
                _path = ''.join((_map[q] for q in qubits))

                # Generate map
                for _qs in _qubits:

                    # Initialize local paths
                    _path_in = _path_out = ''

                    # Add incoming legs
                    for _q in _qs:
                        _path_in += _map[_q]

                    # Add outcoming legs
                    for _q in _qs:
                        _map[_q] = get_symbol(_count)
                        _count += 1
                        _path_out += _map[_q]

                    # Update path
                    _path = _path_out + _path_in + ',' + _path

                # Make sure that qubits order is preserved
                _path += '->' + ''.join([_map[q] for q in qubits])

                # Contracts
                _psi = contract(_path,
                                *reversed(_gates),
                                _psi,
                                backend=backend,
                                optimize=optimize)

                # Block JAX until result is ready (for a more precise runtime)
                if backend == 'jax' and kwargs['block_until_ready']:
                    _psi.block_until_ready()

        # Stop time
        _end_time = time()

        # Update info
        _sim_info['runtime (s)'] = _end_time - _ini_time

    else:

        raise ValueError(f"optimize='{optimize}' not implemented.")

    if verbose:
        print(f'# Runtime (s): {_sim_info["runtime (s)"]:1.2f}', file=stderr)

    # Return state
    if kwargs['return_info']:
        return _psi, _sim_info
    else:
        return _psi
Пример #18
0
def add_dephasing_noise(circuit: Circuit,
                        probs: {float, list[float, ...], dict[any, float]},
                        pauli_indexes: {int, list[int, ...], dict[any,
                                                                  int]} = 3,
                        where: {'before', 'after'} = 'after',
                        verbose: bool = False):
    """
    Given a `Circuit`, add dephasing noise after each instance of a `Gate`,
    which acts independently on the qubits of the gate.
    Note, noise will not be added after an instance of `BaseChannel`

    circuit: Circuit
        The `Circuit` which will be modified. Note, a new `Circuit` is
        returned (this is not in place).
    probs: {float, list[float, ...], dict[any, float]}
        Dephasing probabilities for `circuit`. If `probs` is a single `float`,
        the same probability is applied to all qubits. If `probs` is a list,
        the k-th value is used as the probability for the k-th qubit. If `probs`
        is a `dict`, `probs[q]` will be used as probability for the qubit `q`.
        If `probs[q]` is missing, the probability for a qubit `q` will fallback
        to `probs[any]` (if provided).
    pauli_indexes: {int, list[int, ...], dict[any, int]}
        Pauli indexes representing the dephasing axis (Pauli matrix).  If
        `pauli_indexes` is a single `int`, the same axis is applied to all
        qubits. If `pauli_indexes` is a list, the k-th value is used as the
        axis for the k-th qubit. If `pauli_indexes` is a `dict`,
        `pauli_indexes[q]` will be used as axis for the qubit `q`. If
        `pauli_indexes[q]` is missing, the axis for a qubit `q` will fallback
        to `pauli_indexes[any]` (if provided).
    where: {'before', 'after', 'both'}
        Add noise either `'before'` or `'after'` every gate (default: `after`).
    verbose: bool, optional
        Verbose output.
    """
    from hybridq.circuit import Circuit

    # Check 'where'
    if where not in ['before', 'after']:
        raise ValueError("'where' can only be either 'before' or 'after'")

    # Convert circuit
    circuit = Circuit(circuit)

    # Get all qubits
    qubits = circuit.all_qubits()

    # Convert gammas and probs
    probs = channel.__get_params(qubits, probs, value_type=float)
    pauli_indexes = channel.__get_params(qubits, pauli_indexes, value_type=int)

    # Define how to add noise
    def _add_noise(g):
        # Get gammas and probs
        _probs = {q: probs[q] for q in g.qubits}
        _axis = {q: pauli_indexes[q] for q in g.qubits}

        # Get noise
        noise = channel.LocalDephasingChannel(g.qubits,
                                              p=_probs,
                                              pauli_index=_axis)

        # Return gates
        return [g] if isinstance(
            g, BaseChannel) else ((g, ) +
                                  noise if where == 'after' else noise + (g, ))

    # Update circuit
    return SuperCircuit(g for w in tqdm(
        circuit, disable=not verbose, desc='Add amplitude damping noise')
                        for g in _add_noise(w))
Пример #19
0
def to_nx(circuit: iter[BaseGate],
          add_final_nodes: bool = True,
          node_tags: dict = None,
          edge_tags: dict = None,
          return_qubits_map: bool = False,
          leaves_prefix: str = 'q') -> networkx.Graph:
    """
    Return graph representation of circuit. `to_nx` is deterministic, so it can
    be reused elsewhere.

    Parameters
    ----------
    circuit: iter[BaseGate]
        Circuit to get graph representation from.
    add_final_nodes: bool, optional
        Add final nodes for each qubit to the graph representation of `circuit`.
    node_tags: dict, optional
        Add specific tags to nodes.
    edge_tags: dict, optional
        Add specific tags to edges.
    return_qubits_map: bool, optional
        Return map associated to the Circuit qubits.
    leaves_prefix: str, optional
        Specify prefix to use for leaves.

    Returns
    -------
    networkx.Graph
        Graph representing `circuit`.

    Example
    -------
    >>> import networkx as nx
    >>>
    >>> # Define circuit
    >>> circuit = Circuit(
    >>>     [Gate('X', qubits=[0])**1.2,
    >>>      Gate('ISWAP', qubits=[0, 1])**2.3], Gate('H', [1]))
    >>>
    >>> # Draw graph
    >>> nx.draw_planar(utils.to_nx(circuit))

    .. image:: ../../images/circuit_nx.png
    """
    import networkx as nx

    # Initialize
    if node_tags is None:
        node_tags = {}
    if edge_tags is None:
        edge_tags = {}

    # Check if node is a leaf
    def _is_leaf(node):
        return type(node) == str and node[:len(leaves_prefix)] == leaves_prefix

    # Convert iterable to Circuit
    circuit = Circuit(circuit)

    # Get graph
    graph = nx.DiGraph()

    # Get qubits
    qubits = circuit.all_qubits()

    # Get qubits_map
    qubits_map = {q: i for i, q in enumerate(qubits)}

    # Check that no qubits is 'confused' as leaf
    if any(_is_leaf(q) for q in qubits):
        raise ValueError(
            f"No qubits must start with 'leaves_prefix'={leaves_prefix}.")

    # Add first layer
    for q in qubits:
        graph.add_node(f'{leaves_prefix}_{qubits_map[q]}_i',
                       qubits=[q],
                       **node_tags)

    # Last leg
    last_leg = {q: f'{leaves_prefix}_{qubits_map[q]}_i' for q in qubits}

    # Build network
    for x, gate in enumerate(circuit):

        # Add node
        graph.add_node(x,
                       circuit=Circuit([gate]),
                       qubits=sort(gate.qubits),
                       **node_tags)

        # Add edges (time directed)
        graph.add_edges_from([(last_leg[q], x) for q in gate.qubits],
                             **edge_tags)

        # Update last_leg
        last_leg.update({q: x for q in gate.qubits})

    # Add last indexes if required
    if add_final_nodes:
        for q in qubits:
            graph.add_node(f'{leaves_prefix}_{qubits_map[q]}_f',
                           qubits=[q],
                           **node_tags)
        graph.add_edges_from([(x, f'{leaves_prefix}_{qubits_map[q]}_f')
                              for q, x in last_leg.items()], **edge_tags)

    if return_qubits_map:
        return graph, qubits_map
    else:
        return graph
Пример #20
0
def insert_from_left(circuit: iter[BaseGate],
                     gate: BaseGate,
                     atol: float = 1e-8,
                     *,
                     use_matrix_commutation: bool = True,
                     max_n_qubits_matrix: int = 10,
                     simplify: bool = True,
                     pop: bool = False,
                     pinned_qubits: list[any] = None,
                     inplace: bool = False) -> Circuit:
    """
    Add a gate to circuit starting from the left, commuting with existing gates
    if necessary.

    Parameters
    ----------
    circuit: Circuit
        `gate` will be added to `circuit`.
    gate: Gate
        Gate to add to `circuit`.
    atol: float, optional
        Absolute tollerance while simplifying.
    use_matrix_commutation: bool, optional
        Use matrix commutation while simplifying `circuit`.
    max_n_qubits_matrix: int, optional
        Matrices are computes for gates with up to `max_n_qubits_matrix` qubits
        (default: 10).
    simplify: bool, optional
        Simplify `circuit` while adding `gate` (default: `True`).
    pop: bool, optional
        Remove `gate` if it commutes with all gates in `circuit` (default: `False`).
    pinned_qubits: list[any], optional
        If `pop` is `True`, remove gates unless `gate` share qubits with
        `pinned_qubits` (default: `None`).
    inplace: bool, optional
        If `True`, add `gate` to `circuit` in-place (default: `False`)

    Returns
    -------
    Circuit
        Circuit with `gate` added to it.
    """
    from copy import deepcopy

    # Copy circuit if required
    if not inplace:
        circuit = Circuit(circuit, copy=True)

    # Get qubits
    if gate.provides('qubits') and gate.qubits is not None:
        _qubits = set(gate.qubits)
    else:
        # If gate has not qubits, just append to the left
        circuit.insert(0, deepcopy(gate))
        return circuit

    # Iterate over all the gates
    for _p, _g in enumerate(circuit):
        # Remove if gate simplifies with _g
        try:
            if simplify and gate.inv().isclose(_g, atol=atol):
                del (circuit[_p])
                return circuit
        except:
            pass

        # Otherwise, check if gate can commute with _g. If not, insert gate
        # and exit loop.
        _commute = False
        try:
            if _g.n_qubits <= max_n_qubits_matrix:
                _commute |= not _qubits.intersection(_g.qubits)
                _commute |= use_matrix_commutation and gate.commutes_with(
                    _g, atol=atol)
        except:
            pass
        finally:
            if not _commute:
                circuit.insert(_p, deepcopy(gate))
                return circuit

    # If commutes with everything, just append at the end
    if not pop or _qubits.intersection(pinned_qubits):
        circuit.append(deepcopy(gate))

    # Return circuit
    return circuit
Пример #21
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
Пример #22
0
def to_qasm(circuit: Circuit, qubits_map: dict[any, int] = None) -> str:
    """
    Convert a `Circuit` to QASM language.

    Parameters
    ----------
    circuit: Circuit
        `Circuit` to convert to QASM language.
    qubits_map: dict[any, int], optional
        If provided, `qubits_map` map qubit indexes in `Circuit` to qubit
        indexes in QASM. Otherwise, indexes are assigned to QASM qubits by using
        `Circuit.all_qubits()` order.

    Returns
    -------
    str
        String representing the QAMS circuit.

    Notes
    -----

    The QASM language used in HybridQ is compatible with the standard QASM.
    However, HybridQ introduces few extensions, which are recognized by the
    parser using ``#@`` at the beginning of the line (``#`` at the beginning of
    the line represent a general comment in QASM). At the moment, the following
    QAMS extensions are supported:

    * **qubits**, used to store `qubits_map`,
    * **power**, used to store the power of the gate,
    * **tags**, used to store the tags associated to the gate,
    * **U**, used to store the matrix reprentation of the gate if gate name is `MATRIX`

    If `Gate.qubits` are not specified, a single `.` is used to represent the
    missing qubits. If `Gate.params` are missing, parameters are just omitted.

    Example
    -------
    >>> from hybridq.extras.qasm import to_qasm
    >>> print(to_qasm(Circuit(Gate('H', [q]) for q in range(10))))
    10
    #@ qubits =
    #@ {
    #@   "0": "0",
    #@   "1": "1",
    #@   "2": "2",
    #@   "3": "3",
    #@   "4": "4",
    #@   "5": "5",
    #@   "6": "6",
    #@   "7": "7",
    #@   "8": "8",
    #@   "9": "9"
    #@ }
    h 0
    h 1
    h 2
    h 3
    h 4
    h 5
    h 6
    h 7
    h 8
    h 9
    >>> c = Circuit()
    >>> c.append(Gate('RX', tags={'params': False, 'qubits': False}))
    >>> c.append(Gate('RY', params=[1.23], tags={'params': True, 'qubits': False}))
    >>> c.append(Gate('RZ', qubits=[42], tags={'params': False, 'qubits': True})**1.23)
    >>> c.append(Gate('MATRIX', U=Gate('H').matrix()))
    >>> print(to_qasm(c))
    1
    #@ qubits =
    #@ {
    #@   "0": "42"
    #@ }
    #@ tags =
    #@ {
    #@   "params": false,
    #@   "qubits": false
    #@ }
    rx .
    #@ tags =
    #@ {
    #@   "params": true,
    #@   "qubits": false
    #@ }
    ry . 1.23
    #@ tags =
    #@ {
    #@   "params": false,
    #@   "qubits": true
    #@ }
    #@ power = 1.23
    rz 0
    #@ U =
    #@ [
    #@   [
    #@     "0.7071067811865475",
    #@     "0.7071067811865475"
    #@   ],
    #@   [
    #@     "0.7071067811865475",
    #@     "-0.7071067811865475"
    #@   ]
    #@ ]
    matrix .
    """

    # Initialize output
    _out = ''

    # Get qubits map
    if qubits_map is None:
        qubits_map = {
            q: x
            for x, q in enumerate(
                circuit.all_qubits(ignore_missing_qubits=True))
        }

    # Get inverse map
    inv_qubits_map = {x: str(q) for q, x in qubits_map.items()}

    # Dump number of qubits
    _out += f'{len(qubits_map)}\n'

    # Dump map
    _out += '#@ qubits = \n'
    _out += '\n'.join(
        ['#@ ' + x for x in json.dumps(inv_qubits_map, indent=2).split('\n')])
    _out += '\n'

    for gate in circuit:

        # Store matrix
        if gate.name == 'MATRIX':
            _out += '#@ U = \n'
            _out += '\n'.join('#@ ' + x
                              for x in json.dumps([[str(y) for y in x]
                                                   for x in gate.Matrix],
                                                  indent=2).split('\n'))
            _out += '\n'

        # Dump tags
        if gate.provides('tags') and gate.tags is not None:
            _out += '#@ tags = \n'
            _out += '\n'.join([
                '#@ ' + x for x in json.dumps(gate.tags, indent=2).split('\n')
            ])
            _out += '\n'

        # Dump power
        if gate.provides('power') and gate.power != 1:
            _out += f'#@ power = {gate.power}\n'

        # Dump conjugation
        if gate.provides('is_conjugated') and gate.is_conjugated():
            _out += f'#@ conj\n'

        # Dump transposition
        if gate.provides('is_transposed') and gate.is_transposed():
            _out += f'#@ T\n'

        # Dump name
        _out += gate.name.lower()

        # Dump gates
        if gate.provides('qubits') and gate.qubits is not None:
            _out += ' ' + ' '.join((str(qubits_map[x]) for x in gate.qubits))
        else:
            _out += ' .'

        # Dump params
        if gate.provides('params'):
            if gate.params is not None:
                _out += ' ' + ' '.join((str(x) for x in gate.params))
            else:
                raise ValueError('Not yet implemented.')

        # Add newline
        _out += '\n'

    return _out
Пример #23
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()))
Пример #24
0
def simulate(circuit: SuperCircuit,
             initial_state: any,
             final_state: any = None,
             optimize: any = 'evolution',
             parallel: {bool, int} = False,
             verbose: bool = False,
             **kwargs):
    """
    Frontend to simulate `rho` using different optimization models and
    backends.

    Parameters
    ----------
    circuit: Circuit
        Circuit to simulate.
    initial_state: {str, Circuit, array_like}
        Initial density matrix to evolve.
    final_state: {str, Circuit, array_like}
        Final density matrix to project to.
    optimize: any
        Method to use to perform the simulation. The available methods are:
        - `evolution`: Evolve the density matrix using state vector evolution
        - `tn`: Evolve the density matrix using tensor contraction
        - `clifford`: Evolve the density matrix using Clifford expansion

    See Also
    --------
    `hybridq.circuit.simulation` and `hybridq.circuit.simulation.clifford`
    """
    from hybridq.circuit import Circuit

    # Convert circuit to list
    circuit = list(circuit)

    if optimize == 'clifford':
        from hybridq.circuit.simulation.clifford import update_pauli_string
        from hybridq.gate import BaseGate

        # Check that all gates are BaseGate
        if any(not isinstance(gate, BaseGate) for gate in circuit):
            raise NotImplementedError(
                "'optimize=clifford' only supports 'BaseGate's")

        # Final state cannot be provided if optimize='clifford'
        if final_state is not None:
            raise ValueError(
                "'final_state' cannot be provided if optimize='clifford'.")

        # Get circuit
        circuit = Circuit(circuit)

        # Try to convert to circuit
        try:
            initial_state = Circuit(initial_state)
        except:
            pass

        # Return Clifford expandion
        return update_pauli_string(circuit=circuit,
                                   pauli_string=initial_state,
                                   parallel=parallel,
                                   verbose=verbose,
                                   **kwargs)

    else:
        from hybridq.circuit.simulation import simulate
        from hybridq.utils import sort

        # Convert to SuperCircuit
        circuit = SuperCircuit(circuit)

        # Get left/right qubits
        l_qubits, r_qubits = circuit.all_qubits()

        # Convert SuperCircuit to Circuit
        circuit = __convert(circuit, parallel=parallel, verbose=verbose)

        # Get number of qubits
        nl = len(l_qubits)
        nr = len(r_qubits)

        # Check states
        def _get_state(state, name):
            # If None, return None
            if state is None:
                return None

            # Check if string
            elif isinstance(state, str):
                # If single char, extend to full size
                state = state * (nl + nr) if len(state) == 1 else state

                # Check that state has the right number of chars
                if not (len(state) == (nl + nr) or
                        (l_qubits == r_qubits and len(state) == nl)):
                    raise ValueError(
                        f"'{name}' has the wrong number of qubits.")

                # Extend if needed
                state = state + state if len(state) == nl else state

                # Return
                return state

            # Check if Circuit
            elif isinstance(state, Circuit):
                from hybridq.circuit.utils import matrix

                # Check that qubits are consistent
                if l_qubits != r_qubits or sort(l_qubits) != sort(
                        state.all_qubits()):
                    raise ValueError(
                        f"Qubits in '{name}' are not consistent with 'circuit'."
                    )

                # Get matrix
                U = matrix(state, order=l_qubits)

                # Swap input/output
                return np.transpose(np.reshape(U, (2, ) * 2 * nl),
                                    list(range(nl, 2 * nl)) + list(range(nl)))

            else:
                # Try to convert to numpy array
                state = np.asarray(state)

                # At the moment, only 2-dimensional qubits are allowed
                if set(state.shape) != {2}:
                    raise NotImplementedError(
                        "Only 2-dimensional qubits are allowed.")

                # Check if the number of dimensions matches
                if not (state.ndim == (nl + nr) or
                        (l_qubits == r_qubits and state.ndim == nl)):
                    raise ValueError(
                        f"'{name}' has the wrong number of qubits.")

                # Extend if needed
                if state.ndim == nl:
                    state = np.reshape(np.kron(state.ravel(), state.ravel()),
                                       (2, ) * 2 * nl)

                # Return state
                return state

        # Get states
        initial_state = _get_state(initial_state, 'initial_state')
        final_state = _get_state(final_state, 'final_state')

        # Return results
        return simulate(circuit=circuit,
                        initial_state=initial_state,
                        final_state=final_state,
                        optimize=optimize,
                        parallel=parallel,
                        verbose=verbose,
                        **kwargs)
Пример #25
0
def simulate(circuit: {Circuit, TensorNetwork},
             initial_state: any = None,
             final_state: any = None,
             optimize: any = 'evolution',
             backend: any = 'numpy',
             complex_type: any = 'complex64',
             tensor_only: bool = False,
             simplify: {bool, dict} = True,
             remove_id_gates: bool = True,
             use_mpi: bool = None,
             atol: float = 1e-8,
             verbose: bool = False,
             **kwargs) -> any:
    """
    Frontend to simulate `Circuit` using different optimization models and
    backends.

    Parameters
    ----------
    circuit: {Circuit, TensorNetwork}
        Circuit to simulate.
    initial_state: any, optional
        Initial state to use.
    final_state: any, optional
        Final state to use (only valid for `optimize='tn'`).
    optimize: any, optional
        Optimization to use. At the moment, HybridQ supports two optimizations:
        `optimize='evolution'` (equivalent to `optimize='evolution-hybridq'`)
        and `optimize='tn'` (equivalent to `optimize='cotengra'`).
        `optimize='evolution'` takes an `initial_state` (it can either be a
        string, which is processed using ```prepare_state``` or an `Array`)
        and evolve the quantum state accordingly to `Circuit`. Alternatives are:

        - `optimize='evolution-hybridq'`: use internal `C++` implementation for
          quantum state evolution that uses vectorization instructions (such as
          AVX instructions for Intel processors). This optimization method is
          best suitable for `CPU`s.
        - `optimize='evolution-einsum'`: use `einsum` to perform the evolution
          of the quantum state (via `opt_einsum`). It is possible to futher
          specify optimization for `opt_einsum` by using
          `optimize='evolution-einsum-opt'` where `opt` is one of the available
          optimization in `opt_einsum.contract` (default: `auto`). This
          optimization is best suitable for `GPU`s and `TPU`s (using
          `backend='jax'`).

        `optimize='tn'` (or, equivalently, `optimize='cotengra'`) performs the
        tensor contraction of `Circuit` given an `initial_state` and a
        `final_state` (both must be a `str`). Valid tokens for both
        `initial_state` and `final_state` are:

        - `0`: qubit is set to `0` in the computational basis,
        - `1`: qubit is set to `1` in the computational basis,
        - `+`: qubit is set to `+` state in the computational basis,
        - `-`: qubit is set to `-` state in the computational basis,
        - `.`: qubit is left uncontracted.

        Before the actual contraction, `cotengra` is called to identify an
        optimal contraction. Such contraction is then used to perform the tensor
        contraction.

        If `Circuit` is a `TensorNetwork`, `optimize` must be a
        valid contraction (see `tensor_only` parameter).
    backend: any, optional
        Backend used to perform the simulation. Backend must have `tensordot`,
        `transpose` and `einsum` methods.
    complex_type: any, optional
        Complex type to use for the simulation.
    tensor_only: bool, optional
        If `True` and `optimize=None`, `simulate` will return a
        `TensorNetwork` representing `Circuit`. Otherwise, if
        `optimize='cotengra'`, `simulate` will return the `tuple`
        ```(TensorNetwork```, ```ContractionInfo)```. ```TensorNetwork``` and
        and ```ContractionInfo``` can be respectively used as values for
        `circuit` and `optimize` to perform the actual contraction.
    simplify: {bool, dict}, optional
        Circuit is simplified before the simulation using
        `circuit.utils.simplify`. If non-empty `dict` is provided, `simplify`
        is passed as arguments for `circuit.utils.simplity`.
    remove_id_gates: bool, optional
        Identity gates are removed before to perform the simulation.
        If `False`, identity gates are kept during the simulation.
    use_mpi: bool, optional
        Use `MPI` if available. Unless `use_mpi=False`, `MPI` will be used if
        detected (for instance, if `mpiexec` is used to called HybridQ). If
        `use_mpi=True`, force the use of `MPI` (in case `MPI` is not
        automatically detected).
    atol: float, optional
        Use `atol` as absolute tollerance.
    verbose: bool, optional
        Verbose output.

    Returns
    -------
    Output of `simulate` depends on the chosen parameters.

    Other Parameters
    ----------------
    parallel: int (default: False)
        Parallelize simulation (where possible). If `True`, the number of
        available cpus is used. Otherwise, a `parallel` number of threads is
        used.
    compress: {int, dict} (default: auto)
        Select level of compression for ```circuit.utils.compress```, which is
        run on `Circuit` prior to perform the simulation. If non-empty `dict`
        is provided, `compress` is passed as arguments for
        `circuit.utils.compress`. If `optimize=evolution`, `compress` is set to
        `4` by default. Otherwise, if `optimize=tn`, `compress` is set to `2`
        by default.
    allow_sampling: bool (default: False)
        If `True`, `Gate`s that provide the method `sample` will not be sampled.
    sampling_seed: int (default: None)
        If provided, `numpy.random` state will be saved before sampling and
        `sampling_seed` will be used to sample `Gate`s. `numpy.random` state will
        be restored after sampling.
    block_until_ready: bool (default: True)
        When `backend='jax'`, wait till the results are ready before returning.
    return_numpy_array: bool (default: True)
        When `optimize='hybridq'` and `return_numpy_array` is `False, a `tuple`
        of two `np.ndarray` is returned, corresponding to the real and
        imaginary part of the quantu state. If `True`, the real and imaginary
        part are copied to a single `np.ndarray` of complex numbers.
    return_info: bool (default: False)
        Return extra information collected during the simulation.
    simplify_tn: str (default: 'RC')
        Simplification to apply to `TensorNetwork`. Available simplifications as
        specified in `quimb.tensor.TensorNetwork.full_simplify`.
    max_largest_intermediate: int (default: 2**26)
        Largest intermediate which is allowed during simulation. If
        `optimize='evolution'`, `simulate` will raise an error if the
        largest intermediate is larger than `max_largest_intermediate`.  If
        `optimize='tn'`, slicing will be applied to fit the contraction in
        memory.
    target_largest_intermediate: int (default: 0)
        Stop `cotengra` if a contraction having the largest intermediate smaller
        than `target_largest_intermediate` is found.
    max_iterations: int (default: 1)
        Number of `cotengra` iterations to find optimal contration.
    max_time: int (default: 120)
        Maximum number of seconds allowed to `cotengra` to find optimal
        contraction for each iteration.
    max_repeats: int (default: 16)
        Number of `cotengra` steps to find optimal contraction for each
        iteration.
    temperatures: list[float] (default: [1.0, 0.1, 0.01])
        Temperatures used by `cotengra` to find optimal slicing of the tensor
        network.
    max_n_slices: int (default: None)
        If specified, `simulate` will raise an error if the number of
        slices to fit the tensor contraction in memory is larger than
        `max_n_slices`.
    minimize: str (default: 'combo')
        Cost function to minimize while looking for the best contraction (see
        `cotengra` for more information).
    methods: list[str] (default: ['kahypar', 'greedy'])
        Heuristics used by `cotengra` to find optimal contraction.
    cotengra: dict[any, any] (default: {})
        Extra parameters to pass to `cotengra`.
    """

    # Set defaults
    kwargs.setdefault('allow_sampling', False)
    kwargs.setdefault('sampling_seed', None)

    # Convert simplify
    simplify = simplify if isinstance(simplify, bool) else dict(simplify)

    # Checks
    if tensor_only and type(optimize) == str and 'evolution' in optimize:
        raise ValueError(
            f"'tensor_only' is not support for optimize={optimize}")

    # Try to convert to circuit
    try:
        circuit = Circuit(circuit)
    except:
        pass

    # Simplify circuit
    if isinstance(circuit, Circuit):
        # Flatten circuit
        circuit = utils.flatten(circuit)

        # If 'sampling_seed' is provided, use it
        if kwargs['sampling_seed'] is not None:
            # Store numpy.random state
            __state = np.random.get_state()

            # Set seed
            np.random.seed(int(kwargs['sampling_seed']))

        # If stochastic gates are present, randomly sample from them
        circuit = Circuit(g.sample() if isinstance(g, pr.StochasticGate)
                          and kwargs['allow_sampling'] else g for g in circuit)

        # Restore numpy.random state
        if kwargs['sampling_seed'] is not None:
            np.random.set_state(__state)

        # Get qubits
        qubits = circuit.all_qubits()
        n_qubits = len(qubits)

        # Prepare state
        def _prepare_state(state):
            if isinstance(state, str):
                if len(state) == 1:
                    state *= n_qubits
                if len(state) != n_qubits:
                    raise ValueError(
                        "Wrong number of qubits for initial/final state.")
                return state
            else:
                # Convert to np.ndarray
                state = np.asarray(state)
                # For now, it only supports "qubits" ...
                if any(x != 2 for x in state.shape):
                    raise ValueError(
                        "Only qubits of dimension 2 are supported.")
                # Check number of qubits is consistent
                if state.ndim != n_qubits:
                    raise ValueError(
                        "Wrong number of qubits for initial/final state.")
                return state

        # Prepare initial/final state
        initial_state = None if initial_state is None else _prepare_state(
            initial_state)
        final_state = None if final_state is None else _prepare_state(
            final_state)

        # Strip Gate('I')
        if remove_id_gates:
            circuit = Circuit(gate for gate in circuit if gate.name != 'I')
        # Simplify circuit
        if simplify:
            circuit = utils.simplify(
                circuit,
                remove_id_gates=remove_id_gates,
                atol=atol,
                verbose=verbose,
                **(simplify if isinstance(simplify, dict) else {}))

        # Stop if qubits have changed
        if circuit.all_qubits() != qubits:
            raise ValueError(
                "Active qubits have changed after simplification. Forcing stop."
            )

    # Simulate
    if type(optimize) == str and 'evolution' in optimize:

        # Set default parameters
        optimize = '-'.join(optimize.split('-')[1:])
        if not optimize:
            optimize = 'hybridq'
        kwargs.setdefault('compress', 4)
        kwargs.setdefault('max_largest_intermediate', 2**26)
        kwargs.setdefault('return_info', False)
        kwargs.setdefault('block_until_ready', True)
        kwargs.setdefault('return_numpy_array', True)

        return _simulate_evolution(circuit, initial_state, final_state,
                                   optimize, backend, complex_type, verbose,
                                   **kwargs)
    else:

        # Set default parameters
        kwargs.setdefault('compress', 2)
        kwargs.setdefault('simplify_tn', 'RC')
        kwargs.setdefault('max_iterations', 1)
        try:
            import kahypar as __kahypar__
            kwargs.setdefault('methods', ['kahypar', 'greedy'])
        except ModuleNotFoundError:
            warn("Cannot find module kahypar. Remove it from defaults.")
            kwargs.setdefault('methods', ['greedy'])
        except ImportError:
            warn("Cannot import module kahypar. Remove it from defaults.")
            kwargs.setdefault('methods', ['greedy'])
        kwargs.setdefault('max_time', 120)
        kwargs.setdefault('max_repeats', 16)
        kwargs.setdefault('minimize', 'combo')
        kwargs.setdefault('target_largest_intermediate', 0)
        kwargs.setdefault('max_largest_intermediate', 2**26)
        kwargs.setdefault('temperatures', [1.0, 0.1, 0.01])
        kwargs.setdefault('parallel', None)
        kwargs.setdefault('cotengra', {})
        kwargs.setdefault('max_n_slices', None)
        kwargs.setdefault('return_info', False)

        # If use_mpi==False, force the non-use of MPI
        if not (use_mpi == False) and (use_mpi or _detect_mpi):

            # Warn that MPI is used because detected
            if not use_mpi:
                warn("MPI has been detected. Using MPI.")

            from hybridq.circuit.simulation.simulation_mpi import _simulate_tn_mpi
            return _simulate_tn_mpi(circuit, initial_state, final_state,
                                    optimize, backend, complex_type,
                                    tensor_only, verbose, **kwargs)
        else:

            if _detect_mpi and use_mpi == False:
                warn(
                    "MPI has been detected but use_mpi==False. Not using MPI as requested."
                )

            return _simulate_tn(circuit, initial_state, final_state, optimize,
                                backend, complex_type, tensor_only, verbose,
                                **kwargs)
Пример #26
0
def compress(circuit: iter[BaseGate],
             max_n_qubits: int = 2,
             *,
             exclude_qubits: iter[any] = None,
             use_matrix_commutation: bool = True,
             max_n_qubits_matrix: int = 10,
             skip_compression: iter[{type, str}] = None,
             skip_commutation: iter[{type, str}] = None,
             atol: float = 1e-8,
             verbose: bool = False) -> list[Circuit]:
    """
    Compress gates together up to the specified number of qubits. `compress`
    is deterministic, so it can be reused elsewhere.

    Parameters
    ----------
    circuit: iter[BaseGate]
        Circuit to compress.
    max_n_qubits: int, optional
        Maximum number of qubits that a compressed gate may have.
    exclude_qubits: list[any], optional
        Exclude gates which act on `exclude_qubits` to be compressed.
    use_matrix_commutation: bool, optional
        If `True`, use commutation to maximize compression.
    max_n_qubits_matrix: int, optional
        Limit the size of matrices when checking for commutation.
    skip_compression: iter[{type, str}], optional
        If `BaseGate` is either an instance of any types in `skip_compression`,
        it provides any methods in `skip_compression`, or `BaseGate` name will
        match any names in `skip_compression`, `BaseGate` will not be
        compressed. However, if `use_matrix_commutation` is `True`, commutation will
        be checked against `BaseGate`.
    skip_commutation: iter[{type, str}], optional
        If `BaseGate` is either an instance of any types in `skip_commutation`,
        it provides any methods in `skip_commutation`, or `BaseGate` name will
        match any names in `skip_commutation`, `BaseGate` will not be checked
        against commutation.
    atol: float
        Absolute tollerance for commutation.
    verbose: bool, optional
        Verbose output.

    Returns
    -------
    list[Circuit]
        A list of `Circuit`s, with each `Circuit` representing a compressed
        `BaseGate`.

    See Also
    --------
    hybridq.gate.commutes_with

    Example
    -------
    >>> # Define circuit
    >>> circuit = Circuit(
    >>>     [Gate('X', qubits=[0])**1.2,
    >>>      Gate('ISWAP', qubits=[0, 1])**2.3,
    >>>      Gate('ISWAP', qubits=[0, 2])**2.3])
    >>>
    >>> # Compress circuit up to 1-qubit gates
    >>> utils.compress(circuit, 1)
    [Circuit([
        Gate(name=X, qubits=[0])**1.2
     ]),
     Circuit([
        Gate(name=ISWAP, qubits=[0, 1])**2.3
     ]),
     Circuit([
        Gate(name=ISWAP, qubits=[0, 2])**2.3
     ])]
    >>> # Compress circuit up to 2-qubit gates
    >>> utils.compress(circuit, 2)
    [Circuit([
        Gate(name=X, qubits=[0])**1.2
        Gate(name=ISWAP, qubits=[0, 1])**2.3
     ]),
     Circuit([
        Gate(name=ISWAP, qubits=[0, 2])**2.3
     ])]
    >>> # Compress circuit up to 3-qubit gates
    >>> utils.compress(circuit, 3)
    [Circuit([
        Gate(name=X, qubits=[0])**1.2
        Gate(name=ISWAP, qubits=[0, 1])**2.3
        Gate(name=ISWAP, qubits=[0, 2])**2.3
     ])]
    """
    # If max_n_qubits <= 0, split every gate
    if max_n_qubits <= 0:
        return [Circuit([g]) for g in circuit]

    # Initialize skip_compression and skip_commutation
    skip_compression = tuple() if skip_compression is None else tuple(
        skip_compression)
    skip_commutation = tuple() if skip_commutation is None else tuple(
        skip_commutation)

    def _check_skip(gate, x):
        if isinstance(x, type):
            return isinstance(gate, x)
        elif isinstance(x, str):
            return gate.name == x.upper() or gate.provides(x)
        else:
            raise ValueError(f"'{x}' not supported.")

    # Initialize exclude_qubits
    exclude_qubits = set([] if exclude_qubits is None else exclude_qubits)

    # Convert to Circuit
    circuit = Circuit(circuit)

    # Initialize compressed circuit
    new_circuit = []

    # For every gate in circuit ..
    for gate in tqdm(circuit,
                     disable=not verbose,
                     desc=f'Compress ({max_n_qubits})'):
        # Initialize matrix gate
        _gate = None

        # Initialize _compress
        gate_properties = dict(compress=True, commute=True)

        # Initialize index
        _merge_to = len(new_circuit)

        # If gate does not provide qubits or qubits is None,
        # then gate is not compressible
        if not gate.provides('qubits') or gate.qubits is None:
            gate_properties['compress'] = False
            gate_properties['commute'] = False

        # Otherwise ...
        else:
            # Get qubits
            _q = set(gate.qubits)

            # Get Matrix gate if possible
            try:
                _gate = to_matrix_gate(
                    [gate], max_compress=0) if use_matrix_commutation and len(
                        _q) <= max_n_qubits_matrix else None
            except:
                _gate = None

            # Check if gate must skip compression
            if any(_check_skip(gate, t) for t in skip_compression
                  ) or _q.intersection(exclude_qubits):
                gate_properties['compress'] = False

            # Check if gate must skip commutation
            if any(_check_skip(gate, t) for t in skip_commutation):
                gate_properties['commute'] = False

            # Check for each existing layer
            for i, (_circ, _circ_gate,
                    _circ_properties) in reversed(list(enumerate(new_circuit))):

                # If _circ does not provide any qubits, just break
                try:
                    # Get circuit qubits
                    _cq = set(_circ.all_qubits())
                except:
                    break

                # Check if both gate and _circ can be compressed
                if gate_properties['compress'] and _circ_properties['compress']:
                    # Check if it can be merged
                    if len(_q.union(_cq)) <= max(max_n_qubits, len(_cq),
                                                 len(_q)):
                        _merge_to = i

                # Check commutation
                if use_matrix_commutation and gate_properties[
                        'commute'] and _circ_properties['commute']:
                    # Check if gate and _circ share any qubit
                    if not _q.intersection(_cq):
                        continue

                    # Check if gate and _circ commute using matrix
                    try:
                        if _gate.commutes_with(_circ_gate):
                            continue
                    except:
                        pass

                # Otherwise, just break
                break

        # If it possible to merge the gate to an existing layer
        if _merge_to < len(new_circuit):
            # Get layer
            _nc = new_circuit[_merge_to]

            # Update circuit
            _nc[0].append(gate)

            # Update matrix
            try:
                _nc[1] = to_matrix_gate(
                    [_nc[1], _gate],
                    max_compress=0) if use_matrix_commutation and len(
                        set(_gate.qubits).union(
                            _nc[1].qubits)) <= max_n_qubits_matrix else None
            except:
                _nc[1] = None

            # Update properties
            for k in ['compress', 'commute']:
                _nc[2][k] &= gate_properties[k]

        # Otherwise, create a new layer
        else:
            new_circuit.append([Circuit([gate]), _gate, gate_properties])

    # Return only circuits
    return [c for c, _, _ in new_circuit]
Пример #27
0
def to_tn(circuit: iter[BaseGate],
          complex_type: any = 'complex64',
          return_qubits_map: bool = False,
          leaves_prefix: str = 'q_') -> quimb.tensor.TensorNetwork:
    """
    Return `quimb.tensor.TensorNetwork` representing `circuit`. `to_tn` is
    deterministic, so it can be reused elsewhere.

    Parameters
    ----------
    circuit: iter[BaseGate]
        Circuit to get `quimb.tensor.TensorNetwork` representation from.
    complex_type: any, optional
        Complex type to use while getting the `quimb.tensor.TensorNetwork`
        representation.
    return_qubits_map: bool, optional
        Return map associated to the Circuit qubits.
    leaves_prefix: str, optional
        Specify prefix to use for leaves.

    Returns
    -------
    quimb.tensor.TensorNetwork
        Tensor representing `circuit`.

    Example
    -------
    >>> # Define circuit
    >>> circuit = Circuit(
    >>>     [Gate('X', qubits=[0])**1.2,
    >>>      Gate('ISWAP', qubits=[0, 1])**2.3], Gate('H', [1]))
    >>>
    >>> # Draw graph
    >>> utils.to_tn(circuit).graph()

    .. image:: ../../images/circuit_tn.png
    """
    import quimb.tensor as tn

    # Convert iterable to Circuit
    circuit = Circuit(circuit)

    # Get all qubits
    all_qubits = circuit.all_qubits()

    # Get qubits map
    qubits_map = {q: i for i, q in enumerate(all_qubits)}

    # Get last_tag
    last_tag = {q: 'i' for q in all_qubits}

    # Node generator
    def _get_node(t, gate):

        # Get matrix
        U = np.reshape(gate.matrix().astype(complex_type),
                       [2] * (2 * len(gate.qubits)))

        # Get indexes
        inds = [f'{leaves_prefix}_{qubits_map[q]}_{t}' for q in gate.qubits] + [
            f'{leaves_prefix}_{qubits_map[q]}_{last_tag[q]}'
            for q in gate.qubits
        ]

        # Update last_tag
        for q in gate.qubits:
            last_tag[q] = t

        # Return node
        return tn.Tensor(
            U.astype(complex_type),
            inds=inds,
            tags=[f'{leaves_prefix}_{qubits_map[q]}' for q in gate.qubits] +
            [f'gate-idx_{t}'])

    # Get list of tensors
    tensor = [_get_node(t, gate) for t, gate in enumerate(circuit)]

    # Generate new output map
    output_map = {
        f'{leaves_prefix}_{qubits_map[q]}_{t}':
        f'{leaves_prefix}_{qubits_map[q]}_f' for q, t in last_tag.items()
    }

    # Rename output legs
    for node in tensor:
        node.reindex(output_map, inplace=True)

    # Return tensor network
    if return_qubits_map:
        return tn.TensorNetwork(tensor), qubits_map
    else:
        return tn.TensorNetwork(tensor)
Пример #28
0
if __name__ == '__main__':

    # Set number of qubits
    n_qubits = 23

    # Set number of gates
    n_gates = 2000

    # Generate RQC
    print('# Generate RQC: ', end='')
    circuit = get_rqc(n_qubits, n_gates, use_random_indexes=True)
    print('Done!')

    # Compress circuit
    print('# Compress RQC: ', end='')
    circuit = Circuit(
        utils.to_matrix_gate(c) for c in utils.compress(circuit, 2))
    print('Done!')

    # Get final state
    print('# Compute quantum evolution: ', end='')
    psi = simulate(circuit,
                   optimize='evolution',
                   initial_state='0' * n_qubits,
                   verbose=True)
    print('Done!')

    # Print final state
    for i in np.random.choice(2**n_qubits, size=20, replace=False):
        x = bin(i)[2:].zfill(n_qubits)
        print(f'|{x}> = {psi[tuple(map(int, x))]:+1.5f}')
    print('...')
Пример #29
0
def _simulate_tn_mpi(circuit: Circuit, initial_state: any, final_state: any,
                     optimize: any, backend: any, complex_type: any,
                     tensor_only: bool, verbose: bool, **kwargs):
    import quimb.tensor as tn
    import cotengra as ctg

    # Get MPI
    _mpi_comm = MPI.COMM_WORLD
    _mpi_size = _mpi_comm.Get_size()
    _mpi_rank = _mpi_comm.Get_rank()

    # Set default parameters
    kwargs.setdefault('compress', 2)
    kwargs.setdefault('simplify_tn', 'RC')
    kwargs.setdefault('max_iterations', 1)
    kwargs.setdefault('methods', ['kahypar', 'greedy'])
    kwargs.setdefault('max_time', 120)
    kwargs.setdefault('max_repeats', 16)
    kwargs.setdefault('minimize', 'combo')
    kwargs.setdefault('target_largest_intermediate', 0)
    kwargs.setdefault('max_largest_intermediate', 2**26)
    kwargs.setdefault('temperatures', [1.0, 0.1, 0.01])
    kwargs.setdefault('parallel', None)
    kwargs.setdefault('cotengra', {})
    kwargs.setdefault('max_n_slices', None)
    kwargs.setdefault('return_info', False)

    # Get random leaves_prefix
    leaves_prefix = ''.join(
        np.random.choice(list('abcdefghijklmnopqrstuvwxyz'), size=20))

    # Initialize info
    _sim_info = {}

    # Alias for tn
    if optimize == 'tn':
        optimize = 'cotengra'

    if isinstance(circuit, Circuit):

        if not kwargs['parallel']:
            kwargs['parallel'] = 1
        else:
            # If number of threads not provided, just use half of the number of available cpus
            if isinstance(kwargs['parallel'],
                          bool) and kwargs['parallel'] == True:
                kwargs['parallel'] = cpu_count() // 2

        if optimize is not None and kwargs['parallel'] and kwargs[
                'max_iterations'] == 1:
            warn("Parallelization for MPI works for multiple iterations only. "
                 "For a better performance, use: 'max_iterations' > 1")

        # Get number of qubits
        qubits = circuit.all_qubits()
        n_qubits = len(qubits)

        # If initial/final state is None, set to all .'s
        initial_state = '.' * n_qubits if initial_state is None else initial_state
        final_state = '.' * n_qubits if final_state is None else final_state

        # Initial and final states must be valid strings
        for state, sname in [(initial_state, 'initial_state'),
                             (final_state, 'final_state')]:
            # Get alphabet
            from string import ascii_letters

            # Check if string
            if not isinstance(state, str):
                raise ValueError(f"'{sname}' must be a valid string.")

            # Deprecated error
            if any(x in 'xX' for x in state):
                from warnings import warn

                # Define new DeprecationWarning (to always print the warning
                # signal)
                class DeprecationWarning(Warning):
                    pass

                # Warn the user that '.' is used to represent open qubits
                warn(
                    "Since '0.6.3', letters in the alphabet are used to "
                    "trace selected qubits (including 'x' and 'X'). "
                    "Instead, '.' is used to represent an open qubit.",
                    DeprecationWarning)

            # Check only valid symbols are present
            if set(state).difference('01+-.' + ascii_letters):
                raise ValueError(f"'{sname}' contains invalid symbols.")

            # Check number of qubits
            if len(state) != n_qubits:
                raise ValueError(f"'{sname}' has the wrong number of qubits "
                                 f"(expected {n_qubits}, got {len(state)})")

        # Check memory
        if 2**(initial_state.count('.') +
               final_state.count('.')) > kwargs['max_largest_intermediate']:
            raise MemoryError("Memory for the given number of open qubits "
                              "exceeds the 'max_largest_intermediate'.")

        # Compress circuit
        if kwargs['compress']:
            if verbose:
                print(
                    f"Compress circuit (max_n_qubits={kwargs['compress']}): ",
                    end='',
                    file=stderr)
                _time = time()

            circuit = utils.compress(
                circuit,
                kwargs['compress']['max_n_qubits'] if isinstance(
                    kwargs['compress'], dict) else kwargs['compress'],
                verbose=verbose,
                **({
                    k: v
                    for k, v in kwargs['compress'].items()
                    if k != 'max_n_qubits'
                } if isinstance(kwargs['compress'], dict) else {}))

            circuit = Circuit(
                utils.to_matrix_gate(c, complex_type=complex_type)
                for c in circuit)
            if verbose:
                print(f"Done! ({time()-_time:1.2f}s)", file=stderr)

        # Get tensor network representation of circuit
        tensor, tn_qubits_map = utils.to_tn(circuit,
                                            return_qubits_map=True,
                                            leaves_prefix=leaves_prefix)

        # Define basic MPS
        _mps = {
            '0': np.array([1, 0]),
            '1': np.array([0, 1]),
            '+': np.array([1, 1]) / np.sqrt(2),
            '-': np.array([1, -1]) / np.sqrt(2)
        }

        # Attach initial/final state
        for state, ext in [(initial_state, 'i'), (final_state, 'f')]:
            for s, q in ((s, q) for s, q in zip(state, qubits) if s in _mps):
                inds = [f'{leaves_prefix}_{tn_qubits_map[q]}_{ext}']
                tensor &= tn.Tensor(_mps[s], inds=inds, tags=inds)

        # For each unique letter, apply trace
        for x in set(initial_state + final_state).difference(''.join(_mps) +
                                                             '.'):
            # Get indexes
            inds = [
                f'{leaves_prefix}_{tn_qubits_map[q]}_i'
                for s, q in zip(initial_state, qubits) if s == x
            ]
            inds += [
                f'{leaves_prefix}_{tn_qubits_map[q]}_f'
                for s, q in zip(final_state, qubits) if s == x
            ]

            # Apply trace
            tensor &= tn.Tensor(np.reshape([1] + [0] * (2**len(inds) - 2) +
                                           [1], (2, ) * len(inds)),
                                inds=inds)

        # Simplify if requested
        if kwargs['simplify_tn']:
            tensor.full_simplify_(kwargs['simplify_tn']).astype_(complex_type)
        else:
            # Otherwise, just convert to the given complex_type
            tensor.astype_(complex_type)

        # Get contraction from heuristic
        if optimize == 'cotengra' and kwargs['max_iterations'] > 0:

            # Set cotengra parameters
            def cotengra_params():
                # Get HyperOptimizer
                q = ctg.HyperOptimizer(methods=kwargs['methods'],
                                       max_time=kwargs['max_time'],
                                       max_repeats=kwargs['max_repeats'],
                                       minimize=kwargs['minimize'],
                                       progbar=False,
                                       parallel=False,
                                       **kwargs['cotengra'])

                # For some optlib, HyperOptimizer._retrieve_params is not
                # pickeable. Let's fix the problem by hand.
                q._retrieve_params = __FunctionWrap(q._retrieve_params)

                # Return HyperOptimizer
                return q

            # Get target size
            tli = kwargs['target_largest_intermediate']

            with Pool(kwargs['parallel']) as pool:

                # Sumbit jobs
                _opts = [
                    cotengra_params() for _ in range(kwargs['max_iterations'])
                ]
                _map = [
                    pool.apply_async(tensor.contract, (all, ),
                                     dict(optimize=_opt, get='path-info'))
                    for _opt in _opts
                ]

                with tqdm(total=len(_map),
                          disable=not verbose,
                          desc='Collecting contractions') as pbar:

                    _old_completed = 0
                    while 1:

                        # Count number of completed
                        _completed = 0
                        for _w in _map:
                            _completed += _w.ready()
                            if _w.ready() and not _w.successful():
                                _w.get()

                        # Update pbar
                        pbar.update(_completed - _old_completed)
                        _old_completed = _completed

                        if _completed == len(_map):
                            break

                        # Wait
                        sleep(1)

                # Collect results
                _infos = [_w.get() for _w in _map]

            if kwargs['minimize'] == 'size':
                opt, info = sort(
                    zip(_opts, _infos),
                    key=lambda w:
                    (w[1].largest_intermediate, w[0].best['flops']))[0]
            else:
                opt, info = sort(
                    zip(_opts, _infos),
                    key=lambda w:
                    (w[0].best['flops'], w[1].largest_intermediate))[0]

        if optimize == 'cotengra':

            # Gather best contractions
            _cost = _mpi_comm.gather(
                (info.largest_intermediate, info.opt_cost, _mpi_rank), root=0)
            if _mpi_rank == 0:
                if kwargs['minimize'] == 'size':
                    _best_rank = sort(_cost, key=lambda x: (x[0], x[1]))[0][-1]
                else:
                    _best_rank = sort(_cost, key=lambda x: (x[1], x[0]))[0][-1]
            else:
                _best_rank = None
            _best_rank = _mpi_comm.bcast(_best_rank, root=0)

            if hasattr(opt, '_pool'):
                del (opt._pool)

            # Distribute opt/info
            tensor, info, opt = _mpi_comm.bcast((tensor, info, opt),
                                                root=_best_rank)

        # Just return tensor if required
        if tensor_only:
            if optimize == 'cotengra' and kwargs['max_iterations'] > 0:
                return tensor, (info, opt)
            else:
                return tensor

    else:

        # Set tensor
        tensor = circuit

        if len(optimize) == 2 and isinstance(
                optimize[0], PathInfo) and isinstance(
                    optimize[1], ctg.hyper.HyperOptimizer):

            # Get info and opt from optimize
            info, opt = optimize

            # Set optimization
            optimize = 'cotengra'

        else:

            # Get tensor and path
            tensor = circuit

    # Print some info
    if verbose and _mpi_rank == 0:
        print(
            f'Largest Intermediate: 2^{np.log2(float(info.largest_intermediate)):1.2f}',
            file=stderr)
        print(
            f'Max Largest Intermediate: 2^{np.log2(float(kwargs["max_largest_intermediate"])):1.2f}',
            file=stderr)
        print(f'Flops: 2^{np.log2(float(info.opt_cost)):1.2f}', file=stderr)

    if optimize == 'cotengra':

        if _mpi_rank == 0:

            # Get indexes
            _inds = tensor.outer_inds()

            # Get input indexes and output indexes
            _i_inds = sort([x for x in _inds if x[-2:] == '_i'],
                           key=lambda x: int(x.split('_')[1]))
            _f_inds = sort([x for x in _inds if x[-2:] == '_f'],
                           key=lambda x: int(x.split('_')[1]))

            # Get order
            _inds = [_inds.index(x) for x in _i_inds + _f_inds]

            # Get slice finder
            sf = ctg.SliceFinder(
                info,
                target_size=kwargs['max_largest_intermediate'],
                allow_outer=False)

            # Find slices
            with tqdm(kwargs['temperatures'], disable=not verbose,
                      leave=False) as pbar:
                for _temp in pbar:
                    pbar.set_description(f'Find slices (T={_temp})')
                    ix_sl, cost_sl = sf.search(temperature=_temp)

            # Get slice contractor
            sc = sf.SlicedContractor([t.data for t in tensor])

            # Make sure that no open qubits are sliced
            assert (not {
                ix: i
                for i, ix in enumerate(sc.output) if ix in sc.sliced
            })

            # Print some infos
            if verbose:
                print(
                    f'Number of slices: 2^{np.log2(float(cost_sl.nslices)):1.2f}',
                    file=stderr)
                print(
                    f'Flops+Cuts: 2^{np.log2(float(cost_sl.total_flops)):1.2f}',
                    file=stderr)

            # Update infos
            _sim_info.update({
                'flops': info.opt_cost,
                'largest_intermediate': info.largest_intermediate,
                'n_slices': cost_sl.nslices,
                'total_flops': cost_sl.total_flops
            })

            # Get slices
            slices = list(range(cost_sl.nslices + 1)) + [None] * (
                _mpi_size -
                cost_sl.nslices) if cost_sl.nslices < _mpi_size else [
                    cost_sl.nslices / _mpi_size * i for i in range(_mpi_size)
                ] + [cost_sl.nslices]
            if not np.alltrue(
                [int(x) == x
                 for x in slices if x is not None]) or not np.alltrue([
                     slices[i] < slices[i + 1] for i in range(_mpi_size)
                     if slices[i] is not None and slices[i + 1] is not None
                 ]):
                raise RuntimeError('Something went wrong')

            # Convert all to integers
            slices = [int(x) if x is not None else None for x in slices]

        else:

            sc = slices = None

        # Distribute slicer and slices
        sc, slices = _mpi_comm.bcast((sc, slices), root=0)

        _n_slices = max(x for x in slices if x)
        if kwargs['max_n_slices'] and _n_slices > kwargs['max_n_slices']:
            raise RuntimeError(
                f'Too many slices ({_n_slices} > {kwargs["max_n_slices"]})')

        # Contract slices
        _tensor = None
        if slices[_mpi_rank] is not None and slices[_mpi_rank + 1] is not None:
            for i in tqdm(range(slices[_mpi_rank], slices[_mpi_rank + 1]),
                          desc='Contracting slices',
                          disable=not verbose,
                          leave=False):
                if _tensor is None:
                    _tensor = np.copy(sc.contract_slice(i, backend=backend))
                else:
                    _tensor += sc.contract_slice(i, backend=backend)

        # Gather tensors
        if _mpi_rank != 0:
            _mpi_comm.send(_tensor, dest=0, tag=11)
        elif _mpi_rank == 0:
            for i in tqdm(range(1, _mpi_size),
                          desc='Collecting tensors',
                          disable=not verbose):
                _p_tensor = _mpi_comm.recv(source=i, tag=11)
                if _p_tensor is not None:
                    _tensor += _p_tensor

        if _mpi_rank == 0:

            # Create map
            _map = ''.join([get_symbol(x) for x in range(len(_inds))])
            _map += '->'
            _map += ''.join([get_symbol(x) for x in _inds])

            # Reorder tensor
            tensor = contract(_map, _tensor)

            # Deprecated
            ## Reshape tensor
            #if _inds:
            #    if _i_inds and _f_inds:
            #        tensor = np.reshape(tensor,
            #                            (2**len(_i_inds), 2**len(_f_inds)))
            #    else:
            #        tensor = np.reshape(tensor,
            #                            (2**max(len(_i_inds), len(_f_inds)),))

        else:

            tensor = None

    else:

        if _mpi_rank == 0:

            # Contract tensor
            tensor = tensor.contract(optimize=optimize, backend=backend)

            if hasattr(tensor, 'inds'):

                # Get input indexes and output indexes
                _i_inds = sort([x for x in tensor.inds if x[-2:] == '_i'],
                               key=lambda x: int(x.split('_')[1]))
                _f_inds = sort([x for x in tensor.inds if x[-2:] == '_f'],
                               key=lambda x: int(x.split('_')[1]))

                # Transpose tensor
                tensor.transpose(*(_i_inds + _f_inds), inplace=True)

                # Deprecated
                ## Reshape tensor
                #if _i_inds and _f_inds:
                #    tensor = np.reshape(tensor,
                #                        (2**len(_i_inds), 2**len(_f_inds)))
                #else:
                #    tensor = np.reshape(tensor,
                #                        (2**max(len(_i_inds), len(_f_inds)),))

        else:

            tensor = None

    if kwargs['return_info']:
        return tensor, _sim_info
    else:
        return tensor
Пример #30
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,