class CircuitSampler(ConverterBase):
    """
    The CircuitSampler traverses an Operator and converts any CircuitStateFns into
    approximations of the state function by a DictStateFn or VectorStateFn using a quantum
    backend. Note that in order to approximate the value of the CircuitStateFn, it must 1) send
    state function through a depolarizing channel, which will destroy all phase information and
    2) replace the sampled frequencies with **square roots** of the frequency, rather than the raw
    probability of sampling (which would be the equivalent of sampling the **square** of the
    state function, per the Born rule.

    The CircuitSampler aggressively caches transpiled circuits to handle re-parameterization of
    the same circuit efficiently. If you are converting multiple different Operators,
    you are better off using a different CircuitSampler for each Operator to avoid cache thrashing.
    """
    def __init__(self,
                 backend: Union[BaseBackend, QuantumInstance] = None,
                 statevector: Optional[bool] = None,
                 param_qobj: bool = False,
                 attach_results: bool = False) -> None:
        """
        Args:
            backend: The quantum backend or QuantumInstance to use to sample the circuits.
            statevector: If backend is a statevector backend, whether to replace the
                CircuitStateFns with DictStateFns (from the counts) or VectorStateFns (from the
                statevector). ``None`` will set this argument automatically based on the backend.
            param_qobj: (TODO, not yet available) Whether to use Aer's parameterized Qobj
                capability to avoid re-assembling the circuits.
            attach_results: Whether to attach the data from the backend ``Results`` object for
                a given ``CircuitStateFn``` to an ``execution_results`` field added the converted
                ``DictStateFn`` or ``VectorStateFn``.

        Raises:
            ValueError: Set statevector or param_qobj True when not supported by backend.
        """
        self._quantum_instance = backend if isinstance(backend, QuantumInstance) else\
            QuantumInstance(backend=backend)
        self._statevector = statevector if statevector is not None \
            else self.quantum_instance.is_statevector
        self._param_qobj = param_qobj
        self._attach_results = attach_results

        self._check_quantum_instance_and_modes_consistent()

        # Object state variables
        self._last_op = None
        self._reduced_op_cache = None
        self._circuit_ops_cache = {}
        self._transpiled_circ_cache = None
        self._transpile_before_bind = True
        self._binding_mappings = None

    def _check_quantum_instance_and_modes_consistent(self) -> None:
        """ Checks whether the statevector and param_qobj settings are compatible with the
        backend

        Raises:
            ValueError: statevector or param_qobj are True when not supported by backend.
        """
        if self._statevector and not is_statevector_backend(
                self.quantum_instance.backend):
            raise ValueError(
                'Statevector mode for circuit sampling requires statevector '
                'backend, not {}.'.format(self.quantum_instance.backend))

        if self._param_qobj and not is_aer_provider(
                self.quantum_instance.backend):
            raise ValueError('Parameterized Qobj mode requires Aer '
                             'backend, not {}.'.format(
                                 self.quantum_instance.backend))

    @property
    def backend(self) -> BaseBackend:
        """ Returns the backend.

        Returns:
             The backend used by the CircuitSampler
        """
        return self.quantum_instance.backend

    @backend.setter
    def backend(self, backend: BaseBackend):
        """ Sets backend without additional configuration. """
        self.set_backend(backend)

    def set_backend(self, backend: BaseBackend, **kwargs) -> None:
        """ Sets backend with configuration.

        Raises:
            ValueError: statevector or param_qobj are True when not supported by backend.
        """
        self.quantum_instance = QuantumInstance(backend)
        self.quantum_instance.set_config(**kwargs)

    @property
    def quantum_instance(self) -> QuantumInstance:
        """ Returns the quantum instance.

        Returns:
             The QuantumInstance used by the CircuitSampler
        """
        return self._quantum_instance

    @quantum_instance.setter
    def quantum_instance(
            self, quantum_instance: Union[QuantumInstance,
                                          BaseBackend]) -> None:
        """ Sets the QuantumInstance.

        Raises:
            ValueError: statevector or param_qobj are True when not supported by backend.
        """
        if isinstance(quantum_instance, BaseBackend):
            quantum_instance = QuantumInstance(quantum_instance)
        self._quantum_instance = quantum_instance
        self._check_quantum_instance_and_modes_consistent()

    # pylint: disable=arguments-differ
    def convert(
        self,
        operator: OperatorBase,
        params: Optional[Dict[Union[ParameterExpression, ParameterVector],
                              Union[float, List[float],
                                    List[List[float]]]]] = None
    ) -> OperatorBase:
        r"""
        Converts the Operator to one in which the CircuitStateFns are replaced by
        DictStateFns or VectorStateFns. Extracts the CircuitStateFns out of the Operator,
        caches them, calls ``sample_circuits`` below to get their converted replacements,
        and replaces the CircuitStateFns in operator with the replacement StateFns.

        Args:
            operator: The Operator to convert
            params: A dictionary mapping parameters to either single binding values or lists of
                binding values. The dictionary can also contain pairs of ParameterVectors with
                lists of parameters or lists of lists of parameters to bind to them.

        Returns:
            The converted Operator with CircuitStateFns replaced by DictStateFns or VectorStateFns.
        """
        if self._last_op is None or id(operator) != id(self._last_op):
            # Clear caches
            self._last_op = operator
            self._reduced_op_cache = None
            self._circuit_ops_cache = None
            self._transpiled_circ_cache = None
            self._transpile_before_bind = True

        if not self._reduced_op_cache:
            operator_dicts_replaced = operator.to_circuit_op()
            self._reduced_op_cache = operator_dicts_replaced.reduce()

        if not self._circuit_ops_cache:
            self._circuit_ops_cache = {}
            self._extract_circuitstatefns(self._reduced_op_cache)

        if params:
            num_parameterizations = len(list(params.values())[0])
            param_bindings = [{
                param: value_list[i]
                for (param, value_list) in params.items()
            } for i in range(num_parameterizations)]
        else:
            param_bindings = None
            num_parameterizations = 1

        # Don't pass circuits if we have in the cache, the sampling function knows to use the cache
        circs = list(self._circuit_ops_cache.values()
                     ) if not self._transpiled_circ_cache else None
        sampled_statefn_dicts = self.sample_circuits(
            circuit_sfns=circs, param_bindings=param_bindings)

        def replace_circuits_with_dicts(operator, param_index=0):
            if isinstance(operator, CircuitStateFn):
                return sampled_statefn_dicts[id(operator)][param_index]
            elif isinstance(operator, ListOp):
                return operator.traverse(
                    partial(replace_circuits_with_dicts,
                            param_index=param_index))
            else:
                return operator

        if params:
            return ListOp([
                replace_circuits_with_dicts(self._reduced_op_cache,
                                            param_index=i)
                for i in range(num_parameterizations)
            ])
        else:
            return replace_circuits_with_dicts(self._reduced_op_cache,
                                               param_index=0)

    def _extract_circuitstatefns(self, operator: OperatorBase) -> None:
        r"""
        Recursively extract the ``CircuitStateFns`` contained in operator into the
        ``_circuit_ops_cache`` field.
        """
        if isinstance(operator, CircuitStateFn):
            self._circuit_ops_cache[id(operator)] = operator
        elif isinstance(operator, ListOp):
            for op in operator.oplist:
                self._extract_circuitstatefns(op)

    def sample_circuits(
        self,
        circuit_sfns: Optional[List[CircuitStateFn]] = None,
        param_bindings: Optional[List[Dict[ParameterExpression,
                                           List[float]]]] = None
    ) -> Dict[int, Union[StateFn, List[StateFn]]]:
        r"""
        Samples the CircuitStateFns and returns a dict associating their ``id()`` values to their
        replacement DictStateFn or VectorStateFn. If param_bindings is provided,
        the CircuitStateFns are broken into their parameterizations, and a list of StateFns is
        returned in the dict for each circuit ``id()``. Note that param_bindings is provided here
        in a different format than in ``convert``, and lists of parameters within the dict is not
        supported, and only binding dicts which are valid to be passed into Terra can be included
        in this list.

        Args:
            circuit_sfns: The list of CircuitStateFns to sample.
            param_bindings: The parameterizations to bind to each CircuitStateFn.

        Returns:
            The dictionary mapping ids of the CircuitStateFns to their replacement StateFns.
        """
        if circuit_sfns or not self._transpiled_circ_cache:
            if self._statevector:
                circuits = [
                    op_c.to_circuit(meas=False) for op_c in circuit_sfns
                ]
            else:
                circuits = [
                    op_c.to_circuit(meas=True) for op_c in circuit_sfns
                ]

            try:
                self._transpiled_circ_cache = self.quantum_instance.transpile(
                    circuits)
            except QiskitError:
                logger.debug(
                    r'CircuitSampler failed to transpile circuits with unbound '
                    r'parameters. Attempting to transpile only when circuits are bound '
                    r'now, but this can hurt performance due to repeated transpilation.'
                )
                self._transpile_before_bind = False
                self._transpiled_circ_cache = circuits
        else:
            circuit_sfns = list(self._circuit_ops_cache.values())

        if param_bindings is not None:
            if self._param_qobj:
                ready_circs = self._transpiled_circ_cache
                self._prepare_parameterized_run_config(param_bindings)
            else:
                ready_circs = [
                    circ.assign_parameters(binding)
                    for circ in self._transpiled_circ_cache
                    for binding in param_bindings
                ]
        else:
            ready_circs = self._transpiled_circ_cache

        results = self.quantum_instance.execute(
            ready_circs, had_transpiled=self._transpile_before_bind)

        # Wipe parameterizations, if any
        # self.quantum_instance._run_config.parameterizations = None

        sampled_statefn_dicts = {}
        for i, op_c in enumerate(circuit_sfns):
            # Taking square root because we're replacing a statevector
            # representation of probabilities.
            reps = len(param_bindings) if param_bindings is not None else 1
            c_statefns = []
            for j in range(reps):
                circ_index = (i * reps) + j
                circ_results = results.data(circ_index)

                if 'expval_measurement' in circ_results.get(
                        'snapshots', {}).get('expectation_value', {}):
                    snapshot_data = results.data(circ_index)['snapshots']
                    avg = snapshot_data['expectation_value'][
                        'expval_measurement'][0]['value']
                    if isinstance(avg, (list, tuple)):
                        # Aer versions before 0.4 use a list snapshot format
                        # which must be converted to a complex value.
                        avg = avg[0] + 1j * avg[1]
                    # Will be replaced with just avg when eval is called later
                    num_qubits = circuit_sfns[0].num_qubits
                    result_sfn = (Zero ^ num_qubits).adjoint() * avg
                elif self._statevector:
                    result_sfn = StateFn(op_c.coeff *
                                         results.get_statevector(circ_index))
                else:
                    shots = self.quantum_instance._run_config.shots
                    result_sfn = StateFn({
                        b: (v * op_c.coeff / shots)**.5
                        for (b, v) in results.get_counts(circ_index).items()
                    })
                if self._attach_results:
                    result_sfn.execution_results = circ_results
                c_statefns.append(result_sfn)
            sampled_statefn_dicts[id(op_c)] = c_statefns
        return sampled_statefn_dicts

    # TODO build Aer re-parameterized Qobj.
    def _prepare_parameterized_run_config(self, param_bindings: dict) -> None:
        raise NotImplementedError
def run_BO_vqe_parallel(
    phys_params,
    get_qubit_op,
    ansatz,
    info_sharing_mode='shared',
    nb_iter=30,
    nb_init='max',
    init_jobs=1,
    nb_shots=1024,
    backend_name='qasm_simulator',
    initial_layout=None,
    optimization_level=3,
    verbose=False,
    dump_results=False,
):
    """ 
    Run the BO VQE algorithm, parallelised over different physical parameter values. It 
    has different information sharing modes, documented below under `info_sharing_mode`

    Parameters
    ----------
    phys_params : array
        The set of physical parameters to run the BO VQE for. In the chemistry case this
        is the nuclear separations, for TFIM it is the set of B values
    get_qubit_op : callable, (float) -> (WeightedPauliOperator,float)
        Generates the qubit Hamiltonian as well as any energy shift that is missing from the
        qubit Hamiltonian (could be added back as a identity Pauli string). The chemistry
        functions above, e.g. `get_H2_qubit_op` would be an example
    ansatz : ParameterisedAnsatz object
        The variational ansatz object, an instance of a subclass of ParameterisedAnsatz 
    
    info_sharing_mode : {'independent','shared','random','left','right'}
        (BO) This controls the evaluation sharing of the BO instances, cases:
            'independent' : The BO do not share data, each only recieves its own evaluations
            'shared' :  Each BO obj gains access to evaluations of all of the others. 
            'random1' : The BO do not get the evaluations others have requested, but in 
                addition to their own they get an equivalent number of randomly chosen 
                parameter points 
            'random2' : The BO do not get the evaluations others have requested, but in 
                addition to their own they get an equivalent number of randomly chosen 
                parameter points. These points are not chosen fully at random, but instead 
                if x1 and x2 are BO[1] and BO[2]'s chosen evaluations respectively then BO[1] 
                get an additional point y2 that is |x2-x1| away from x1 but in a random 
                direction, similar for BO[2], etc.
            'left', 'right' : Implement information sharing but in a directional way, so 
                that (using 'left' as an example) BO[1] gets its evaluation as well as 
                BO[0]; BO[2] gets its point as well as BO[1] and BO[0], etc. To ensure all 
                BO's get an equal number of evaluations this is padded with random points. 
                These points are not chosen fully at random, they are chosen in the same way
                as 'random2' described above.
    nb_iter : int, default 30
        (BO) Sets the number of iteration rounds of the BO
    nb_init : int or keyword 'max', default 'max'
        (BO) Sets the number of initial data points to feed into the BO before starting
        iteration rounds. If set to 'max' it will generate the maximum number of initial
        points such that it submits `init_jobs` worth of circuits to a qiskit backend.
    init_jobs : int, default 1
        (BO) The number of qiskit jobs to use to generate initial data. (Most real device
        backends accept up to 900 circuits in one job.)

    nb_shots : int, default 1024
        (Qiskit) Number of measurements shots when executing circuits
    backend_name : name of qiskit backend, defaults to 'qasm_simulator'
        (Qiskit) Refers to either a IBMQ provider or Aer simulator backend
    initial_layout : int array, optional
        (Qiskit) Passed to a `QuantumInstance` obj, used in transpiling
    optimization_level : int, default 3
        (Qiskit) Passed to a `QuantumInstance` obj, used in transpiling

    verbose : bool, optional
        Set level of output of the function
    dump_results : bool, default False
        Flag for whether or not to dump the accumulated results objs

    Returns
    -------
    exact_energies : array, size=len(phys_params)
        True energies of the molecular ground state at each distance
    BO_energies : array, size=len(phys_params)
        BO VQE estimatated energies of the molecular ground state at each distance
    BO_energies_std : array, size=len(phys_params)
        The std on the final estimate of BO_energies. This comes from the std of the 
        final circuit evaluation at the BO optimal, it does not include uncertainties
        due to the BO's lack of confidence about its optimal
    optimal_params : array, shape=(len(phys_params),nb_params)
        Optimal parameter sets found for each separation
    accumulated_results : list
        If `dump_results` is set to True this will return the accumulated results dicts
        from the BO, else it will be an empty list
    """

    # to save results sets
    accumulated_results = []

    # check the information sharing arg is recognised
    if not info_sharing_mode in [
            'independent', 'shared', 'random1', 'random2', 'left', 'right'
    ]:
        print(
            'BO information sharing mode ' + f'{info_sharing_mode}' +
            ' not recognised, please choose: ' +
            '"independent", "shared", "random1", "random2", "left" or "right".',
            file=sys.stderr)
        raise ValueError

    # make qubit ops
    if verbose: print('making qubit ops...')
    qubit_ops, exact_energies, shifts = _make_qubit_ops(
        phys_params, get_qubit_op)
    # group pauli operators for measurement
    qubit_ops = [
        TPBGroupedWeightedPauliOperator.unsorted_grouping(op)
        for op in qubit_ops
    ]

    # ensure the ansatz and qubit Hamiltonians have same number of qubits
    assert qubit_ops[0].num_qubits == ansatz.num_qubits

    # create quantum instances
    if (backend_name[:4] == 'ibmq'):
        _is_device = True

        from qiskit import IBMQ
        IBMQ.load_account()
        provider = IBMQ.get_provider(group='samsung')
        backend = provider.get_backend(backend_name)
    else:
        _is_device = False
        backend = Aer.get_backend(backend_name)
    inst_fewshots = QuantumInstance(
        backend,
        shots=nb_shots,
        optimization_level=optimization_level,
        initial_layout=initial_layout,
        skip_qobj_validation=(not _is_device),
    )
    inst_bigshots = QuantumInstance(
        backend,
        shots=8192,
        optimization_level=optimization_level,
        initial_layout=initial_layout,
        skip_qobj_validation=(not _is_device),
    )

    # generate and transpile measurement circuits, using first of qubit_ops
    measurement_circuits = qubit_ops[0].construct_evaluation_circuit(
        wave_function=ansatz.circuit,
        statevector_mode=inst_fewshots.is_statevector)
    t_measurement_circuits = inst_fewshots.transpile(measurement_circuits)

    def obtain_results(params_values,
                       quantum_instance,
                       circuit_name_prefixes=None):
        """
        Function to wrap executions on the quantum backend, binds parameter values
        and executes.
        (Optionally `circuit_name_prefixes` arg sets the prefix names of the eval
        circuits, else they are numbered sequentially. If the size of the name array
        is different from the size of `params_values` it will crash.)

        Gets `ansatz` and `t_measurement_circuits` from the surrounding scope. 
        `quantum_instance` is an arg because we switch between high and low shot 
        number instances.
        """
        if np.ndim(params_values) == 1:
            params_values = [params_values]

        if circuit_name_prefixes is not None:
            assert len(circuit_name_prefixes) == params_values.shape[0]

        # package and bind circuits
        bound_circs = []
        for pidx, p in enumerate(params_values):
            for cc in t_measurement_circuits:
                tmp = cc.bind_parameters(dict(zip(ansatz.params, p)))
                if circuit_name_prefixes is not None:
                    prefix = circuit_name_prefixes[pidx]
                else:
                    prefix = str(pidx)
                tmp.name = prefix + tmp.name
                bound_circs.append(tmp)

        # See if one can add a noise model here and the number of parameters
        result = quantum_instance.execute(bound_circs, had_transpiled=True)

        if dump_results:
            accumulated_results.append(
                [params_values.tolist(),
                 result.to_dict()])

        return result

    # ===================
    # run BO Optim
    # ===================

    # setup
    DOMAIN_FULL = [(0, 2 * np.pi) for i in range(ansatz.nb_params)]
    DOMAIN_BO = [{
        'name': str(i),
        'type': 'continuous',
        'domain': d
    } for i, d in enumerate(DOMAIN_FULL)]
    bo_args = ut.gen_default_argsbo()
    bo_args.update({'domain': DOMAIN_BO, 'initial_design_numdata': 0})

    # get initial values using bigshots
    if nb_init == 'max':
        _nb_init = init_jobs * 900 // len(t_measurement_circuits)
    else:
        _nb_init = nb_init
    if verbose: print('getting initialisation data...')
    Xinit = 2 * np.pi * np.random.random(_nb_init * ansatz.nb_params).reshape(
        (_nb_init, ansatz.nb_params))
    init_results = obtain_results(Xinit, inst_bigshots)

    # initialise BO obj for each distance
    Bopts = []
    _update_weights = []
    if verbose: print('initialising BO objects...')
    for qo in qubit_ops:
        # evaluate this distance's specific weighted qubit operators using the results
        Yinit = np.array([[
            np.real(
                qo.evaluate_with_result(
                    init_results,
                    statevector_mode=inst_bigshots.is_statevector,
                    circuit_name_prefix=str(i))[0])
            for i in range(Xinit.shape[0])
        ]]).T
        tmp = GPyOpt.methods.BayesianOptimization(
            lambda x: None,  # blank cost function
            X=Xinit,
            Y=Yinit,
            **bo_args)
        tmp.run_optimization(max_iter=0, eps=0)  # (may not be needed)
        Bopts.append(tmp)

        # This block is only to ensure the linear decrease of the exploration
        if (getattr(tmp, '_dynamic_weights') == 'linear'):
            _update_weights.append(True)

            def dynamics_weight(n):
                return max(0.000001,
                           bo_args['acquisition_weight'] * (1 - n / nb_iter))
        else:
            _update_weights.append(False)

    # main loop
    for iter_idx in range(nb_iter):

        # for the last 5 shots use large number of measurements
        inst = inst_fewshots
        _msg = 'at optimisation round ' + f'{iter_idx}'
        if nb_iter - iter_idx < 6:
            inst = inst_bigshots
            _msg += ' (using big shot number)'
        if verbose: print(_msg)

        # query each BO obj where it would like an evaluation, and pool them all togther
        circuit_name_prefixes = []
        circ_name_to_x_map = {}
        for idx, bo in enumerate(Bopts):
            bo._update_model(bo.normalization_type)
            if (_update_weights[idx]):
                bo.acquisition.exploration_weight = dynamics_weight(iter_idx)
            x = bo._compute_next_evaluations()
            if idx == 0:
                Xnew = x
            else:
                Xnew = np.vstack((Xnew, x))

            _circ_name = str(idx) + '-base'
            circuit_name_prefixes.append(_circ_name)
            circ_name_to_x_map[_circ_name] = x[0]

            # to implement 'random1' we pad out each with completely random evaluations
            if info_sharing_mode == 'random1':
                nb_extra_points = len(phys_params) - 1
                extra_points = 2 * np.pi * np.random.random(
                    nb_extra_points * ansatz.nb_params).reshape(
                        (nb_extra_points, ansatz.nb_params))
                Xnew = np.vstack((Xnew, extra_points))

                _circ_names = [
                    str(idx) + '-' + str(i) for i in range(len(phys_params))
                    if not i == idx
                ]
                circuit_name_prefixes += _circ_names
                circ_name_to_x_map.update(dict(zip(_circ_names, extra_points)))

        # to implement 'random2','left','right' strategies we have to wait until all primary
        # evaluations have been processed
        tmp = copy.deepcopy(Xnew)
        if info_sharing_mode in ['random2', 'left', 'right']:
            for boidx, bo in enumerate(Bopts):
                bo_eval_point = tmp[boidx]

                for pidx, p in enumerate(tmp):
                    if (((info_sharing_mode == 'random2') and
                         (not boidx == pidx)) or
                        ((info_sharing_mode == 'left') and (boidx < pidx)) or
                        ((info_sharing_mode == 'right') and (boidx > pidx))):
                        # distance is euclidean distance, but since the values are angles we want
                        # to minimize the element-wise differences by optionally shifting one of
                        # the points by ±2\pi
                        disp_vector = np.minimum(
                            (bo_eval_point - p)**2,
                            ((bo_eval_point + 2 * np.pi) - p)**2)
                        disp_vector = np.minimum(
                            disp_vector, ((bo_eval_point - 2 * np.pi) - p)**2)
                        dist = np.sqrt(np.sum(disp_vector))
                        # generate random vector in N-d space then scale it to have length we want,
                        # using 'Hypersphere Point Picking' Gaussian approach
                        random_displacement = np.random.normal(
                            size=ansatz.nb_params)
                        random_displacement = random_displacement * dist / np.sqrt(
                            np.sum(random_displacement**2))
                        Xnew = np.vstack(
                            (Xnew, bo_eval_point + random_displacement))

                        _circ_name = str(boidx) + '-' + str(pidx)
                        circuit_name_prefixes.append(_circ_name)
                        circ_name_to_x_map[
                            _circ_name] = bo_eval_point + random_displacement

        # sense check on number of circuits generated
        if info_sharing_mode in ['independent', 'shared']:
            assert len(circuit_name_prefixes) == len(phys_params)
        elif info_sharing_mode in ['random1', 'random2']:
            assert len(circuit_name_prefixes) == len(phys_params)**2
        elif info_sharing_mode in ['left', 'right']:
            assert len(circuit_name_prefixes) == len(phys_params) * (
                len(phys_params) + 1) // 2

        # get results object at new param values
        new_results = obtain_results(
            Xnew, inst, circuit_name_prefixes=circuit_name_prefixes)

        # iterate over the BO obj's passing them all the correctly weighted data
        for idx, (bo, qo) in enumerate(zip(Bopts, qubit_ops)):

            if info_sharing_mode == 'independent':
                _pull_from = [str(idx) + '-base']
            elif info_sharing_mode == 'shared':
                _pull_from = [
                    str(i) + '-base' for i in range(len(phys_params))
                ]
            elif info_sharing_mode in ['random1', 'random2']:
                _pull_from = ([str(idx) + '-base'] + [
                    str(idx) + '-' + str(i)
                    for i in range(len(phys_params)) if not i == idx
                ])
            elif info_sharing_mode == 'left':
                _pull_from = ([str(i) + '-base' for i in range(idx + 1)] + [
                    str(idx) + '-' + str(i)
                    for i in range(idx + 1, len(phys_params))
                ])
            elif info_sharing_mode == 'right':
                _pull_from = (
                    [str(i) + '-base' for i in range(idx, len(phys_params))] +
                    [str(idx) + '-' + str(i) for i in range(idx)])

            if not info_sharing_mode == 'independent':
                assert len(_pull_from) == len(phys_params)  # sense check

            Ynew = np.array([[
                np.real(
                    qo.evaluate_with_result(
                        new_results,
                        statevector_mode=inst_bigshots.is_statevector,
                        circuit_name_prefix=i)[0]) for i in _pull_from
            ]]).T
            Xnew = np.array([circ_name_to_x_map[i] for i in _pull_from])

            bo.X = np.vstack((bo.X, Xnew))
            bo.Y = np.vstack((bo.Y, Ynew))

    #finalize (may not be needed)
    if verbose: print('finalising...')
    for bo in Bopts:
        bo.run_optimization(max_iter=0, eps=0)

    # Results found
    for idx, (dist, bo) in enumerate(zip(phys_params, Bopts)):
        (Xseen, Yseen), (Xexp, Yexp) = bo.get_best()
        if verbose:
            print("at distance " + f'{dist}')
            print("best seen: " + f'{Yseen}')
            print("best expected: " + f'{Yexp}')
            print(bo.model.model)
            bo.plot_convergence()
        if idx == 0:
            Xfinal = Xexp
        else:
            Xfinal = np.vstack([Xfinal, Xexp])

    # obtain final BO estimates from an evaluation of the optimal params
    final_results = obtain_results(Xfinal, inst_bigshots)
    BO_energies = np.zeros(len(phys_params))
    BO_energies_std = np.zeros(len(phys_params))
    for idx, qo in enumerate(qubit_ops):
        mean, std = qo.evaluate_with_result(
            final_results,
            statevector_mode=inst_bigshots.is_statevector,
            circuit_name_prefix=str(idx))
        BO_energies[idx] = np.real(mean)
        BO_energies_std[idx] = np.real(std)

    return exact_energies + shifts, BO_energies + shifts, BO_energies_std, Xfinal, accumulated_results
Exemple #3
0
class CircuitSampler(ConverterBase):
    """
    The CircuitSampler traverses an Operator and converts any CircuitStateFns into
    approximations of the state function by a DictStateFn or VectorStateFn using a quantum
    backend. Note that in order to approximate the value of the CircuitStateFn, it must 1) send
    state function through a depolarizing channel, which will destroy all phase information and
    2) replace the sampled frequencies with **square roots** of the frequency, rather than the raw
    probability of sampling (which would be the equivalent of sampling the **square** of the
    state function, per the Born rule.

    The CircuitSampler aggressively caches transpiled circuits to handle re-parameterization of
    the same circuit efficiently. If you are converting multiple different Operators,
    you are better off using a different CircuitSampler for each Operator to avoid cache thrashing.
    """
    def __init__(self,
                 backend: Union[Backend, BaseBackend, QuantumInstance],
                 statevector: Optional[bool] = None,
                 param_qobj: bool = False,
                 attach_results: bool = False) -> None:
        """
        Args:
            backend: The quantum backend or QuantumInstance to use to sample the circuits.
            statevector: If backend is a statevector backend, whether to replace the
                CircuitStateFns with DictStateFns (from the counts) or VectorStateFns (from the
                statevector). ``None`` will set this argument automatically based on the backend.
            attach_results: Whether to attach the data from the backend ``Results`` object for
                a given ``CircuitStateFn``` to an ``execution_results`` field added the converted
                ``DictStateFn`` or ``VectorStateFn``.
            param_qobj: Whether to use Aer's parameterized Qobj capability to avoid re-assembling
                the circuits.

        Raises:
            ValueError: Set statevector or param_qobj True when not supported by backend.
        """
        self._quantum_instance = backend if isinstance(backend, QuantumInstance) else\
            QuantumInstance(backend=backend)
        self._statevector = statevector if statevector is not None \
            else self.quantum_instance.is_statevector
        self._param_qobj = param_qobj
        self._attach_results = attach_results

        self._check_quantum_instance_and_modes_consistent()

        # Object state variables
        self._last_op = None
        self._reduced_op_cache = None
        self._circuit_ops_cache = {}  # type: Dict[int, CircuitStateFn]
        self._transpiled_circ_cache = None  # type: Optional[List[Any]]
        self._transpiled_circ_templates = None  # type: Optional[List[Any]]
        self._transpile_before_bind = True
        self._binding_mappings = None

    def _check_quantum_instance_and_modes_consistent(self) -> None:
        """ Checks whether the statevector and param_qobj settings are compatible with the
        backend

        Raises:
            ValueError: statevector or param_qobj are True when not supported by backend.
        """
        if self._statevector and not is_statevector_backend(
                self.quantum_instance.backend):
            raise ValueError(
                'Statevector mode for circuit sampling requires statevector '
                'backend, not {}.'.format(self.quantum_instance.backend))

        if self._param_qobj and not is_aer_provider(
                self.quantum_instance.backend):
            raise ValueError('Parameterized Qobj mode requires Aer '
                             'backend, not {}.'.format(
                                 self.quantum_instance.backend))

    @property
    def backend(self) -> Union[Backend, BaseBackend]:
        """ Returns the backend.

        Returns:
             The backend used by the CircuitSampler
        """
        return self.quantum_instance.backend

    @backend.setter
    def backend(self, backend: Union[Backend, BaseBackend]):
        """ Sets backend without additional configuration. """
        self.set_backend(backend)

    def set_backend(self, backend: Union[Backend, BaseBackend],
                    **kwargs) -> None:
        """ Sets backend with configuration.

        Raises:
            ValueError: statevector or param_qobj are True when not supported by backend.
        """
        self.quantum_instance = QuantumInstance(backend)
        self.quantum_instance.set_config(**kwargs)

    @property
    def quantum_instance(self) -> QuantumInstance:
        """ Returns the quantum instance.

        Returns:
             The QuantumInstance used by the CircuitSampler
        """
        return self._quantum_instance

    @quantum_instance.setter
    def quantum_instance(
        self, quantum_instance: Union[QuantumInstance, Backend,
                                      BaseBackend]) -> None:
        """ Sets the QuantumInstance.

        Raises:
            ValueError: statevector or param_qobj are True when not supported by backend.
        """
        if isinstance(quantum_instance, (Backend, BaseBackend)):
            quantum_instance = QuantumInstance(quantum_instance)
        self._quantum_instance = quantum_instance
        self._check_quantum_instance_and_modes_consistent()

    # pylint: disable=arguments-differ
    def convert(
        self,
        operator: OperatorBase,
        params: Optional[Dict[Parameter, Union[float, List[float],
                                               List[List[float]]]]] = None
    ) -> OperatorBase:
        r"""
        Converts the Operator to one in which the CircuitStateFns are replaced by
        DictStateFns or VectorStateFns. Extracts the CircuitStateFns out of the Operator,
        caches them, calls ``sample_circuits`` below to get their converted replacements,
        and replaces the CircuitStateFns in operator with the replacement StateFns.

        Args:
            operator: The Operator to convert
            params: A dictionary mapping parameters to either single binding values or lists of
                binding values.

        Returns:
            The converted Operator with CircuitStateFns replaced by DictStateFns or VectorStateFns.
        Raises:
            AquaError: if extracted circuits are empty.
        """
        if self._last_op is None or id(operator) != id(self._last_op):
            # Clear caches
            self._last_op = operator
            self._reduced_op_cache = None
            self._circuit_ops_cache = None
            self._transpiled_circ_cache = None
            self._transpile_before_bind = True

        if not self._reduced_op_cache:
            operator_dicts_replaced = operator.to_circuit_op()
            self._reduced_op_cache = operator_dicts_replaced.reduce()

        if not self._circuit_ops_cache:
            self._circuit_ops_cache = {}
            self._extract_circuitstatefns(self._reduced_op_cache)
            if not self._circuit_ops_cache:
                raise AquaError(
                    'Circuits are empty. '
                    'Check that the operator is an instance of CircuitStateFn or its ListOp.'
                )

        if params is not None and len(params.keys()) > 0:
            p_0 = list(params.values())[0]  # type: ignore
            if isinstance(p_0, (list, np.ndarray)):
                num_parameterizations = len(cast(List, p_0))
                param_bindings = [
                    {
                        param: value_list[i]  # type: ignore
                        for (param, value_list) in params.items()
                    } for i in range(num_parameterizations)
                ]
            else:
                num_parameterizations = 1
                param_bindings = [params]  # type: ignore

        else:
            param_bindings = None
            num_parameterizations = 1

        # Don't pass circuits if we have in the cache, the sampling function knows to use the cache
        circs = list(self._circuit_ops_cache.values()
                     ) if not self._transpiled_circ_cache else None
        p_b = cast(List[Dict[Parameter, float]], param_bindings)
        sampled_statefn_dicts = self.sample_circuits(circuit_sfns=circs,
                                                     param_bindings=p_b)

        def replace_circuits_with_dicts(operator, param_index=0):
            if isinstance(operator, CircuitStateFn):
                return sampled_statefn_dicts[id(operator)][param_index]
            elif isinstance(operator, ListOp):
                return operator.traverse(
                    partial(replace_circuits_with_dicts,
                            param_index=param_index))
            else:
                return operator

        if params:
            return ListOp([
                replace_circuits_with_dicts(self._reduced_op_cache,
                                            param_index=i)
                for i in range(num_parameterizations)
            ])
        else:
            return replace_circuits_with_dicts(self._reduced_op_cache,
                                               param_index=0)

    def _extract_circuitstatefns(self, operator: OperatorBase) -> None:
        r"""
        Recursively extract the ``CircuitStateFns`` contained in operator into the
        ``_circuit_ops_cache`` field.
        """
        if isinstance(operator, CircuitStateFn):
            self._circuit_ops_cache[id(operator)] = operator
        elif isinstance(operator, ListOp):
            for op in operator.oplist:
                self._extract_circuitstatefns(op)

    def sample_circuits(
        self,
        circuit_sfns: Optional[List[CircuitStateFn]] = None,
        param_bindings: Optional[List[Dict[Parameter, float]]] = None
    ) -> Dict[int, Union[StateFn, List[StateFn]]]:
        r"""
        Samples the CircuitStateFns and returns a dict associating their ``id()`` values to their
        replacement DictStateFn or VectorStateFn. If param_bindings is provided,
        the CircuitStateFns are broken into their parameterizations, and a list of StateFns is
        returned in the dict for each circuit ``id()``. Note that param_bindings is provided here
        in a different format than in ``convert``, and lists of parameters within the dict is not
        supported, and only binding dicts which are valid to be passed into Terra can be included
        in this list.

        Args:
            circuit_sfns: The list of CircuitStateFns to sample.
            param_bindings: The parameterizations to bind to each CircuitStateFn.

        Returns:
            The dictionary mapping ids of the CircuitStateFns to their replacement StateFns.
        Raises:
            AquaError: if extracted circuits are empty.
        """
        if not circuit_sfns and not self._transpiled_circ_cache:
            raise AquaError('CircuitStateFn is empty and there is no cache.')

        if circuit_sfns:
            self._transpiled_circ_templates = None
            if self._statevector:
                circuits = [
                    op_c.to_circuit(meas=False) for op_c in circuit_sfns
                ]
            else:
                circuits = [
                    op_c.to_circuit(meas=True) for op_c in circuit_sfns
                ]

            try:
                self._transpiled_circ_cache = self.quantum_instance.transpile(
                    circuits)
            except QiskitError:
                logger.debug(
                    r'CircuitSampler failed to transpile circuits with unbound '
                    r'parameters. Attempting to transpile only when circuits are bound '
                    r'now, but this can hurt performance due to repeated transpilation.'
                )
                self._transpile_before_bind = False
                self._transpiled_circ_cache = circuits
        else:
            circuit_sfns = list(self._circuit_ops_cache.values())

        if param_bindings is not None:
            if self._param_qobj:
                start_time = time()
                ready_circs = self._prepare_parameterized_run_config(
                    param_bindings)
                end_time = time()
                logger.debug('Parameter conversion %.5f (ms)',
                             (end_time - start_time) * 1000)
            else:
                start_time = time()
                ready_circs = [
                    circ.assign_parameters(_filter_params(circ, binding))
                    for circ in self._transpiled_circ_cache
                    for binding in param_bindings
                ]
                end_time = time()
                logger.debug('Parameter binding %.5f (ms)',
                             (end_time - start_time) * 1000)
        else:
            ready_circs = self._transpiled_circ_cache

        results = self.quantum_instance.execute(
            ready_circs, had_transpiled=self._transpile_before_bind)

        if param_bindings is not None and self._param_qobj:
            self._clean_parameterized_run_config()

        # Wipe parameterizations, if any
        # self.quantum_instance._run_config.parameterizations = None

        sampled_statefn_dicts = {}
        for i, op_c in enumerate(circuit_sfns):
            # Taking square root because we're replacing a statevector
            # representation of probabilities.
            reps = len(param_bindings) if param_bindings is not None else 1
            c_statefns = []
            for j in range(reps):
                circ_index = (i * reps) + j
                circ_results = results.data(circ_index)

                if 'expval_measurement' in circ_results.get(
                        'snapshots', {}).get('expectation_value', {}):
                    snapshot_data = results.data(circ_index)['snapshots']
                    avg = snapshot_data['expectation_value'][
                        'expval_measurement'][0]['value']
                    if isinstance(avg, (list, tuple)):
                        # Aer versions before 0.4 use a list snapshot format
                        # which must be converted to a complex value.
                        avg = avg[0] + 1j * avg[1]
                    # Will be replaced with just avg when eval is called later
                    num_qubits = circuit_sfns[0].num_qubits
                    result_sfn = DictStateFn(
                        '0' * num_qubits,
                        is_measurement=op_c.is_measurement) * avg
                elif self._statevector:
                    result_sfn = StateFn(op_c.coeff *
                                         results.get_statevector(circ_index),
                                         is_measurement=op_c.is_measurement)
                else:
                    shots = self.quantum_instance._run_config.shots
                    result_sfn = StateFn(
                        {
                            b: (v / shots)**0.5 * op_c.coeff
                            for (b,
                                 v) in results.get_counts(circ_index).items()
                        },
                        is_measurement=op_c.is_measurement)
                if self._attach_results:
                    result_sfn.execution_results = circ_results
                c_statefns.append(result_sfn)
            sampled_statefn_dicts[id(op_c)] = c_statefns
        return sampled_statefn_dicts

    def _build_aer_params(self, circuit: QuantumCircuit,
                          building_param_tables: Dict[Tuple[int, int],
                                                      List[float]],
                          input_params: Dict[Parameter, float]) -> None:
        def resolve_param(inst_param):
            if not isinstance(inst_param, ParameterExpression):
                return None
            param_mappings = {}
            for param in inst_param._parameter_symbols.keys():
                if param not in input_params:
                    raise ValueError('unexpected parameter: {0}'.format(param))
                param_mappings[param] = input_params[param]
            return float(inst_param.bind(param_mappings))

        gate_index = 0
        for inst, _, _ in circuit.data:
            param_index = 0
            for inst_param in inst.params:
                val = resolve_param(inst_param)
                if val is not None:
                    param_key = (gate_index, param_index)
                    if param_key in building_param_tables:
                        building_param_tables[param_key].append(val)
                    else:
                        building_param_tables[param_key] = [val]
                param_index += 1
            gate_index += 1

    def _prepare_parameterized_run_config(
            self, param_bindings: List[Dict[Parameter, float]]) -> List[Any]:

        self.quantum_instance._run_config.parameterizations = []

        if self._transpiled_circ_templates is None \
                or len(self._transpiled_circ_templates) != len(self._transpiled_circ_cache):

            # temporally resolve parameters of self._transpiled_circ_cache
            # They will be overridden in Aer from the next iterations
            self._transpiled_circ_templates = [
                circ.assign_parameters(_filter_params(circ, param_bindings[0]))
                for circ in self._transpiled_circ_cache
            ]

        for circ in self._transpiled_circ_cache:
            building_param_tables = {
            }  # type: Dict[Tuple[int, int], List[float]]
            for param_binding in param_bindings:
                self._build_aer_params(circ, building_param_tables,
                                       param_binding)
            param_tables = []
            for gate_and_param_indices in building_param_tables:
                gate_index = gate_and_param_indices[0]
                param_index = gate_and_param_indices[1]
                param_tables.append([[gate_index, param_index],
                                     building_param_tables[(gate_index,
                                                            param_index)]])
            self.quantum_instance._run_config.parameterizations.append(
                param_tables)

        return self._transpiled_circ_templates

    def _clean_parameterized_run_config(self) -> None:
        self.quantum_instance._run_config.parameterizations = []