def test_results_by_qubit_groups(): er1 = ExperimentResult( setting=ExperimentSetting(plusX(0), sZ(0)), expectation=0.0, std_err=0.0, total_counts=1, ) er2 = ExperimentResult( setting=ExperimentSetting(plusX(0), sZ(1)), expectation=0.0, std_err=0.0, total_counts=1, ) er3 = ExperimentResult( setting=ExperimentSetting(plusX(0), sX(0) * sZ(1)), expectation=0.0, std_err=0.0, total_counts=1, ) er4 = ExperimentResult( setting=ExperimentSetting(plusX(0), sX(0) * sZ(2)), expectation=0.0, std_err=0.0, total_counts=1, ) groups = [(0, ), (1, ), (2, 0)] res_by_group = get_results_by_qubit_groups([er1, er2, er3, er4], groups) assert res_by_group == {(0, ): [er1], (1, ): [er2], (0, 2): [er1, er4]}
def test_group_experiments_greedy(): ungrouped_tomo_expt = Experiment( [ [ ExperimentSetting( _pauli_to_product_state( PauliTerm.from_compact_str("(1+0j)*Z7Y8Z1Y4Z2Y5Y0X6")), PauliTerm.from_compact_str("(1+0j)*Z4X8Y5X3Y7Y1"), ) ], [ExperimentSetting(plusZ(7), sY(1))], ], program=Program(H(0), H(1), H(2)), ) grouped_tomo_expt = group_settings(ungrouped_tomo_expt, method="greedy") expected_grouped_tomo_expt = Experiment( [[ ExperimentSetting( TensorProductState.from_str( "Z0_7 * Y0_8 * Z0_1 * Y0_4 * Z0_2 * Y0_5 * Y0_0 * X0_6"), PauliTerm.from_compact_str("(1+0j)*Z4X8Y5X3Y7Y1"), ), ExperimentSetting(plusZ(7), sY(1)), ]], program=Program(H(0), H(1), H(2)), ) assert grouped_tomo_expt == expected_grouped_tomo_expt
def test_setting_no_in_back_compat(): out_ops = _generate_random_paulis(n_qubits=4, n_terms=7) for oop in out_ops: expt = ExperimentSetting(TensorProductState(), oop) expt2 = ExperimentSetting.from_str(str(expt)) assert expt == expt2 assert expt2.in_operator == sI() assert expt2.out_operator == oop
def test_setting_no_in(): out_ops = _generate_random_paulis(n_qubits=4, n_terms=7) for oop in out_ops: expt = ExperimentSetting(zeros_state(oop.get_qubits()), oop) expt2 = ExperimentSetting.from_str(str(expt)) assert expt == expt2 assert expt2.in_operator == functools.reduce(mul, [sZ(q) for q in oop.get_qubits()], sI()) assert expt2.out_operator == oop
def test_experiment_setting(): in_states = _generate_random_states(n_qubits=4, n_terms=7) out_ops = _generate_random_paulis(n_qubits=4, n_terms=7) for ist, oop in zip(in_states, out_ops): expt = ExperimentSetting(ist, oop) assert str(expt) == expt.serializable() expt2 = ExperimentSetting.from_str(str(expt)) assert expt == expt2 assert expt2.in_state == ist assert expt2.out_operator == oop
def test_max_tpb_overlap_3(): # add another ExperimentSetting to the above expt_setting = ExperimentSetting( _pauli_to_product_state( PauliTerm.from_compact_str("(1+0j)*Z7Y8Z1Y4Z2Y5Y0X6")), PauliTerm.from_compact_str("(1+0j)*Z4X8Y5X3Y7Y1"), ) expt_setting2 = ExperimentSetting(plusZ(7), sY(1)) p = Program(H(0), H(1), H(2)) tomo_expt2 = Experiment([expt_setting, expt_setting2], p) expected_dict2 = {expt_setting: [expt_setting, expt_setting2]} assert expected_dict2 == _max_tpb_overlap(tomo_expt2)
def test_merge_disjoint_experiments(): sett1 = ExperimentSetting(TensorProductState(), sX(0) * sY(1)) sett2 = ExperimentSetting(plusZ(1), sY(1)) sett3 = ExperimentSetting(plusZ(0), sX(0)) sett4 = ExperimentSetting(minusX(1), sY(1)) sett5 = ExperimentSetting(TensorProductState(), sZ(2)) expt1 = Experiment(settings=[sett1, sett2], program=Program(X(1))) expt2 = Experiment(settings=[sett3, sett4], program=Program(Z(0))) expt3 = Experiment(settings=[sett5], program=Program()) merged_expt = merge_disjoint_experiments([expt1, expt2, expt3]) assert len(merged_expt) == 2
def test_group_experiments(grouping_method): expts = [ # cf above, I removed the inner nesting. Still grouped visually ExperimentSetting(TensorProductState(), sX(0) * sI(1)), ExperimentSetting(TensorProductState(), sI(0) * sX(1)), ExperimentSetting(TensorProductState(), sZ(0) * sI(1)), ExperimentSetting(TensorProductState(), sI(0) * sZ(1)), ] suite = Experiment(expts, Program()) grouped_suite = group_settings(suite, method=grouping_method) assert len(suite) == 4 assert len(grouped_suite) == 2
def _max_tpb_overlap( tomo_expt: Experiment, ) -> Dict[ExperimentSetting, List[ExperimentSetting]]: """ Given an input Experiment, provide a dictionary indicating which ExperimentSettings share a tensor product basis :param tomo_expt: Experiment, from which to group ExperimentSettings that share a tpb and can be run together :return: dictionary keyed with ExperimentSetting (specifying a tpb), and with each value being a list of ExperimentSettings (diagonal in that tpb) """ # initialize empty dictionary diagonal_sets: Dict[ExperimentSetting, List[ExperimentSetting]] = {} # loop through ExperimentSettings of the Experiment for expt_setting in tomo_expt: # no need to group already grouped Experiment assert len(expt_setting) == 1, "already grouped?" unpacked_expt_setting = expt_setting[0] # calculate max overlap of expt_setting with keys of diagonal_sets # keep track of whether a shared tpb was found found_tpb = False # loop through dict items for es, es_list in diagonal_sets.items(): trial_es_list = es_list + [unpacked_expt_setting] diag_in_term = _max_weight_state(expst.in_state for expst in trial_es_list) diag_out_term = _max_weight_operator(expst.out_operator for expst in trial_es_list) # max_weight_xxx returns None if the set of xxx's don't share a TPB, so the following # conditional is True if expt_setting can be inserted into the current es_list. if diag_in_term is not None and diag_out_term is not None: found_tpb = True assert len(diag_in_term) >= len( es.in_state ), "Highest weight in-state can't be smaller than the given in-state" assert len(diag_out_term) >= len( es.out_operator ), "Highest weight out-PauliTerm can't be smaller than the given out-PauliTerm" # update the diagonalizing basis (key of dict) if necessary if len(diag_in_term) > len( es.in_state) or len(diag_out_term) > len( es.out_operator): del diagonal_sets[es] new_es = ExperimentSetting(diag_in_term, diag_out_term) diagonal_sets[new_es] = trial_es_list else: diagonal_sets[es] = trial_es_list break if not found_tpb: # made it through entire dict without finding any ExperimentSetting with shared tpb, # so need to make a new item diagonal_sets[unpacked_expt_setting] = [unpacked_expt_setting] return diagonal_sets
def test_max_tpb_overlap_2(): expt_setting = ExperimentSetting( _pauli_to_product_state( PauliTerm.from_compact_str("(1+0j)*Z7Y8Z1Y4Z2Y5Y0X6")), PauliTerm.from_compact_str("(1+0j)*Z4X8Y5X3Y7Y1"), ) p = Program(H(0), H(1), H(2)) tomo_expt = Experiment([expt_setting], p) expected_dict = {expt_setting: [expt_setting]} assert expected_dict == _max_tpb_overlap(tomo_expt)
def _operator_object_hook(obj: Mapping[str, Any]) -> Union[Mapping[str, Any], Experiment]: if "type" in obj and obj["type"] in ["Experiment", "TomographyExperiment"]: # I bet this doesn't work for grouped experiment settings settings = [[ExperimentSetting.from_str(s) for s in stt] for stt in obj["settings"]] p = Program(obj["program"]) p.wrap_in_numshots_loop(obj["shots"]) ex = Experiment(settings=settings, program=p, symmetrization=obj["symmetrization"]) ex.reset = obj["reset"] return ex return obj
def test_max_tpb_overlap_1(): tomo_expt_settings = [ ExperimentSetting(plusZ(1) * plusX(0), sY(2) * sY(1)), ExperimentSetting(plusX(2) * plusZ(1), sY(2) * sZ(0)), ] tomo_expt_program = Program(H(0), H(1), H(2)) tomo_expt = Experiment(tomo_expt_settings, tomo_expt_program) expected_dict = { ExperimentSetting( plusX(0) * plusZ(1) * plusX(2), sZ(0) * sY(1) * sY(2)): [ ExperimentSetting(plusZ(1) * plusX(0), sY(2) * sY(1)), ExperimentSetting(plusX(2) * plusZ(1), sY(2) * sZ(0)), ] } assert expected_dict == _max_tpb_overlap(tomo_expt)
def test_expt_settings_diagonal_in_tpb(): def _expt_settings_diagonal_in_tpb(es1: ExperimentSetting, es2: ExperimentSetting): """ Extends the concept of being diagonal in the same tpb to ExperimentSettings, by determining if the pairs of in_states and out_operators are separately diagonal in the same tpb """ max_weight_in = _max_weight_state([es1.in_state, es2.in_state]) max_weight_out = _max_weight_operator( [es1.out_operator, es2.out_operator]) return max_weight_in is not None and max_weight_out is not None expt_setting1 = ExperimentSetting(plusZ(1) * plusX(0), sY(1) * sZ(0)) expt_setting2 = ExperimentSetting(plusY(2) * plusZ(1), sZ(2) * sY(1)) assert _expt_settings_diagonal_in_tpb(expt_setting1, expt_setting2) expt_setting3 = ExperimentSetting(plusX(2) * plusZ(1), sZ(2) * sY(1)) expt_setting4 = ExperimentSetting(plusY(2) * plusZ(1), sX(2) * sY(1)) assert not _expt_settings_diagonal_in_tpb(expt_setting2, expt_setting3) assert not _expt_settings_diagonal_in_tpb(expt_setting2, expt_setting4)
def test_expt_settings_share_ntpb(): expts = [ [ ExperimentSetting(zeros_state([0, 1]), sX(0) * sI(1)), ExperimentSetting(zeros_state([0, 1]), sI(0) * sX(1)), ], [ ExperimentSetting(zeros_state([0, 1]), sZ(0) * sI(1)), ExperimentSetting(zeros_state([0, 1]), sI(0) * sZ(1)), ], ] for group in expts: for e1, e2 in itertools.combinations(group, 2): assert _max_weight_state([e1.in_state, e2.in_state]) is not None assert _max_weight_operator([e1.out_operator, e2.out_operator]) is not None
def _operator_object_hook(obj): if 'type' in obj and obj['type'] == 'TomographyExperiment': # I bet this doesn't work for grouped experiment settings settings = [[ExperimentSetting.from_str(s) for s in stt] for stt in obj['settings']] p = Program(obj['program']) p.wrap_in_numshots_loop(obj['shots']) ex = TomographyExperiment(settings=settings, program=p, symmetrization=obj['symmetrization']) ex.reset = obj['reset'] return ex return obj
def generate_calibration_experiment(self) -> "Experiment": """ Generate another ``Experiment`` object that can be used to calibrate the various multi-qubit observables involved in this ``Experiment``. This is achieved by preparing the plus-one (minus-one) eigenstate of each ``out_operator``, and measuring the resulting expectation value of the same ``out_operator``. Ideally, this would always give +1 (-1), but when symmetric readout error is present the effect is to scale the resultant expectations by some constant factor. Determining this scale factor is what we call *readout calibration*, and then the readout error in subsequent measurements can then be mitigated by simply dividing by the scale factor. :return: A new ``Experiment`` that can calibrate the readout error of all the observables involved in this experiment. """ if self.calibration != CalibrationMethod.PLUS_EIGENSTATE: raise ValueError( 'We currently only support the "plus eigenstate" calibration method.' ) calibration_settings = [] for settings in self: assert len(settings) == 1 calibration_settings.append( ExperimentSetting( in_state=settings[0].out_operator, out_operator=settings[0].out_operator, additional_expectations=settings[0]. additional_expectations, )) calibration_program = Program() if self.reset: calibration_program += RESET() calibration_program.wrap_in_numshots_loop(self.shots) if self.symmetrization != SymmetrizationLevel.EXHAUSTIVE: raise ValueError( "We currently only support calibration for exhaustive symmetrization" ) return Experiment( settings=calibration_settings, program=calibration_program, symmetrization=SymmetrizationLevel.EXHAUSTIVE, calibration=CalibrationMethod.NONE, )
def build_setting_memory_map(self, setting: ExperimentSetting) -> Dict[str, List[float]]: """ Build the memory map corresponding to the state preparation and measurement specifications encoded in the provided ``ExperimentSetting``, taking into account the full set of qubits that are present in the ``Experiment`` object. :return: Memory map for state prep and measurement. """ meas_qubits = self.get_meas_qubits() in_pt = PauliTerm.from_list([(op, meas_qubits.index(cast(int, q))) for q, op in setting._in_operator()]) out_pt = PauliTerm.from_list([(op, meas_qubits.index(cast(int, q))) for q, op in setting.out_operator]) preparation_map = pauli_term_to_preparation_memory_map(in_pt) measurement_map = pauli_term_to_measurement_memory_map(out_pt) return {**preparation_map, **measurement_map}
def experiment( self, experiment: Experiment, memory_map: Optional[Mapping[str, Sequence[Union[int, float]]]] = None, ) -> List[ExperimentResult]: """ Run an ``Experiment`` on a QVM or QPU backend. An ``Experiment`` is composed of: - A main ``Program`` body (or ansatz). - A collection of ``ExperimentSetting`` objects, each of which encodes a particular state preparation and measurement. - A ``SymmetrizationLevel`` for enacting different readout symmetrization strategies. - A number of shots to collect for each (unsymmetrized) ``ExperimentSetting``. Because the main ``Program`` is static from run to run of an ``Experiment``, we can leverage our platform's Parametric Compilation feature. This means that the ``Program`` can be compiled only once, and the various alterations due to state preparation, measurement, and symmetrization can all be realized at runtime by providing a ``memory_map``. Thus, the steps in the ``experiment`` method are as follows: 1. Generate a parameterized program corresponding to the ``Experiment`` (see the ``Experiment.generate_experiment_program()`` method for more details on how it changes the main body program to support state preparation, measurement, and symmetrization). 2. Compile the parameterized program into a parametric (binary) executable, which contains declared variables that can be assigned at runtime. 3. For each ``ExperimentSetting`` in the ``Experiment``, we repeat the following: a. Build a collection of memory maps that correspond to the various state preparation, measurement, and symmetrization specifications. b. Run the parametric executable on the QVM or QPU backend, providing the memory map to assign variables at runtime. c. Extract the desired statistics from the classified bitstrings that are produced by the QVM or QPU backend, and package them in an ``ExperimentResult`` object. 4. Return the list of ``ExperimentResult`` objects. This method is extremely useful shorthand for running near-term applications and algorithms, which often have this ansatz + settings structure. :param experiment: The ``Experiment`` to run. :param memory_map: A dictionary mapping declared variables / parameters to their values. The values are a list of floats or integers. Each float or integer corresponds to a particular classical memory register. The memory map provided to the ``experiment`` method corresponds to variables in the main body program that we would like to change at runtime (e.g. the variational parameters provided to the ansatz of the variational quantum eigensolver). :return: A list of ``ExperimentResult`` objects containing the statistics gathered according to the specifications of the ``Experiment``. """ experiment_program = experiment.generate_experiment_program() executable = self.compile(experiment_program) if memory_map is None: memory_map = {} results = [] for settings in experiment: if len(settings) > 1: raise ValueError("settings must be of length 1") setting = settings[0] qubits = cast(List[int], setting.out_operator.get_qubits()) experiment_setting_memory_map = experiment.build_setting_memory_map( setting) symmetrization_memory_maps = experiment.build_symmetrization_memory_maps( qubits) merged_memory_maps = merge_memory_map_lists( [experiment_setting_memory_map], symmetrization_memory_maps) all_bitstrings = [] for merged_memory_map in merged_memory_maps: final_memory_map = {**memory_map, **merged_memory_map} self.qam.reset() bitstrings = self.run(executable, memory_map=final_memory_map) if "symmetrization" in final_memory_map: bitmask = np.array( np.array(final_memory_map["symmetrization"]) / np.pi, dtype=int) bitstrings = np.bitwise_xor(bitstrings, bitmask) all_bitstrings.append(bitstrings) symmetrized_bitstrings = np.concatenate(all_bitstrings) joint_expectations = [experiment.get_meas_registers(qubits)] if setting.additional_expectations: joint_expectations += setting.additional_expectations expectations = bitstrings_to_expectations( symmetrized_bitstrings, joint_expectations=joint_expectations) means = cast(np.ndarray, np.mean(expectations, axis=0)) std_errs = np.std(expectations, axis=0, ddof=1) / np.sqrt( len(expectations)) joint_results = [] for qubit_subset, mean, std_err in zip(joint_expectations, means, std_errs): out_operator = PauliTerm.from_list([ (setting.out_operator[i], i) for i in qubit_subset ]) s = ExperimentSetting( in_state=setting.in_state, out_operator=out_operator, additional_expectations=None, ) r = ExperimentResult(setting=s, expectation=mean, std_err=std_err, total_counts=len(expectations)) joint_results.append(r) result = ExperimentResult( setting=setting, expectation=joint_results[0].expectation, std_err=joint_results[0].std_err, total_counts=joint_results[0].total_counts, additional_results=joint_results[1:], ) results.append(result) return results