Source code for qiskit_experiments.framework.composite.composite_analysis
# This code is part of Qiskit.## (C) Copyright IBM 2021.## This code is licensed under the Apache License, Version 2.0. You may# obtain a copy of this license in the LICENSE.txt file in the root directory# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.## Any modifications or derivative works of this code must retain this# copyright notice, and modified files need to carry a notice indicating# that they have been altered from the originals."""Composite Experiment Analysis class."""fromtypingimportList,Dict,Union,Optional,Tupleimportnumpyasnpfromqiskit.resultimportmarginal_distributionfromqiskit.result.postprocessimportformat_counts_memoryfromqiskit_experiments.frameworkimportBaseAnalysis,ExperimentDatafromqiskit_experiments.framework.analysis_result_dataimportAnalysisResultDatafromqiskit_experiments.framework.base_analysisimport_requires_copyfromqiskit_experiments.exceptionsimportAnalysisError
[docs]classCompositeAnalysis(BaseAnalysis):"""Run analysis for composite experiments. Composite experiments consist of several component experiments run together in a single execution, the results of which are returned as a single list of circuit result data in the :class:`ExperimentData` container. Analysis of this composite circuit data involves constructing a list of experiment data containers for each component experiment containing the marginalized circuit result data for that experiment. These are saved as :meth:`~.ExperimentData.child_data` in the main :class:`.ExperimentData` container. Each component experiment data is then analyzed using the analysis class from the corresponding component experiment. .. note:: If the composite :class:`ExperimentData` does not already contain child experiment data containers for the component experiments they will be initialized and added to the experiment data when :meth:`~.CompositeAnalysis.run` is called on the composite data. When calling :meth:`~.CompositeAnalysis.run` on experiment data already containing initialized component experiment data, any previously stored circuit data will be cleared and replaced with the marginalized data from the composite experiment data. """def__init__(self,analyses:List[BaseAnalysis],flatten_results:bool=True,generate_figures:Optional[str]="always",):"""Initialize a composite analysis class. Args: analyses: a list of component experiment analysis objects. flatten_results: If True flatten all component experiment results into a single ExperimentData container, including nested composite experiments. If False save each component experiment results as a separate child ExperimentData container. generate_figures: Optional flag to set the figure generation behavior. If ``always``, figures are always generated. If ``never``, figures are never generated. If ``selective``, figures are generated if the analysis ``quality`` is ``bad``. """super().__init__()self._analyses=analysesself._flatten_results=Falseifflatten_results:self._set_flatten_results()self._set_generate_figures(generate_figures)
[docs]defcomponent_analysis(self,index:Optional[int]=None)->Union[BaseAnalysis,List[BaseAnalysis]]:"""Return the component experiment Analysis instance. Args: index: Optional, the component index to return analysis for. If None return a list of all component analysis instances. Returns: The analysis instance for the specified index, or a list of all analysis instances if index is None. """ifindexisNone:returnself._analysesreturnself._analyses[index]
[docs]defset_options(self,**fields):"""Set the analysis options for the experiment. If the `broadcast` argument set to `True`, the analysis options will cascade to the child experiments."""super().set_options(**fields)iffields.get("broadcast",None):forsub_analysisinself._analyses:sub_analysis.set_options(**fields)
[docs]defrun(self,experiment_data:ExperimentData,replace_results:bool=False,**options,)->ExperimentData:# Make a new copy of experiment data if not updating resultsifnotreplace_resultsand_requires_copy(experiment_data):experiment_data=experiment_data.copy()ifnotself._flatten_results:# Initialize child components if they are not initialized# This only needs to be done if results are not being flattenedself._add_child_data(experiment_data)# Run analysis with replace_results = True since we have already# created the copy if it was requiredreturnsuper().run(experiment_data,replace_results=True,**options)
def_run_analysis(self,experiment_data:ExperimentData):# Return list of experiment data containers for each component experiment# containing the marginalized data from the composite experimentcomponent_expdata=self._component_experiment_data(experiment_data)# Run the component analysis on each component datafori,sub_expdatainenumerate(component_expdata):# Since copy for replace result is handled at the parent level# we always run with replace result on component analysisself._analyses[i].run(sub_expdata,replace_results=True)# Analysis is running in parallel so we add loop to wait# for all component analysis to finish before returning# the parent experiment analysis resultsforsub_expdataincomponent_expdata:sub_expdata.block_for_results()# Optionally flatten results from all component experiments# for adding to the main experiment data containerifself._flatten_results:analysis_results,figures=self._combine_results(component_expdata)forresinanalysis_results:# Override experiment ID because entries are flattenedres.experiment_id=experiment_data.experiment_idreturnanalysis_results,figuresreturn[],[]def_component_experiment_data(self,experiment_data:ExperimentData)->List[ExperimentData]:"""Return a list of marginalized experiment data for component experiments. Args: experiment_data: a composite experiment data container. Returns: The list of analysis-ready marginalized experiment data for each component experiment. Raises: AnalysisError: If the component experiment data cannot be extracted. """ifnotself._flatten_results:# Retrieve child data for component experiments for updatingcomponent_index=experiment_data.metadata.get("component_child_index",[])ifnotcomponent_index:raiseAnalysisError("Unable to extract component child experiment data")component_expdata=[experiment_data.child_data(i)foriincomponent_index]else:# Initialize temporary ExperimentData containers for# each component experiment to analysis on. These will# not be saved but results and figures will be collected# from themcomponent_expdata=self._initialize_component_experiment_data(experiment_data)# Compute marginalize data for each component experimentmarginalized_data=self._marginalized_component_data(experiment_data.data())# Add the marginalized component data and component job metadata# to each component child experiment. Note that this will clear# any currently stored data in the experiment. Since copying of# child data is handled by the `replace_results` kwarg of the# parent container it is safe to always clear and replace the# results of child containers in this stepforsub_expdata,sub_datainzip(component_expdata,marginalized_data):# Clear any previously stored data and add marginalized datasub_expdata._result_data.clear()sub_expdata.add_data(sub_data)returncomponent_expdatadef_marginalized_component_data(self,composite_data:List[Dict])->List[List[Dict]]:"""Return marginalized data for component experiments. Args: composite_data: a list of composite experiment circuit data. Returns: A List of lists of marginalized circuit data for each component experiment in the composite experiment. """# Marginalize datamarginalized_data={}fordatumincomposite_data:metadata=datum.get("metadata",{})# Add marginalized data to sub experimentsif"composite_clbits"inmetadata:composite_clbits=metadata["composite_clbits"]else:composite_clbits=None# Pre-process the memory if any to avoid redundant calls to format_counts_memoryf_memory=self._format_memory(datum,composite_clbits)fori,indexinenumerate(metadata["composite_index"]):ifindexnotinmarginalized_data:# Initialize data list for marginalizedmarginalized_data[index]=[]sub_data={k:vfork,vindatum.items()ifknotin("metadata","counts","memory")}sub_data["metadata"]=metadata["composite_metadata"][i]if"counts"indatum:ifcomposite_clbitsisnotNone:sub_data["counts"]=marginal_distribution(counts=datum["counts"],indices=composite_clbits[i],)else:sub_data["counts"]=datum["counts"]if"memory"indatum:ifcomposite_clbitsisnotNone:# level 2iff_memoryisnotNone:idx=slice(-1-composite_clbits[i][-1],-composite_clbits[i][0]orNone)sub_data["memory"]=[shot[idx]forshotinf_memory]# level 1else:mem=np.array(datum["memory"])# Averaged level 1 dataiflen(mem.shape)==2:sub_data["memory"]=mem[composite_clbits[i]].tolist()# Single-shot level 1 dataiflen(mem.shape)==3:sub_data["memory"]=mem[:,composite_clbits[i]].tolist()else:sub_data["memory"]=datum["memory"]marginalized_data[index].append(sub_data)# Sort by indexreturn[marginalized_data[i]foriinsorted(marginalized_data.keys())]@staticmethoddef_format_memory(datum:Dict,composite_clbits:List):"""A helper method to convert level 2 memory (if it exists) to bit-string format."""f_memory=Noneif("memory"indatumandcomposite_clbitsisnotNoneandisinstance(datum["memory"][0],str)):num_cbits=1+max(cbitforcbit_listincomposite_clbitsforcbitincbit_list)header={"memory_slots":num_cbits}f_memory=list(format_counts_memory(shot,header)forshotindatum["memory"])returnf_memorydef_add_child_data(self,experiment_data:ExperimentData):"""Save empty component experiment data as child data. This will initialize empty ExperimentData objects for each component experiment and add them as child data to the main composite experiment ExperimentData container container for saving. Args: experiment_data: a composite experiment experiment data container. """component_index=experiment_data.metadata.get("component_child_index",[])ifcomponent_index:# Child components are already initializedreturn# Initialize the component experiment data containers and add them# as child data to the current experiment datachild_components=self._initialize_component_experiment_data(experiment_data)start_index=len(experiment_data.child_data())fori,subdatainenumerate(child_components):experiment_data.add_child_data(subdata)component_index.append(start_index+i)# Store the indices of the added child data in metadataexperiment_data.metadata["component_child_index"]=component_indexdef_initialize_component_experiment_data(self,experiment_data:ExperimentData)->List[ExperimentData]:"""Initialize empty experiment data containers for component experiments. Args: experiment_data: a composite experiment experiment data container. Returns: The list of experiment data containers for each component experiment containing the component metadata, and tags, share level, and auto save settings of the composite experiment. """# Extract component experiment types and metadata so they can be# added to the component experiment data containersmetadata=experiment_data.metadatanum_components=len(self._analyses)experiment_types=metadata.get("component_types",[None]*num_components)component_metadata=metadata.get("component_metadata",[{}]*num_components)# Create component experiments and set the backend and# metadata for the componentscomponent_expdata=[]fori,_inenumerate(self._analyses):subdata=ExperimentData(backend=experiment_data.backend)subdata.experiment_type=experiment_types[i]subdata.metadata.update(component_metadata[i])ifself._flatten_results:# Explicitly set auto_save to false so the temporary# data can't accidentally be savedsubdata.auto_save=Falseelse:# Copy tags, share_level and auto_save from the parent# experiment data if results are not being flattened.subdata.tags=experiment_data.tagssubdata.share_level=experiment_data.share_levelsubdata.auto_save=experiment_data.auto_savecomponent_expdata.append(subdata)returncomponent_expdatadef_set_flatten_results(self):"""Recursively set flatten_results to True for all composite components."""self._flatten_results=Trueforanalysisinself._analyses:ifisinstance(analysis,CompositeAnalysis):analysis._set_flatten_results()def_set_generate_figures(self,generate_figures):"""Recursively propagate ``generate_figures`` to all child experiments."""self._generate_figures=generate_figuresforanalysisinself._analyses:ifisinstance(analysis,CompositeAnalysis):analysis._set_generate_figures(generate_figures)else:analysis._generate_figures=generate_figuresdef_combine_results(self,component_experiment_data:List[ExperimentData],)->Tuple[List[AnalysisResultData],List["matplotlib.figure.Figure"]]:"""Combine analysis results from component experiment data. Args: component_experiment_data: list of experiment data containers containing the analysis results for each component experiment. Returns: A pair of the combined list of all analysis results from each of the component experiments, and a list of all figures from each component experiment. """analysis_results=[]figures=[]forsub_expdataincomponent_experiment_data:figures+=sub_expdata._figures.values()# Convert Dataframe Series back into AnalysisResultData# This is due to limitation that _run_analysis must return List[AnalysisResultData],# and some composite analysis such as TphiAnalysis overrides this method to# return extra quantity computed from sub analysis results.# This produces unnecessary data conversion.# The _run_analysis mechanism seems just complicating the entire logic.# Since it's impossible to deprecate the usage of this protected method,# we should implement new CompositeAnalysis class with much more efficient# internal logic. Note that the child data structure is no longer necessary# because dataframe offers more efficient data filtering mechanisms.analysis_table=sub_expdata.analysis_results(columns="all",dataframe=True)for_,seriesinanalysis_table.iterrows():data=AnalysisResultData.from_table_element(**series.to_dict())analysis_results.append(data)forartifactinsub_expdata.artifacts():analysis_results.append(artifact)returnanalysis_results,figures