diff --git a/cirq-core/cirq/study/result.py b/cirq-core/cirq/study/result.py index 32b0c33d9ca..968b03b0d5c 100644 --- a/cirq-core/cirq/study/result.py +++ b/cirq-core/cirq/study/result.py @@ -406,11 +406,12 @@ def data(self) -> pd.DataFrame: self._data = self.dataframe_from_measurements(self.measurements) return self._data + def _record_dict_repr(self): + """Helper function for use in __repr__ to display the records field.""" + return '{' + ', '.join(f'{k!r}: {proper_repr(v)}' for k, v in self.records.items()) + '}' + def __repr__(self) -> str: - record_dict_repr = ( - '{' + ', '.join(f'{k!r}: {proper_repr(v)}' for k, v in self.records.items()) + '}' - ) - return f'cirq.ResultDict(params={self.params!r}, records={record_dict_repr})' + return f'cirq.ResultDict(params={self.params!r}, records={self._record_dict_repr()})' def _repr_pretty_(self, p: Any, cycle: bool) -> None: """Output to show in ipython and Jupyter notebooks.""" @@ -435,6 +436,11 @@ def _json_dict_(self): } return {'params': self.params, 'records': packed_records} + @classmethod + def _from_packed_records(cls, records, **kwargs): + """Helper function for `_from_json_dict_` to construct from packed records.""" + return cls(records={key: _unpack_digits(**val) for key, val in records.items()}, **kwargs) + @classmethod def _from_json_dict_(cls, params, **kwargs): if 'measurements' in kwargs: @@ -443,10 +449,7 @@ def _from_json_dict_(cls, params, **kwargs): params=params, measurements={key: _unpack_digits(**val) for key, val in measurements.items()}, ) - records = kwargs['records'] - return cls( - params=params, records={key: _unpack_digits(**val) for key, val in records.items()} - ) + return cls._from_packed_records(params=params, records=kwargs['records']) def _pack_digits(digits: np.ndarray, pack_bits: str = 'auto') -> Tuple[str, bool]: diff --git a/cirq-google/cirq_google/__init__.py b/cirq-google/cirq_google/__init__.py index 971de532d61..1b516754a1c 100644 --- a/cirq-google/cirq_google/__init__.py +++ b/cirq-google/cirq_google/__init__.py @@ -73,6 +73,7 @@ EngineJob, EngineProgram, EngineProcessor, + EngineResult, ProtoVersion, QuantumEngineSampler, ValidatingSampler, diff --git a/cirq-google/cirq_google/engine/__init__.py b/cirq-google/cirq_google/engine/__init__.py index 26dfd406715..04771f8b96e 100644 --- a/cirq-google/cirq_google/engine/__init__.py +++ b/cirq-google/cirq_google/engine/__init__.py @@ -76,3 +76,5 @@ create_noiseless_virtual_engine_from_templates, create_noiseless_virtual_engine_from_latest_templates, ) + +from cirq_google.engine.engine_result import EngineResult diff --git a/cirq-google/cirq_google/engine/abstract_job.py b/cirq-google/cirq_google/engine/abstract_job.py index 0bf77dc0e24..aba8f07cddb 100644 --- a/cirq-google/cirq_google/engine/abstract_job.py +++ b/cirq-google/cirq_google/engine/abstract_job.py @@ -18,6 +18,7 @@ import cirq from cirq_google.cloud import quantum +from cirq_google.engine.engine_result import EngineResult if TYPE_CHECKING: import datetime @@ -161,7 +162,7 @@ def delete(self) -> Optional[bool]: """Deletes the job and result, if any.""" @abc.abstractmethod - def batched_results(self) -> Sequence[Sequence[cirq.Result]]: + def batched_results(self) -> Sequence[Sequence[EngineResult]]: """Returns the job results, blocking until the job is complete. This method is intended for batched jobs. Instead of flattening @@ -170,7 +171,7 @@ def batched_results(self) -> Sequence[Sequence[cirq.Result]]: """ @abc.abstractmethod - def results(self) -> Sequence[cirq.Result]: + def results(self) -> Sequence[EngineResult]: """Returns the job results, blocking until the job is complete.""" @abc.abstractmethod diff --git a/cirq-google/cirq_google/engine/abstract_local_job_test.py b/cirq-google/cirq_google/engine/abstract_local_job_test.py index 6735a872656..4cc6dff50d2 100644 --- a/cirq-google/cirq_google/engine/abstract_local_job_test.py +++ b/cirq-google/cirq_google/engine/abstract_local_job_test.py @@ -19,6 +19,7 @@ from cirq_google.cloud import quantum from cirq_google.engine.calibration_result import CalibrationResult from cirq_google.engine.abstract_local_job import AbstractLocalJob +from cirq_google.engine.engine_result import EngineResult class NothingJob(AbstractLocalJob): @@ -40,10 +41,10 @@ def cancel(self) -> None: def delete(self) -> None: pass - def batched_results(self) -> Sequence[Sequence[cirq.Result]]: + def batched_results(self) -> Sequence[Sequence[EngineResult]]: return [] # coverage: ignore - def results(self) -> Sequence[cirq.Result]: + def results(self) -> Sequence[EngineResult]: return [] # coverage: ignore def calibration_results(self) -> Sequence[CalibrationResult]: diff --git a/cirq-google/cirq_google/engine/engine_job.py b/cirq-google/cirq_google/engine/engine_job.py index 416d161c88a..dda063ec679 100644 --- a/cirq-google/cirq_google/engine/engine_job.py +++ b/cirq-google/cirq_google/engine/engine_job.py @@ -24,6 +24,7 @@ from cirq_google.engine.calibration_result import CalibrationResult from cirq_google.cloud import quantum from cirq_google.engine.result_type import ResultType +from cirq_google.engine.engine_result import EngineResult from cirq_google.api import v1, v2 if TYPE_CHECKING: @@ -39,6 +40,10 @@ ] +def _flatten(result: Sequence[Sequence[EngineResult]]) -> List[EngineResult]: + return [res for result_list in result for res in result_list] + + class EngineJob(abstract_job.AbstractJob): """A job created via the Quantum Engine API. @@ -81,9 +86,9 @@ def __init__( self.job_id = job_id self.context = context self._job = _job - self._results: Optional[Sequence[cirq.Result]] = None + self._results: Optional[Sequence[EngineResult]] = None self._calibration_results: Optional[Sequence[CalibrationResult]] = None - self._batched_results: Optional[Sequence[Sequence[cirq.Result]]] = None + self._batched_results: Optional[Sequence[Sequence[EngineResult]]] = None self.result_type = result_type def id(self) -> str: @@ -122,10 +127,8 @@ def create_time(self) -> 'datetime.datetime': def update_time(self) -> 'datetime.datetime': """Returns when the job was last updated.""" - self._job = self.context.client.get_job( - self.project_id, self.program_id, self.job_id, False - ) - return self._job.update_time + job = self._refresh_job() + return job.update_time def description(self) -> str: """Returns the description of the job.""" @@ -257,7 +260,7 @@ def delete(self) -> None: """Deletes the job and result, if any.""" self.context.client.delete_job(self.project_id, self.program_id, self.job_id) - def batched_results(self) -> Sequence[Sequence[cirq.Result]]: + def batched_results(self) -> Sequence[Sequence[EngineResult]]: """Returns the job results, blocking until the job is complete. This method is intended for batched jobs. Instead of flattening @@ -287,7 +290,7 @@ def _wait_for_result(self): ) return response.result - def results(self) -> Sequence[cirq.Result]: + def results(self) -> Sequence[EngineResult]: """Returns the job results, blocking until the job is complete.""" import cirq_google.engine.engine as engine_base @@ -299,17 +302,17 @@ def results(self) -> Sequence[cirq.Result]: or result_type == 'cirq.api.google.v1.Result' ): v1_parsed_result = v1.program_pb2.Result.FromString(result.value) - self._results = _get_job_results_v1(v1_parsed_result) + self._results = self._get_job_results_v1(v1_parsed_result) # coverage: ignore elif ( result_type == 'cirq.google.api.v2.Result' or result_type == 'cirq.api.google.v2.Result' ): v2_parsed_result = v2.result_pb2.Result.FromString(result.value) - self._results = _get_job_results_v2(v2_parsed_result) + self._results = self._get_job_results_v2(v2_parsed_result) elif result.Is(v2.batch_pb2.BatchResult.DESCRIPTOR): v2_parsed_result = v2.batch_pb2.BatchResult.FromString(result.value) self._batched_results = self._get_batch_results_v2(v2_parsed_result) - self._results = self._flatten(self._batched_results) + self._results = _flatten(self._batched_results) else: raise ValueError(f'invalid result proto version: {result_type}') return self._results @@ -340,19 +343,45 @@ def calibration_results(self) -> Sequence[CalibrationResult]: self._calibration_results = cal_results return self._calibration_results - @classmethod - def _get_batch_results_v2( - cls, results: v2.batch_pb2.BatchResult - ) -> Sequence[Sequence[cirq.Result]]: + def _get_job_results_v1(self, result: v1.program_pb2.Result) -> Sequence[EngineResult]: + # coverage: ignore + job_id = self.id() + job_finished = self.update_time() + trial_results = [] - for result in results.results: - # Add a new list for the result - trial_results.append(_get_job_results_v2(result)) + for sweep_result in result.sweep_results: + sweep_repetitions = sweep_result.repetitions + key_sizes = [(m.key, len(m.qubits)) for m in sweep_result.measurement_keys] + for result in sweep_result.parameterized_results: + data = result.measurement_results + measurements = v1.unpack_results(data, sweep_repetitions, key_sizes) + + trial_results.append( + EngineResult( + params=cirq.ParamResolver(result.params.assignments), + measurements=measurements, + job_id=job_id, + job_finished_time=job_finished, + ) + ) return trial_results - @classmethod - def _flatten(cls, result) -> Sequence[cirq.Result]: - return [res for result_list in result for res in result_list] + def _get_job_results_v2(self, result: v2.result_pb2.Result) -> Sequence[EngineResult]: + sweep_results = v2.results_from_proto(result) + job_id = self.id() + job_finished = self.update_time() + + # Flatten to single list to match to sampler api. + return [ + EngineResult.from_result(result, job_id=job_id, job_finished_time=job_finished) + for sweep_result in sweep_results + for result in sweep_result + ] + + def _get_batch_results_v2( + self, results: v2.batch_pb2.BatchResult + ) -> Sequence[Sequence[EngineResult]]: + return [self._get_job_results_v2(result) for result in results.results] def __iter__(self) -> Iterator[cirq.Result]: return iter(self.results()) @@ -401,29 +430,6 @@ def _deserialize_run_context(run_context: any_pb2.Any) -> Tuple[int, List[cirq.S raise ValueError(f'unsupported run_context type: {run_context_type}') -def _get_job_results_v1(result: v1.program_pb2.Result) -> Sequence[cirq.Result]: - trial_results = [] - for sweep_result in result.sweep_results: - sweep_repetitions = sweep_result.repetitions - key_sizes = [(m.key, len(m.qubits)) for m in sweep_result.measurement_keys] - for result in sweep_result.parameterized_results: - data = result.measurement_results - measurements = v1.unpack_results(data, sweep_repetitions, key_sizes) - - trial_results.append( - cirq.ResultDict( - params=cirq.ParamResolver(result.params.assignments), measurements=measurements - ) - ) - return trial_results - - -def _get_job_results_v2(result: v2.result_pb2.Result) -> Sequence[cirq.Result]: - sweep_results = v2.results_from_proto(result) - # Flatten to single list to match to sampler api. - return [trial_result for sweep_result in sweep_results for trial_result in sweep_result] - - def _raise_on_failure(job: quantum.QuantumJob) -> None: execution_status = job.execution_status state = execution_status.state diff --git a/cirq-google/cirq_google/engine/engine_job_test.py b/cirq-google/cirq_google/engine/engine_job_test.py index d164d2b6ff8..87301983491 100644 --- a/cirq-google/cirq_google/engine/engine_job_test.py +++ b/cirq-google/cirq_google/engine/engine_job_test.py @@ -501,11 +501,14 @@ def test_delete(delete_job): ) ) +UPDATE_TIME = datetime.datetime.now(tz=datetime.timezone.utc) + @mock.patch('cirq_google.engine.engine_client.EngineClient.get_job_results') def test_results(get_job_results): qjob = quantum.QuantumJob( - execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.SUCCESS) + execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.SUCCESS), + update_time=UPDATE_TIME, ) get_job_results.return_value = RESULTS @@ -520,7 +523,8 @@ def test_results(get_job_results): @mock.patch('cirq_google.engine.engine_client.EngineClient.get_job_results') def test_results_iter(get_job_results): qjob = quantum.QuantumJob( - execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.SUCCESS) + execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.SUCCESS), + update_time=UPDATE_TIME, ) get_job_results.return_value = RESULTS @@ -534,7 +538,8 @@ def test_results_iter(get_job_results): @mock.patch('cirq_google.engine.engine_client.EngineClient.get_job_results') def test_results_getitem(get_job_results): qjob = quantum.QuantumJob( - execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.SUCCESS) + execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.SUCCESS), + update_time=UPDATE_TIME, ) get_job_results.return_value = RESULTS @@ -548,7 +553,8 @@ def test_results_getitem(get_job_results): @mock.patch('cirq_google.engine.engine_client.EngineClient.get_job_results') def test_batched_results(get_job_results): qjob = quantum.QuantumJob( - execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.SUCCESS) + execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.SUCCESS), + update_time=UPDATE_TIME, ) get_job_results.return_value = BATCH_RESULTS @@ -574,7 +580,8 @@ def test_batched_results(get_job_results): @mock.patch('cirq_google.engine.engine_client.EngineClient.get_job_results') def test_batched_results_not_a_batch(get_job_results): qjob = quantum.QuantumJob( - execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.SUCCESS) + execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.SUCCESS), + update_time=UPDATE_TIME, ) get_job_results.return_value = RESULTS job = cg.EngineJob('a', 'b', 'steve', EngineContext(), _job=qjob) @@ -585,7 +592,8 @@ def test_batched_results_not_a_batch(get_job_results): @mock.patch('cirq_google.engine.engine_client.EngineClient.get_job_results') def test_calibration_results(get_job_results): qjob = quantum.QuantumJob( - execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.SUCCESS) + execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.SUCCESS), + update_time=UPDATE_TIME, ) get_job_results.return_value = CALIBRATION_RESULT job = cg.EngineJob('a', 'b', 'steve', EngineContext(), _job=qjob) @@ -603,7 +611,8 @@ def test_calibration_results(get_job_results): @mock.patch('cirq_google.engine.engine_client.EngineClient.get_job_results') def test_calibration_defaults(get_job_results): qjob = quantum.QuantumJob( - execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.SUCCESS) + execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.SUCCESS), + update_time=UPDATE_TIME, ) result = v2.calibration_pb2.FocusedCalibrationResult() result.results.add() @@ -622,7 +631,8 @@ def test_calibration_defaults(get_job_results): @mock.patch('cirq_google.engine.engine_client.EngineClient.get_job_results') def test_calibration_results_not_a_calibration(get_job_results): qjob = quantum.QuantumJob( - execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.SUCCESS) + execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.SUCCESS), + update_time=UPDATE_TIME, ) get_job_results.return_value = RESULTS job = cg.EngineJob('a', 'b', 'steve', EngineContext(), _job=qjob) @@ -633,7 +643,8 @@ def test_calibration_results_not_a_calibration(get_job_results): @mock.patch('cirq_google.engine.engine_client.EngineClient.get_job_results') def test_results_len(get_job_results): qjob = quantum.QuantumJob( - execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.SUCCESS) + execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.SUCCESS), + update_time=UPDATE_TIME, ) get_job_results.return_value = RESULTS @@ -645,7 +656,8 @@ def test_results_len(get_job_results): @mock.patch('time.sleep', return_value=None) def test_timeout(patched_time_sleep, get_job): qjob = quantum.QuantumJob( - execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.RUNNING) + execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.RUNNING), + update_time=UPDATE_TIME, ) get_job.return_value = qjob job = cg.EngineJob('a', 'b', 'steve', EngineContext(timeout=500)) diff --git a/cirq-google/cirq_google/engine/engine_processor_test.py b/cirq-google/cirq_google/engine/engine_processor_test.py index c937ff55a6a..9cd45334222 100644 --- a/cirq-google/cirq_google/engine/engine_processor_test.py +++ b/cirq-google/cirq_google/engine/engine_processor_test.py @@ -835,7 +835,9 @@ def test_run_sweep_params(client): name='projects/proj/programs/prog/jobs/job-id', execution_status={'state': 'READY'} ), ) - client().get_job.return_value = quantum.QuantumJob(execution_status={'state': 'SUCCESS'}) + client().get_job.return_value = quantum.QuantumJob( + execution_status={'state': 'SUCCESS'}, update_time=_to_timestamp('2019-07-09T23:39:59Z') + ) client().get_job_results.return_value = quantum.QuantumResult(result=util.pack_any(_RESULTS_V2)) processor = cg.EngineProcessor('a', 'p', EngineContext()) @@ -848,6 +850,10 @@ def test_run_sweep_params(client): assert results[i].repetitions == 1 assert results[i].params.param_dict == {'a': v} assert results[i].measurements == {'q': np.array([[0]], dtype='uint8')} + for result in results: + assert result.job_id == job.id() + assert result.job_finished_time is not None + assert results == cirq.read_json(json_text=cirq.to_json(results)) client().create_program.assert_called_once() client().create_job.assert_called_once() @@ -875,7 +881,9 @@ def test_run_batch(client): name='projects/proj/programs/prog/jobs/job-id', execution_status={'state': 'READY'} ), ) - client().get_job.return_value = quantum.QuantumJob(execution_status={'state': 'SUCCESS'}) + client().get_job.return_value = quantum.QuantumJob( + execution_status={'state': 'SUCCESS'}, update_time=_to_timestamp('2019-07-09T23:39:59Z') + ) client().get_job_results.return_value = quantum.QuantumResult(result=_BATCH_RESULTS_V2) processor = cg.EngineProcessor('a', 'p', EngineContext()) @@ -890,6 +898,8 @@ def test_run_batch(client): assert results[i].repetitions == 1 assert results[i].params.param_dict == {'a': v} assert results[i].measurements == {'q': np.array([[0]], dtype='uint8')} + for result in results: + assert result.job_id == job.id() client().create_program.assert_called_once() client().create_job.assert_called_once() run_context = v2.batch_pb2.BatchRunContext() @@ -965,7 +975,9 @@ def test_sampler(client): name='projects/proj/programs/prog/jobs/job-id', execution_status={'state': 'READY'} ), ) - client().get_job.return_value = quantum.QuantumJob(execution_status={'state': 'SUCCESS'}) + client().get_job.return_value = quantum.QuantumJob( + execution_status={'state': 'SUCCESS'}, update_time=_to_timestamp('2019-07-09T23:39:59Z') + ) client().get_job_results.return_value = quantum.QuantumResult(result=util.pack_any(_RESULTS_V2)) processor = cg.EngineProcessor('proj', 'mysim', EngineContext()) sampler = processor.get_sampler() diff --git a/cirq-google/cirq_google/engine/engine_program_test.py b/cirq-google/cirq_google/engine/engine_program_test.py index bd557268847..a232df279c3 100644 --- a/cirq-google/cirq_google/engine/engine_program_test.py +++ b/cirq-google/cirq_google/engine/engine_program_test.py @@ -245,11 +245,13 @@ def test_run_in_batch_mode(): @mock.patch('cirq_google.engine.engine_client.EngineClient.get_job_results') @mock.patch('cirq_google.engine.engine_client.EngineClient.create_job') def test_run_delegation(create_job, get_results): + dt = datetime.datetime.now(tz=datetime.timezone.utc) create_job.return_value = ( 'steve', quantum.QuantumJob( name='projects/a/programs/b/jobs/steve', execution_status=quantum.ExecutionStatus(state=quantum.ExecutionStatus.State.SUCCESS), + update_time=dt, ), ) get_results.return_value = quantum.QuantumResult( @@ -287,9 +289,11 @@ def test_run_delegation(create_job, get_results): job_id='steve', repetitions=10, param_resolver=param_resolver, processor_ids=['mine'] ) - assert results == cirq.ResultDict( + assert results == cg.EngineResult( params=cirq.ParamResolver({'a': 1.0}), measurements={'q': np.array([[False], [True], [True], [False]], dtype=bool)}, + job_id='steve', + job_finished_time=dt, ) diff --git a/cirq-google/cirq_google/engine/engine_result.py b/cirq-google/cirq_google/engine/engine_result.py new file mode 100644 index 00000000000..9a4e41fd290 --- /dev/null +++ b/cirq-google/cirq_google/engine/engine_result.py @@ -0,0 +1,113 @@ +# Copyright 2022 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import datetime +from typing import Optional, Mapping, TYPE_CHECKING, Any, Dict + +import numpy as np + +from cirq import study + +if TYPE_CHECKING: + import cirq + + +class EngineResult(study.ResultDict): + """A ResultDict with additional job metadata. + + Please see the documentation for `cirq.ResultDict` for more information. + + Additional Attributes: + job_id: A string job identifier. + job_finished_time: A timestamp for when the job finished. + """ + + def __init__( + self, + *, # Forces keyword args. + job_id: str, + job_finished_time: datetime.datetime, + params: Optional[study.ParamResolver] = None, + measurements: Optional[Mapping[str, np.ndarray]] = None, + records: Optional[Mapping[str, np.ndarray]] = None, + ): + """Initialize the result. + + Args: + job_id: A string job identifier. + job_finished_time: A timestamp for when the job finished; will be converted to UTC. + params: A ParamResolver of settings used for this result. + measurements: A dictionary from measurement gate key to measurement + results. See `cirq.ResultDict`. + records: A dictionary from measurement gate key to measurement + results. See `cirq.ResultDict`. + """ + super().__init__(params=params, measurements=measurements, records=records) + self.job_id = job_id + self.job_finished_time = job_finished_time + + @classmethod + def from_result( + cls, result: 'cirq.Result', *, job_id: str, job_finished_time: datetime.datetime + ): + if isinstance(result, study.ResultDict): + # optimize by using private methods + return cls( + params=result._params, + measurements=result._measurements, + records=result._records, + job_id=job_id, + job_finished_time=job_finished_time, + ) + else: + return cls( + params=result.params, + measurements=result.measurements, + records=result.records, + job_id=job_id, + job_finished_time=job_finished_time, + ) + + def __eq__(self, other): + if not isinstance(other, EngineResult): + return False + + return ( + super().__eq__(other) + and self.job_id == other.job_id + and self.job_finished_time == other.job_finished_time + ) + + def __repr__(self) -> str: + return ( + f'cirq_google.EngineResult(params={self.params!r}, ' + f'records={self._record_dict_repr()}, ' + f'job_id={self.job_id!r}, ' + f'job_finished_time={self.job_finished_time!r})' + ) + + @classmethod + def _json_namespace_(cls) -> str: + return 'cirq.google' + + def _json_dict_(self) -> Dict[str, Any]: + d = super()._json_dict_() + d['job_id'] = self.job_id + d['job_finished_time'] = self.job_finished_time + return d + + @classmethod + def _from_json_dict_(cls, params, records, job_id, job_finished_time, **kwargs): + return cls._from_packed_records( + params=params, records=records, job_id=job_id, job_finished_time=job_finished_time + ) diff --git a/cirq-google/cirq_google/engine/engine_result_test.py b/cirq-google/cirq_google/engine/engine_result_test.py new file mode 100644 index 00000000000..b836b8fa588 --- /dev/null +++ b/cirq-google/cirq_google/engine/engine_result_test.py @@ -0,0 +1,111 @@ +# Copyright 2022 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime +from typing import Mapping + +import pandas as pd + +import cirq +import cirq_google as cg +import numpy as np + +_DT = datetime.datetime(2022, 4, 1, 1, 23, 45, tzinfo=datetime.timezone.utc) + + +def test_engine_result(): + res = cg.EngineResult( + job_id='my_job_id', + job_finished_time=_DT, + params=None, + measurements={'a': np.array([[0, 0], [1, 1]]), 'b': np.array([[0, 0, 0], [1, 1, 1]])}, + ) + + assert res.job_id == 'my_job_id' + assert res.job_finished_time <= datetime.datetime.now(tz=datetime.timezone.utc) + assert res.measurements['a'].shape == (2, 2) + + cirq.testing.assert_equivalent_repr(res, global_vals={'cirq_google': cg}) + + +def test_engine_result_from_result_dict(): + res = cg.EngineResult( + job_id='my_job_id', + job_finished_time=_DT, + params=None, + measurements={'a': np.array([[0, 0], [1, 1]]), 'b': np.array([[0, 0, 0], [1, 1, 1]])}, + ) + + res2 = cirq.ResultDict( + params=None, + measurements={'a': np.array([[0, 0], [1, 1]]), 'b': np.array([[0, 0, 0], [1, 1, 1]])}, + ) + assert res2 != res + assert res != res2 + assert res == cg.EngineResult.from_result(res2, job_id='my_job_id', job_finished_time=_DT) + + +def test_engine_result_eq(): + res1 = cg.EngineResult( + job_id='my_job_id', + job_finished_time=_DT, + params=None, + measurements={'a': np.array([[0, 0], [1, 1]]), 'b': np.array([[0, 0, 0], [1, 1, 1]])}, + ) + res2 = cg.EngineResult( + job_id='my_job_id', + job_finished_time=_DT, + params=None, + measurements={'a': np.array([[0, 0], [1, 1]]), 'b': np.array([[0, 0, 0], [1, 1, 1]])}, + ) + assert res1 == res2 + + res3 = cg.EngineResult( + job_id='my_other_job_id', + job_finished_time=_DT, + params=None, + measurements={'a': np.array([[0, 0], [1, 1]]), 'b': np.array([[0, 0, 0], [1, 1, 1]])}, + ) + assert res1 != res3 + + +class MyResult(cirq.Result): + @property + def params(self) -> 'cirq.ParamResolver': + return cirq.ParamResolver() + + @property + def measurements(self) -> Mapping[str, np.ndarray]: + return {'a': np.array([[0, 0], [1, 1]]), 'b': np.array([[0, 0, 0], [1, 1, 1]])} + + @property + def records(self) -> Mapping[str, np.ndarray]: + return {k: v[:, np.newaxis, :] for k, v in self.measurements.items()} + + @property + def data(self) -> pd.DataFrame: + # coverage: ignore + return cirq.Result.dataframe_from_measurements(self.measurements) + + +def test_engine_result_from_result(): + res = cg.EngineResult( + job_id='my_job_id', + job_finished_time=_DT, + params=None, + measurements={'a': np.array([[0, 0], [1, 1]]), 'b': np.array([[0, 0, 0], [1, 1, 1]])}, + ) + + res2 = MyResult() + assert res == cg.EngineResult.from_result(res2, job_id='my_job_id', job_finished_time=_DT) diff --git a/cirq-google/cirq_google/engine/engine_test.py b/cirq-google/cirq_google/engine/engine_test.py index c6ed38871f5..6ca9c203955 100644 --- a/cirq-google/cirq_google/engine/engine_test.py +++ b/cirq-google/cirq_google/engine/engine_test.py @@ -13,6 +13,7 @@ # limitations under the License. """Tests for engine.""" +import datetime from unittest import mock import time import numpy as np @@ -333,6 +334,9 @@ def test_engine_str(): assert str(engine) == 'Engine(project_id=\'proj\')' +_DT = datetime.datetime.now(tz=datetime.timezone.utc) + + def setup_run_circuit_with_result_(client, result): client().create_program.return_value = ( 'prog', @@ -344,7 +348,9 @@ def setup_run_circuit_with_result_(client, result): name='projects/proj/programs/prog/jobs/job-id', execution_status={'state': 'READY'} ), ) - client().get_job.return_value = quantum.QuantumJob(execution_status={'state': 'SUCCESS'}) + client().get_job.return_value = quantum.QuantumJob( + execution_status={'state': 'SUCCESS'}, update_time=_DT + ) client().get_job_results.return_value = quantum.QuantumResult(result=result) @@ -375,6 +381,7 @@ def test_run_circuit(client): parameter_sweeps=[v2.run_context_pb2.ParameterSweep(repetitions=1)] ) ), + update_time=_DT, ), False, ) diff --git a/cirq-google/cirq_google/engine/simulated_local_job.py b/cirq-google/cirq_google/engine/simulated_local_job.py index 4bb30b62abe..fa671cd03c9 100644 --- a/cirq-google/cirq_google/engine/simulated_local_job.py +++ b/cirq-google/cirq_google/engine/simulated_local_job.py @@ -13,6 +13,7 @@ # limitations under the License. """An implementation of AbstractJob that uses in-memory constructs and a provided sampler to execute circuits.""" +import datetime from typing import cast, List, Optional, Sequence, Tuple import concurrent.futures @@ -22,12 +23,33 @@ from cirq_google.engine.calibration_result import CalibrationResult from cirq_google.engine.abstract_local_job import AbstractLocalJob from cirq_google.engine.local_simulation_type import LocalSimulationType +from cirq_google.engine.engine_result import EngineResult -def _flatten_results(batch_results: Sequence[Sequence[cirq.Result]]): +def _flatten_results(batch_results: Sequence[Sequence[EngineResult]]) -> List[EngineResult]: return [result for batch in batch_results for result in batch] +def _to_engine_results( + batch_results: Sequence[Sequence['cirq.Result']], + *, + job_id: str, + job_finished_time: datetime.datetime = None, +) -> List[List[EngineResult]]: + """Convert cirq.Result from simulators into (simulated) EngineResults.""" + + if job_finished_time is None: + job_finished_time = datetime.datetime.now(tz=datetime.timezone.utc) + + return [ + [ + EngineResult.from_result(result, job_id=job_id, job_finished_time=job_finished_time) + for result in batch + ] + for batch in batch_results + ] + + class SimulatedLocalJob(AbstractLocalJob): """A quantum job backed by a (local) sampler. @@ -91,7 +113,7 @@ def delete(self) -> None: self.program().delete_job(self.id()) self._state = quantum.ExecutionStatus.State.STATE_UNSPECIFIED - def batched_results(self) -> Sequence[Sequence[cirq.Result]]: + def batched_results(self) -> Sequence[Sequence[EngineResult]]: """Returns the job results, blocking until the job is complete. This method is intended for batched jobs. Instead of flattening @@ -105,7 +127,7 @@ def batched_results(self) -> Sequence[Sequence[cirq.Result]]: else: raise ValueError('Unsupported simulation type {self._type}') - def _execute_results(self) -> Sequence[Sequence[cirq.Result]]: + def _execute_results(self) -> Sequence[Sequence[EngineResult]]: """Executes the circuit and sweeps on the sampler. For synchronous execution, this is called when the results() @@ -124,15 +146,16 @@ def _execute_results(self) -> Sequence[Sequence[cirq.Result]]: batch_results = self._sampler.run_batch( programs=programs, params_list=cast(List[cirq.Sweepable], sweeps), repetitions=reps ) + batch_engine_results = _to_engine_results(batch_results, job_id=self.id()) self._state = quantum.ExecutionStatus.State.SUCCESS - return batch_results + return batch_engine_results except Exception as e: self._failure_code = '500' self._failure_message = str(e) self._state = quantum.ExecutionStatus.State.FAILURE raise e - def results(self) -> Sequence[cirq.Result]: + def results(self) -> Sequence[EngineResult]: """Returns the job results, blocking until the job is complete.""" if self._type == LocalSimulationType.SYNCHRONOUS: return _flatten_results(self._execute_results()) diff --git a/cirq-google/cirq_google/engine/simulated_local_job_test.py b/cirq-google/cirq_google/engine/simulated_local_job_test.py index 0fcd290c79b..89a6fd4b35c 100644 --- a/cirq-google/cirq_google/engine/simulated_local_job_test.py +++ b/cirq-google/cirq_google/engine/simulated_local_job_test.py @@ -107,6 +107,11 @@ def test_run_batch(simulation_type): assert np.all(results[2].measurements['m2'] == 0) assert np.all(results[3].measurements['m2'] == 1) + for result in results: + assert result.job_id == 'test_job' + assert result.job_finished_time is not None + assert results == cirq.read_json(json_text=cirq.to_json(results)) + def test_cancel(): program = ParentProgram([cirq.Circuit(cirq.X(Q), cirq.measure(Q, key='m'))], None) diff --git a/cirq-google/cirq_google/json_resolver_cache.py b/cirq-google/cirq_google/json_resolver_cache.py index 56494f2ef55..8a3a138f8e7 100644 --- a/cirq-google/cirq_google/json_resolver_cache.py +++ b/cirq-google/cirq_google/json_resolver_cache.py @@ -64,4 +64,5 @@ def _class_resolver_dictionary() -> Dict[str, ObjectFactory]: 'cirq.google.SimulatedProcessorWithLocalDeviceRecord': cirq_google.SimulatedProcessorWithLocalDeviceRecord, 'cirq.google.HardcodedQubitPlacer': cirq_google.HardcodedQubitPlacer, # pylint: enable=line-too-long + 'cirq.google.EngineResult': cirq_google.EngineResult, } diff --git a/cirq-google/cirq_google/json_test_data/cirq.google.EngineResult.json b/cirq-google/cirq_google/json_test_data/cirq.google.EngineResult.json new file mode 100644 index 00000000000..8b079d15c82 --- /dev/null +++ b/cirq-google/cirq_google/json_test_data/cirq.google.EngineResult.json @@ -0,0 +1,32 @@ +{ + "cirq_type": "cirq.google.EngineResult", + "params": { + "cirq_type": "ParamResolver", + "param_dict": [ + [ + { + "cirq_type": "sympy.Symbol", + "name": "a" + }, + 0.5 + ] + ] + }, + "records": { + "m": { + "packed_digits": "d32a", + "binary": true, + "dtype": "bool", + "shape": [ + 3, + 1, + 5 + ] + } + }, + "job_id": "my_job_id", + "job_finished_time": { + "cirq_type": "datetime.datetime", + "timestamp": 1648801425.0 + } +} \ No newline at end of file diff --git a/cirq-google/cirq_google/json_test_data/cirq.google.EngineResult.repr b/cirq-google/cirq_google/json_test_data/cirq.google.EngineResult.repr new file mode 100644 index 00000000000..f368f177917 --- /dev/null +++ b/cirq-google/cirq_google/json_test_data/cirq.google.EngineResult.repr @@ -0,0 +1 @@ +cirq_google.EngineResult(params=cirq.ParamResolver({sympy.Symbol('a'): 0.5}), records={'m': np.array([[[True, True, False, True, False]], [[False, True, True, False, False]], [[True, False, True, False, True]]], dtype=np.bool)}, job_id='my_job_id', job_finished_time=datetime.datetime(2022, 4, 1, 8, 23, 45, tzinfo=datetime.timezone.utc)) diff --git a/cirq-google/cirq_google/json_test_data/spec.py b/cirq-google/cirq_google/json_test_data/spec.py index 1fe671e4ae9..57e83e43a44 100644 --- a/cirq-google/cirq_google/json_test_data/spec.py +++ b/cirq-google/cirq_google/json_test_data/spec.py @@ -76,6 +76,7 @@ 'EngineProcessorRecord', 'SimulatedProcessorRecord', 'SimulatedProcessorWithLocalDeviceRecord', + 'EngineResult', ] }, resolver_cache=_class_resolver_dictionary(),