diff --git a/Makefile b/Makefile index 86fbb93bc..7ed9f754e 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ lint: pylint -rn qiskit/providers/ibmq test mypy: - mypy --module qiskit.providers.ibmq --show-error-codes --no-site-packages + mypy --module qiskit.providers.ibmq --show-error-codes --no-site-packages --python-version 3.7 style: pycodestyle qiskit test diff --git a/qiskit/providers/ibmq/api/clients/experiment.py b/qiskit/providers/ibmq/api/clients/experiment.py index 39258461b..c1a656c41 100644 --- a/qiskit/providers/ibmq/api/clients/experiment.py +++ b/qiskit/providers/ibmq/api/clients/experiment.py @@ -46,15 +46,19 @@ def __init__( def experiments( self, + limit: Optional[int], + marker: Optional[str], backend_name: Optional[str], experiment_type: Optional[str] = None, start_time: Optional[List] = None, device_components: Optional[List[str]] = None, tags: Optional[List[str]] = None - ) -> List[Dict]: + ) -> Dict: """Retrieve experiments, with optional filtering. Args: + limit: Number of experiments to retrieve. + marker: Marker used to indicate where to start the next query. backend_name: Name of the backend. experiment_type: Experiment type. start_time: A list of timestamps used to filter by experiment start time. @@ -62,11 +66,11 @@ def experiments( tags: Tags used for filtering. Returns: - A list of experiments. + A list of experiments and the marker, if applicable. """ resp = self.base_api.experiments( - backend_name, experiment_type, start_time, device_components, tags) - return resp['experiments'] + limit, marker, backend_name, experiment_type, start_time, device_components, tags) + return resp def experiment_get(self, experiment_id: str) -> Dict: """Get a specific experiment. @@ -180,15 +184,19 @@ def experiment_devices(self) -> List: def analysis_results( self, + limit: Optional[int], + marker: Optional[str], backend_name: Optional[str] = None, device_components: Optional[List[str]] = None, experiment_uuid: Optional[str] = None, result_type: Optional[str] = None, quality: Optional[List[str]] = None - ) -> List[Dict]: + ) -> Dict: """Return a list of analysis results. Args: + limit: Number of analysis results to retrieve. + marker: Marker used to indicate where to start the next query. backend_name: Name of the backend. device_components: A list of device components used for filtering. experiment_uuid: Experiment UUID used for filtering. @@ -196,11 +204,12 @@ def analysis_results( quality: Quality value used for filtering. Returns: - A list of analysis results. + A list of analysis results and the marker, if applicable. """ resp = self.base_api.analysis_results( - backend_name, device_components, experiment_uuid, result_type, quality) - return resp['analysis_results'] + limit, marker, backend_name, device_components, experiment_uuid, + result_type, quality) + return resp def analysis_result_upload(self, result: Dict) -> Dict: """Upload an analysis result. diff --git a/qiskit/providers/ibmq/api/rest/root.py b/qiskit/providers/ibmq/api/rest/root.py index 2b1c6e1da..ec308af1d 100644 --- a/qiskit/providers/ibmq/api/rest/root.py +++ b/qiskit/providers/ibmq/api/rest/root.py @@ -150,6 +150,8 @@ def reservations(self) -> List: def experiments( self, + limit: Optional[int], + marker: Optional[str], backend_name: Optional[str] = None, experiment_type: Optional[str] = None, start_time: Optional[List] = None, @@ -159,6 +161,8 @@ def experiments( """Return experiment data. Args: + limit: Number of experiments to retrieve. + marker: Marker used to indicate where to start the next query. backend_name: Name of the backend. experiment_type: Experiment type. start_time: A list of timestamps used to filter by experiment start time. @@ -180,6 +184,10 @@ def experiments( params['device_components'] = device_components if tags: params['tags'] = tags + if limit: + params['limit'] = limit + if marker: + params['marker'] = marker return self.session.get(url, params=params).json() def experiment_devices(self) -> Dict: @@ -207,6 +215,8 @@ def experiment_upload(self, experiment: Dict) -> Dict: def analysis_results( self, + limit: Optional[int], + marker: Optional[str], backend_name: Optional[str] = None, device_components: Optional[List[str]] = None, experiment_uuid: Optional[str] = None, @@ -216,6 +226,8 @@ def analysis_results( """Return all analysis results. Args: + limit: Number of analysis results to retrieve. + marker: Marker used to indicate where to start the next query. backend_name: Name of the backend. device_components: A list of device components used for filtering. experiment_uuid: Experiment UUID used for filtering. @@ -237,6 +249,10 @@ def analysis_results( params['quality'] = quality if result_type: params['type'] = result_type + if limit: + params['limit'] = limit + if marker: + params['marker'] = marker return self.session.get(url, params=params).json() def analysis_result_upload(self, result: Dict) -> Dict: diff --git a/qiskit/providers/ibmq/experiment/experiment.py b/qiskit/providers/ibmq/experiment/experiment.py index 345316a75..e03b3ba5a 100644 --- a/qiskit/providers/ibmq/experiment/experiment.py +++ b/qiskit/providers/ibmq/experiment/experiment.py @@ -90,6 +90,7 @@ def __init__( self._updated_datetime = None self._api_client = provider.experiment._api_client # type: ignore[has-type] + self._provider = provider def update_from_remote_data(self, remote_data: Dict) -> None: """Update the attributes of this instance using remote data. @@ -184,10 +185,8 @@ def analysis_results(self) -> List: """Return analysis results associated with this experiment.""" if self._analysis_results is None: try: - response = self._api_client.analysis_results(experiment_uuid=self.uuid) - self._analysis_results = [] - for result in response: - self._analysis_results.append(AnalysisResult.from_remote_data(result)) + self._analysis_results = self._provider.experiment.analysis_results( + experiment_id=self.uuid, limit=None) except RequestsApiError as api_err: logger.warning("Unable to retrieve analysis results for this experiment: %s", str(api_err)) diff --git a/qiskit/providers/ibmq/experiment/experimentservice.py b/qiskit/providers/ibmq/experiment/experimentservice.py index a1394bc94..eaa3c3028 100644 --- a/qiskit/providers/ibmq/experiment/experimentservice.py +++ b/qiskit/providers/ibmq/experiment/experimentservice.py @@ -99,6 +99,7 @@ def backends(self) -> List[Dict]: def experiments( self, + limit: Optional[int] = 10, backend_name: Optional[str] = None, type: Optional[str] = None, # pylint: disable=redefined-builtin start_datetime: Optional[datetime] = None, @@ -110,6 +111,7 @@ def experiments( """Retrieve all experiments, with optional filtering. Args: + limit: Number of experiments to retrieve. ``None`` indicates no limit. backend_name: Backend name used for filtering. type: Experiment type used for filtering. start_datetime: Filter by the given start timestamp, in local time. This is used to @@ -133,8 +135,11 @@ def experiments( A list of experiments. Raises: - ValueError: If an invalid `tags_operator` value is specified. + ValueError: If an invalid parameter value is specified. """ + if limit is not None and (not isinstance(limit, int) or limit <= 0): # type: ignore + raise ValueError(f"{limit} is not a valid `limit`, which has to be a positive integer.") + start_time_filters = [] if start_datetime: st_filter = 'ge:{}'.format(local_to_utc_str(start_datetime)) @@ -153,11 +158,19 @@ def experiments( raise ValueError('{} is not a valid `tags_operator`. Valid values are ' '"AND" and "OR".'.format(tags_operator)) - raw_data = self._api_client.experiments( - backend_name, type, start_time_filters, device_components, tags_filter) experiments = [] - for exp in raw_data: - experiments.append(Experiment.from_remote_data(self._provider, exp)) + marker = None + while limit is None or limit > 0: + raw_data = self._api_client.experiments( + limit, marker, backend_name, type, start_time_filters, + device_components, tags_filter) + marker = raw_data.get('marker') + for exp in raw_data['experiments']: + experiments.append(Experiment.from_remote_data(self._provider, exp)) + if limit: + limit -= len(raw_data['experiments']) + if not marker: # No more experiments to return. + break return experiments def upload_experiment(self, experiment: Experiment) -> None: @@ -250,6 +263,7 @@ def delete_experiment(self, experiment: Union[Experiment, str]) -> Optional[Expe def analysis_results( self, + limit: Optional[int] = 10, backend_name: Optional[str] = None, device_components: Optional[List[str]] = None, experiment_id: Optional[str] = None, @@ -259,6 +273,7 @@ def analysis_results( """Retrieve all analysis results, with optional filtering. Args: + limit: Number of analysis results to retrieve. backend_name: Backend name used for filtering. device_components: Filter by device components. An analysis result's device components must match this list exactly for it to be included. @@ -274,7 +289,13 @@ def analysis_results( Returns: A list of analysis results. + + Raises: + ValueError: If an invalid parameter value is specified. """ + if limit is not None and (not isinstance(limit, int) or limit <= 0): # type: ignore + raise ValueError(f"{limit} is not a valid `limit`, which has to be a positive integer.") + qualit_list = [] if quality: for op, qual in quality: @@ -282,12 +303,20 @@ def analysis_results( qual = qual.value qual_str = qual if op == 'eq' else "{}:{}".format(op, qual) qualit_list.append(qual_str) - response = self._api_client.analysis_results( - backend_name=backend_name, device_components=device_components, - experiment_uuid=experiment_id, result_type=result_type, quality=qualit_list) results = [] - for result in response: - results.append(AnalysisResult.from_remote_data(result)) + marker = None + while limit is None or limit > 0: + raw_data = self._api_client.analysis_results( + limit=limit, marker=marker, + backend_name=backend_name, device_components=device_components, + experiment_uuid=experiment_id, result_type=result_type, quality=qualit_list) + marker = raw_data.get('marker') + for result in raw_data['analysis_results']: + results.append(AnalysisResult.from_remote_data(result)) + if limit: + limit -= len(raw_data['analysis_results']) + if not marker: # No more experiments to return. + break return results def upload_analysis_result(self, result: AnalysisResult) -> None: diff --git a/releasenotes/notes/experiments-limit-c7805ef972b7b4c1.yaml b/releasenotes/notes/experiments-limit-c7805ef972b7b4c1.yaml new file mode 100644 index 000000000..0717edffe --- /dev/null +++ b/releasenotes/notes/experiments-limit-c7805ef972b7b4c1.yaml @@ -0,0 +1,15 @@ +--- +features: + - | + Methods + :meth:`qiskit.providers.ibmq.experiment.ExperimentService.experiments` and + :meth"`qiskit.providers.ibmq.experiment.ExperimentService.analysis_results` + now support a ``limit`` parameter that allows you to limit the number of + experiments and analysis results returned. +upgrade: + - | + A new parameter, ``limit`` is now the first parameter for both + :meth:`qiskit.providers.ibmq.experiment.ExperimentService.experiments` and + :meth"`qiskit.providers.ibmq.experiment.ExperimentService.analysis_results` + methods. This ``limit`` has a default value of 10, meaning by deafult only + 10 experiments and analysis results will be returned. diff --git a/test/ibmq/test_experiment.py b/test/ibmq/test_experiment.py index 3124fe4c6..5f3619f7d 100644 --- a/test/ibmq/test_experiment.py +++ b/test/ibmq/test_experiment.py @@ -89,7 +89,7 @@ def test_unauthorized(self): self.provider._experiment = saved_experiment def test_experiments(self): - """Test retrieving all experiments.""" + """Test retrieving experiments.""" self.assertTrue(self.experiments, "No experiments found.") for exp in self.experiments: self.assertTrue(isinstance(exp, Experiment)) @@ -103,7 +103,8 @@ def test_experiments_with_backend(self): """Test retrieving all experiments for a specific backend.""" backend_name = self.experiments[0].backend_name ref_uuid = self.experiments[0].uuid - backend_experiments = self.provider.experiment.experiments(backend_name=backend_name) + backend_experiments = self.provider.experiment.experiments( + backend_name=backend_name, limit=None) found = False for exp in backend_experiments: @@ -117,7 +118,7 @@ def test_experiments_with_type(self): """Test retrieving all experiments for a specific type.""" expr_type = self.experiments[0].type ref_uuid = self.experiments[0].uuid - backend_experiments = self.provider.experiment.experiments(type=expr_type) + backend_experiments = self.provider.experiment.experiments(type=expr_type, limit=None) found = False for exp in backend_experiments: @@ -145,7 +146,7 @@ def test_experiments_with_start_time(self): for start_dt, end_dt, expected, title in sub_tests: with self.subTest(title=title): backend_experiments = self.provider.experiment.experiments( - start_datetime=start_dt, end_datetime=end_dt) + start_datetime=start_dt, end_datetime=end_dt, limit=None) found = False for exp in backend_experiments: if start_dt: @@ -180,7 +181,7 @@ def test_experiments_with_tags(self): for tags, operator, found in sub_tests: with self.subTest(tags=tags, operator=operator): experiments = self.provider.experiment.experiments( - tags=tags, tags_operator=operator) + tags=tags, tags_operator=operator, limit=None) ref_expr_found = False for expr in experiments: msg = "Tags {} not fond in experiment tags {}".format(tags, expr.tags) @@ -329,7 +330,8 @@ def test_results_experiments_device_components(self): for dev_comp, found in sub_tests: with self.subTest(dev_comp=dev_comp): - f_results = self.provider.experiment.analysis_results(device_components=dev_comp) + f_results = self.provider.experiment.analysis_results( + device_components=dev_comp, limit=None) self.assertEqual(ref_result.uuid in [res.uuid for res in f_results], found, "Analysis result {} with device component {} (not)found " "unexpectedly when filter using device component={}. " @@ -341,7 +343,8 @@ def test_results_experiments_device_components(self): "does not match {}.".format( result.uuid, result.device_components, dev_comp)) - f_experiments = self.provider.experiment.experiments(device_components=dev_comp) + f_experiments = self.provider.experiment.experiments( + device_components=dev_comp, limit=None) for exp in f_experiments[:5]: found = False result_dev_comp = [] @@ -366,7 +369,7 @@ def test_analysis_results_experiment_uuid(self): for expr_uuid, found in sub_tests: with self.subTest(expr_uuid=expr_uuid): f_results = self.provider.experiment.analysis_results( - experiment_id=expr_uuid) + experiment_id=expr_uuid, limit=None) self.assertEqual(ref_result.uuid in [res.uuid for res in f_results], found, "Analysis result {} with experiment uuid {} (not)found " "unexpectedly when filter using experiment uuid={}. " @@ -390,7 +393,8 @@ def test_analysis_results_type(self): for res_type, found in sub_tests: with self.subTest(res_type=res_type): - f_results = self.provider.experiment.analysis_results(result_type=res_type) + f_results = self.provider.experiment.analysis_results( + result_type=res_type, limit=None) self.assertEqual(ref_result.uuid in [res.uuid for res in f_results], found, "Analysis result {} with type {} (not)found unexpectedly " "when filter using type={}. Found={}.".format( @@ -436,7 +440,7 @@ def test_analysis_results_quality(self): for quality, found in sub_tests: with self.subTest(quality=quality): - f_results = self.provider.experiment.analysis_results(quality=quality) + f_results = self.provider.experiment.analysis_results(quality=quality, limit=None) self.assertEqual(ref_result.uuid in [res.uuid for res in f_results], found, "Analysis result {} with quality {} (not)found unexpectedly " "when filter using quality={}. Found={}.".format( @@ -526,6 +530,33 @@ def test_upload_update_plot_data(self): rplot = self.provider.experiment.retrieve_plot(new_exp.uuid, plot_name) self.assertEqual(rplot, friend_bytes, "Retrieved plot not equal updated plot.") + def test_experiments_limit(self): + """Test getting experiments with a limit.""" + limits = [10, 1000, 1001] + + for limit in limits: + with self.subTest(limit=limit): + experiments = self.provider.experiment.experiments(limit=limit) + self.assertEqual(len(experiments), limit) + self.assertEqual(len({expr.uuid for expr in experiments}), len(experiments)) + + with self.assertRaises(ValueError) as context_manager: + self.provider.experiment.experiments(limit=-1) + self.assertIn("limit", str(context_manager.exception)) + + def test_analysis_results_limit(self): + """Test getting analysis results with a limit.""" + limits = [10, 1000, 1001] + for limit in limits: + with self.subTest(limit=limit): + results = self.provider.experiment.analysis_results(limit=limit) + self.assertEqual(len(results), limit) + self.assertEqual(len({res.uuid for res in results}), len(results)) + + with self.assertRaises(ValueError) as context_manager: + self.provider.experiment.analysis_results(limit=-1) + self.assertIn("limit", str(context_manager.exception)) + def _create_experiment( self, backend_name: Optional[str] = None,