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
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 = []