diff --git a/cirq-google/cirq_google/cloud/quantum_v1alpha1/types/quantum.py b/cirq-google/cirq_google/cloud/quantum_v1alpha1/types/quantum.py index 517e07e9f4f..418c20931e9 100644 --- a/cirq-google/cirq_google/cloud/quantum_v1alpha1/types/quantum.py +++ b/cirq-google/cirq_google/cloud/quantum_v1alpha1/types/quantum.py @@ -156,15 +156,48 @@ class QuantumJob(proto.Message): class DeviceConfigSelector(proto.Message): r"""- + This message has `oneof`_ fields (mutually exclusive fields). + For each oneof, at most one member field can be set at the same time. + Setting any member of the oneof automatically clears all other + members. + .. _oneof: https://proto-plus-python.readthedocs.io/en/stable/fields.html#oneofs-mutually-exclusive-fields Attributes: run_name (str): - + This field is a member of `oneof`_ ``top_level_identifier``. + snapshot_id (str): + - + This field is a member of `oneof`_ ``top_level_identifier``. config_alias (str): - """ - run_name = proto.Field(proto.STRING, number=1) - config_alias = proto.Field(proto.STRING, number=2) + run_name: str = proto.Field(proto.STRING, number=1, oneof='top_level_identifier') + snapshot_id: str = proto.Field(proto.STRING, number=3, oneof='top_level_identifier') + config_alias: str = proto.Field(proto.STRING, number=2) + + +class DeviceConfigKey(proto.Message): + r"""- + This message has `oneof`_ fields (mutually exclusive fields). + For each oneof, at most one member field can be set at the same time. + Setting any member of the oneof automatically clears all other + members. + .. _oneof: https://proto-plus-python.readthedocs.io/en/stable/fields.html#oneofs-mutually-exclusive-fields + Attributes: + run (str): + - + This field is a member of `oneof`_ ``top_level_identifier``. + snapshot_id (str): + - + This field is a member of `oneof`_ ``top_level_identifier``. + config_alias (str): + - + """ + + run: str = proto.Field(proto.STRING, number=1, oneof='top_level_identifier') + snapshot_id: str = proto.Field(proto.STRING, number=3, oneof='top_level_identifier') + config_alias: str = proto.Field(proto.STRING, number=2) class SchedulingConfig(proto.Message): @@ -611,18 +644,4 @@ class QuantumReservation(proto.Message): whitelisted_users = proto.RepeatedField(proto.STRING, number=5) -class DeviceConfigKey(proto.Message): - r"""- - - Attributes: - run (str): - - - config_alias (str): - - - """ - - run = proto.Field(proto.STRING, number=1) - config_alias = proto.Field(proto.STRING, number=2) - - __all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/cirq-google/cirq_google/engine/abstract_processor.py b/cirq-google/cirq_google/engine/abstract_processor.py index c39c3660930..a5307cd292c 100644 --- a/cirq-google/cirq_google/engine/abstract_processor.py +++ b/cirq-google/cirq_google/engine/abstract_processor.py @@ -57,8 +57,9 @@ async def run_async( self, program: cirq.Circuit, *, - run_name: str, device_config_name: str, + run_name: str = "", + snapshot_id: str = "", program_id: Optional[str] = None, job_id: Optional[str] = None, param_resolver: Optional[cirq.ParamResolver] = None, @@ -75,7 +76,12 @@ async def run_async( provided, a moment by moment schedule will be used. run_name: A unique identifier representing an automation run for the processor. An Automation Run contains a collection of device - configurations for the processor. + configurations for the processor. `snapshot_id` and `run_name` + should not both be set. Choose one. + snapshot_id: A unique identifier for an immutable snapshot reference. + A snapshot contains a collection of device configurations for the + processor. `snapshot_id` and `run_name` should not both be set. + Choose one. device_config_name: An identifier used to select the processor configuration utilized to run the job. A configuration identifies the set of available qubits, couplers, and supported gates in the processor. @@ -109,6 +115,7 @@ async def run_async( job_description=job_description, job_labels=job_labels, run_name=run_name, + snapshot_id=snapshot_id, device_config_name=device_config_name, ) return job.results()[0] @@ -120,8 +127,9 @@ async def run_sweep_async( self, program: cirq.AbstractCircuit, *, - run_name: str, device_config_name: str, + run_name: str = "", + snapshot_id: str = "", program_id: Optional[str] = None, job_id: Optional[str] = None, params: cirq.Sweepable = None, @@ -144,6 +152,9 @@ async def run_sweep_async( device_config_name: An identifier used to select the processor configuration utilized to run the job. A configuration identifies the set of available qubits, couplers, and supported gates in the processor. + snapshot_id: A unique identifier for an immutable snapshot reference. + A snapshot contains a collection of device configurations for the + processor. Both `snapshot_id` and `run_name` should not be set. program_id: A user-provided identifier for the program. This must be unique within the Google Cloud project being used. If this parameter is not provided, a random id of the format diff --git a/cirq-google/cirq_google/engine/engine.py b/cirq-google/cirq_google/engine/engine.py index e809dc9dcbf..f196f1a0534 100644 --- a/cirq-google/cirq_google/engine/engine.py +++ b/cirq-google/cirq_google/engine/engine.py @@ -223,6 +223,7 @@ def run( job_labels: Optional[Dict[str, str]] = None, *, run_name: str = "", + snapshot_id: str = "", device_config_name: str = "", ) -> cirq.Result: """Runs the supplied Circuit via Quantum Engine. @@ -250,6 +251,9 @@ def run( specified processor. An Automation Run contains a collection of device configurations for a processor. If specified, `processor_id` is required to be set. + snapshot_id: A unique identifier for an immutable snapshot reference. + A snapshot contains a collection of device configurations for the + processor. device_config_name: An identifier used to select the processor configuration utilized to run the job. A configuration identifies the set of available qubits, couplers, and supported gates in the processor. @@ -277,6 +281,7 @@ def run( job_description=job_description, job_labels=job_labels, run_name=run_name, + snapshot_id=snapshot_id, device_config_name=device_config_name, ) )[0] @@ -295,6 +300,7 @@ async def run_sweep_async( job_labels: Optional[Dict[str, str]] = None, *, run_name: str = "", + snapshot_id: str = "", device_config_name: str = "", ) -> engine_job.EngineJob: """Runs the supplied Circuit via Quantum Engine. @@ -325,6 +331,9 @@ async def run_sweep_async( specified processor. An Automation Run contains a collection of device configurations for a processor. If specified, `processor_id` is required to be set. + snapshot_id: A unique identifier for an immutable snapshot reference. + A snapshot contains a collection of device configurations for the + processor. device_config_name: An identifier used to select the processor configuration utilized to run the job. A configuration identifies the set of available qubits, couplers, and supported gates in the processor. @@ -360,6 +369,7 @@ async def run_sweep_async( job_labels=job_labels, processor_id=processor_id, run_name=run_name, + snapshot_id=snapshot_id, device_config_name=device_config_name, ) return engine_job.EngineJob( @@ -381,6 +391,7 @@ async def run_sweep_async( description=job_description, labels=job_labels, run_name=run_name, + snapshot_id=snapshot_id, device_config_name=device_config_name, ) diff --git a/cirq-google/cirq_google/engine/engine_client.py b/cirq-google/cirq_google/engine/engine_client.py index 4fbbc19764e..8290a187a64 100644 --- a/cirq-google/cirq_google/engine/engine_client.py +++ b/cirq-google/cirq_google/engine/engine_client.py @@ -388,6 +388,7 @@ async def create_job_async( labels: Optional[Dict[str, str]] = None, *, run_name: str = "", + snapshot_id: str = "", device_config_name: str = "", ) -> Tuple[str, quantum.QuantumJob]: """Creates and runs a job on Quantum Engine. @@ -409,6 +410,9 @@ async def create_job_async( specified processor. An Automation Run contains a collection of device configurations for a processor. If specified, `processor_id` is required to be set. + snapshot_id: A unique identifier for an immutable snapshot reference. + A snapshot contains a collection of device configurations for the + processor. device_config_name: An identifier used to select the processor configuration utilized to run the job. A configuration identifies the set of available qubits, couplers, and supported gates in the processor. @@ -421,25 +425,37 @@ async def create_job_async( ValueError: If only one of `run_name` and `device_config_name` are specified. ValueError: If either `run_name` and `device_config_name` are set but `processor_id` is empty. + ValueError: If both `run_name` and `snapshot_id` are specified. """ # Check program to run and program parameters. if priority and not 0 <= priority < 1000: raise ValueError('priority must be between 0 and 1000') if not processor_id: raise ValueError('Must specify a processor id when creating a job.') - if bool(run_name) ^ bool(device_config_name): - raise ValueError('Cannot specify only one of `run_name` and `device_config_name`') + if run_name and snapshot_id: + raise ValueError('Cannot specify both `run_name` and `snapshot_id`') + if (bool(run_name) or bool(snapshot_id)) ^ bool(device_config_name): + raise ValueError( + 'Cannot specify only one of top level identifier (e.g `run_name`, `snapshot_id`)' + ' and `device_config_name`' + ) # Create job. + if snapshot_id: + selector = quantum.DeviceConfigSelector( + snapshot_id=snapshot_id or None, config_alias=device_config_name + ) + else: + selector = quantum.DeviceConfigSelector( + run_name=run_name or None, config_alias=device_config_name + ) job_name = _job_name_from_ids(project_id, program_id, job_id) if job_id else '' job = quantum.QuantumJob( name=job_name, scheduling_config=quantum.SchedulingConfig( processor_selector=quantum.SchedulingConfig.ProcessorSelector( processor=_processor_name_from_ids(project_id, processor_id), - device_config_selector=quantum.DeviceConfigSelector( - run_name=run_name, config_alias=device_config_name - ), + device_config_selector=selector, ) ), run_context=run_context, @@ -737,6 +753,7 @@ def run_job_over_stream( job_labels: Optional[Dict[str, str]] = None, processor_id: str = "", run_name: str = "", + snapshot_id: str = "", device_config_name: str = "", ) -> duet.AwaitableFuture[Union[quantum.QuantumResult, quantum.QuantumJob]]: """Runs a job with the given program and job information over a stream. @@ -761,6 +778,9 @@ def run_job_over_stream( specified processor. An Automation Run contains a collection of device configurations for a processor. If specified, `processor_id` is required to be set. + snapshot_id: A unique identifier for an immutable snapshot reference. + A snapshot contains a collection of device configurations for the + processor. device_config_name: An identifier used to select the processor configuration utilized to run the job. A configuration identifies the set of available qubits, couplers, and supported gates in the processor. @@ -773,14 +793,19 @@ def run_job_over_stream( ValueError: If the priority is not between 0 and 1000. ValueError: If `processor_id` is not set. ValueError: If only one of `run_name` and `device_config_name` are specified. + ValueError: If both `run_name` and `snapshot_id` are specified. """ # Check program to run and program parameters. if priority and not 0 <= priority < 1000: raise ValueError('priority must be between 0 and 1000') if not processor_id: raise ValueError('Must specify a processor id when creating a job.') - if bool(run_name) ^ bool(device_config_name): - raise ValueError('Cannot specify only one of `run_name` and `device_config_name`') + if run_name and snapshot_id: + raise ValueError('Cannot specify both `run_name` and `snapshot_id`') + if (bool(run_name) or bool(snapshot_id)) ^ bool(device_config_name): + raise ValueError( + 'Cannot specify only one of top level identifier and `device_config_name`' + ) project_name = _project_name(project_id) @@ -791,14 +816,21 @@ def run_job_over_stream( if program_labels: program.labels.update(program_labels) + if snapshot_id: + selector = quantum.DeviceConfigSelector( + snapshot_id=snapshot_id or None, config_alias=device_config_name + ) + else: + selector = quantum.DeviceConfigSelector( + run_name=run_name or None, config_alias=device_config_name + ) + job = quantum.QuantumJob( name=_job_name_from_ids(project_id, program_id, job_id), scheduling_config=quantum.SchedulingConfig( processor_selector=quantum.SchedulingConfig.ProcessorSelector( processor=_processor_name_from_ids(project_id, processor_id), - device_config_selector=quantum.DeviceConfigSelector( - run_name=run_name, config_alias=device_config_name - ), + device_config_selector=selector, ) ), run_context=run_context, diff --git a/cirq-google/cirq_google/engine/engine_client_test.py b/cirq-google/cirq_google/engine/engine_client_test.py index 744ffb84f19..7bda6cbac5b 100644 --- a/cirq-google/cirq_google/engine/engine_client_test.py +++ b/cirq-google/cirq_google/engine/engine_client_test.py @@ -373,9 +373,7 @@ def test_create_job(client_constructor): priority=10, processor_selector=quantum.SchedulingConfig.ProcessorSelector( processor='projects/proj/processors/processor0', - device_config_selector=quantum.DeviceConfigSelector( - run_name="", config_alias="" - ), + device_config_selector=quantum.DeviceConfigSelector(), ), ), description='A job', @@ -398,9 +396,7 @@ def test_create_job(client_constructor): priority=10, processor_selector=quantum.SchedulingConfig.ProcessorSelector( processor='projects/proj/processors/processor0', - device_config_selector=quantum.DeviceConfigSelector( - run_name="", config_alias="" - ), + device_config_selector=quantum.DeviceConfigSelector(), ), ), description='A job', @@ -421,9 +417,7 @@ def test_create_job(client_constructor): priority=10, processor_selector=quantum.SchedulingConfig.ProcessorSelector( processor='projects/proj/processors/processor0', - device_config_selector=quantum.DeviceConfigSelector( - run_name="", config_alias="" - ), + device_config_selector=quantum.DeviceConfigSelector(), ), ), labels=labels, @@ -445,9 +439,7 @@ def test_create_job(client_constructor): priority=10, processor_selector=quantum.SchedulingConfig.ProcessorSelector( processor='projects/proj/processors/processor0', - device_config_selector=quantum.DeviceConfigSelector( - run_name="", config_alias="" - ), + device_config_selector=quantum.DeviceConfigSelector(), ), ), ), @@ -466,9 +458,7 @@ def test_create_job(client_constructor): priority=10, processor_selector=quantum.SchedulingConfig.ProcessorSelector( processor='projects/proj/processors/processor0', - device_config_selector=quantum.DeviceConfigSelector( - run_name="", config_alias="" - ), + device_config_selector=quantum.DeviceConfigSelector(), ), ), ), @@ -489,25 +479,22 @@ def test_create_job(client_constructor): @mock.patch.dict(os.environ, clear='CIRQ_TESTING') @mock.patch.object(quantum, 'QuantumEngineServiceAsyncClient', autospec=True) @pytest.mark.parametrize( - 'processor_id, run_name, device_config_name, error_message', + 'processor_id, run_name, snapshot_id, device_config_name, error_message', [ - ('', '', '', 'Must specify a processor id when creating a job.'), + ('', '', '', '', 'Must specify a processor id when creating a job.'), + ('processor0', 'RUN_NAME', '', '', 'Cannot specify only one of top level identifier'), + ('processor0', '', '', 'CONFIG_ALIAS', 'Cannot specify only one of top level identifier'), ( 'processor0', - 'RUN_NAME', - '', - 'Cannot specify only one of `run_name` and `device_config_name`', - ), - ( - 'processor0', - '', + 'run_name', + 'snapshot_id', 'CONFIG_ALIAS', - 'Cannot specify only one of `run_name` and `device_config_name`', + 'Cannot specify both `run_name` and `snapshot_id`', ), ], ) def test_create_job_with_invalid_processor_and_device_config_arguments_throws( - client_constructor, processor_id, run_name, device_config_name, error_message + client_constructor, processor_id, run_name, snapshot_id, device_config_name, error_message ): grpc_client = _setup_client_mock(client_constructor) result = quantum.QuantumJob(name='projects/proj/programs/prog/jobs/job0') @@ -521,16 +508,19 @@ def test_create_job_with_invalid_processor_and_device_config_arguments_throws( job_id=None, processor_id=processor_id, run_name=run_name, + snapshot_id=snapshot_id, device_config_name=device_config_name, ) @mock.patch.dict(os.environ, clear='CIRQ_TESTING') @mock.patch.object(quantum, 'QuantumEngineServiceAsyncClient', autospec=True) -@pytest.mark.parametrize('processor_id', [('processor0'), ('processor0')]) -@pytest.mark.parametrize('run_name, device_config_name', [('RUN_NAME', 'CONFIG_NAME'), ('', '')]) -def test_create_job_with_run_name_and_device_config_name( - client_constructor, processor_id, run_name, device_config_name +@pytest.mark.parametrize( + 'run_name, snapshot_id, device_config_name', + [('RUN_NAME', '', 'CONFIG_NAME'), ('', '', ''), ('', '', '')], +) +def test_create_job_with_run_name_and_device_config_name_succeeds( + client_constructor, run_name, snapshot_id, device_config_name ): grpc_client = _setup_client_mock(client_constructor) result = quantum.QuantumJob(name='projects/proj/programs/prog/jobs/job0') @@ -542,8 +532,9 @@ def test_create_job_with_run_name_and_device_config_name( project_id='proj', program_id='prog', job_id='job0', - processor_id=processor_id, + processor_id="processor0", run_name=run_name, + snapshot_id=snapshot_id, device_config_name=device_config_name, run_context=run_context, priority=10, @@ -559,7 +550,7 @@ def test_create_job_with_run_name_and_device_config_name( processor_selector=quantum.SchedulingConfig.ProcessorSelector( processor='projects/proj/processors/processor0', device_config_selector=quantum.DeviceConfigSelector( - run_name=run_name, config_alias=device_config_name + run_name=run_name or None, config_alias=device_config_name ), ), ), @@ -568,6 +559,39 @@ def test_create_job_with_run_name_and_device_config_name( ) +@mock.patch.dict(os.environ, clear='CIRQ_TESTING') +@mock.patch.object(quantum, 'QuantumEngineServiceAsyncClient', autospec=True) +def test_create_job_with_snapshot_id_and_config_successfully_passes_device_config_selector( + client_constructor, +): + grpc_client = _setup_client_mock(client_constructor) + result = quantum.QuantumJob(name='projects/proj/programs/prog/jobs/job0') + grpc_client.create_quantum_job.return_value = result + run_context = any_pb2.Any() + processor_id = "processor0" + snapshot_id = "SNAPSHOT_ID" + device_config_name = "DEVICE_CONFIG_NAME" + client = EngineClient() + + client.create_job( + project_id='proj', + program_id='prog', + job_id='job0', + processor_id=processor_id, + snapshot_id=snapshot_id, + device_config_name=device_config_name, + run_context=run_context, + priority=10, + ) + + job = grpc_client.create_quantum_job.call_args[0][0] + device_config_selector = ( + job.quantum_job.scheduling_config.processor_selector.device_config_selector + ) + assert device_config_selector.snapshot_id == snapshot_id + assert device_config_selector.config_alias == device_config_name + + @pytest.mark.parametrize( 'run_job_kwargs, expected_submit_args', [ @@ -785,9 +809,8 @@ def test_run_job_over_stream( _setup_client_mock(client_constructor) stream_manager = _setup_stream_manager_mock(manager_constructor) - result = quantum.QuantumResult(parent='projects/proj/programs/prog/jobs/job0') - expected_future = duet.AwaitableFuture() - expected_future.try_set_result(result) + parent = expected_submit_args[2].name + expected_future = duet.futuretools.completed_future(quantum.QuantumResult(parent=parent)) stream_manager.submit.return_value = expected_future client = EngineClient() @@ -797,6 +820,86 @@ def test_run_job_over_stream( stream_manager.submit.assert_called_with(*expected_submit_args) +@mock.patch.object(quantum, 'QuantumEngineServiceAsyncClient', autospec=True) +@mock.patch.object(engine_stream_manager, 'StreamManager', autospec=True) +def test_run_job_over_stream_with_snapshot_id_returns_correct_future( + manager_constructor, client_constructor +): + _setup_client_mock(client_constructor) + stream_manager = _setup_stream_manager_mock(manager_constructor) + client = EngineClient() + run_job_kwargs = ( + { + 'project_id': 'proj', + 'program_id': 'prog', + 'code': any_pb2.Any(), + 'job_id': 'job0', + 'processor_id': 'processor0', + 'run_context': any_pb2.Any(), + 'snapshot_id': 'SNAPSHOT_ID', + 'device_config_name': 'CONFIG_NAME', + }, + ) + + expected_future = duet.futuretools.completed_future( + quantum.QuantumResult(parent='projects/proj/programs/prog/jobs/job0') + ) + stream_manager.submit.return_value = expected_future + stream_manager.submit.return_value = expected_future + + actual_future = client.run_job_over_stream(**run_job_kwargs[0]) + + assert actual_future == expected_future + + +@mock.patch.object(quantum, 'QuantumEngineServiceAsyncClient', autospec=True) +@mock.patch.object(engine_stream_manager, 'StreamManager', autospec=True) +def test_run_job_over_stream_with_snapshot_id_propogates_snapshot_id( + manager_constructor, client_constructor +): + _setup_client_mock(client_constructor) + stream_manager = _setup_stream_manager_mock(manager_constructor) + client = EngineClient() + run_job_kwargs = ( + { + 'project_id': 'proj', + 'program_id': 'prog', + 'code': any_pb2.Any(), + 'job_id': 'job0', + 'processor_id': 'processor0', + 'run_context': any_pb2.Any(), + 'snapshot_id': 'SNAPSHOT_ID', + 'device_config_name': 'CONFIG_NAME', + }, + ) + expected_submit_args = ( + [ + 'projects/proj', + quantum.QuantumProgram(name='projects/proj/programs/prog', code=any_pb2.Any()), + quantum.QuantumJob( + name='projects/proj/programs/prog/jobs/job0', + run_context=any_pb2.Any(), + scheduling_config=quantum.SchedulingConfig( + processor_selector=quantum.SchedulingConfig.ProcessorSelector( + processor='projects/proj/processors/processor0', + device_config_selector=quantum.DeviceConfigSelector( + snapshot_id="SNAPSHOT_ID", config_alias="CONFIG_NAME" + ), + ) + ), + ), + ], + ) + parent = expected_submit_args[0][2].name + expected_future = duet.futuretools.completed_future(quantum.QuantumResult(parent=parent)) + stream_manager.submit.return_value = expected_future + stream_manager.submit.return_value = expected_future + + _ = client.run_job_over_stream(**run_job_kwargs[0]) + + stream_manager.submit.assert_called_with(*expected_submit_args[0]) + + def test_run_job_over_stream_with_priority_out_of_bound_raises(): client = EngineClient() @@ -826,13 +929,20 @@ def test_run_job_over_stream_processor_unset_raises(): ) -@pytest.mark.parametrize('run_name, device_config_name', [('run1', ''), ('', 'device_config1')]) -def test_run_job_over_stream_invalid_device_config_raises(run_name, device_config_name): +@pytest.mark.parametrize( + 'run_name, snapshot_id, device_config_name, error_message', + [ + ('run1', '', '', 'Cannot specify only one of top level identifier'), + ('', '', 'device_config1', 'Cannot specify only one of top level identifier'), + ('run', 'snapshot_id', 'config', 'Cannot specify both `run_name` and `snapshot_id`'), + ], +) +def test_run_job_over_stream_invalid_device_config_raises( + run_name, snapshot_id, device_config_name, error_message +): client = EngineClient() - with pytest.raises( - ValueError, match='Cannot specify only one of `run_name` and `device_config_name`' - ): + with pytest.raises(ValueError, match=error_message): client.run_job_over_stream( project_id='proj', program_id='prog', @@ -841,6 +951,7 @@ def test_run_job_over_stream_invalid_device_config_raises(run_name, device_confi processor_id='mysim', run_context=any_pb2.Any(), run_name=run_name, + snapshot_id=snapshot_id, device_config_name=device_config_name, ) diff --git a/cirq-google/cirq_google/engine/engine_processor.py b/cirq-google/cirq_google/engine/engine_processor.py index bd91c80a033..0619685c1a8 100644 --- a/cirq-google/cirq_google/engine/engine_processor.py +++ b/cirq-google/cirq_google/engine/engine_processor.py @@ -87,7 +87,7 @@ def engine(self) -> 'engine_base.Engine': return engine_base.Engine(self.project_id, context=self.context) def get_sampler( - self, run_name: str = "", device_config_name: str = "" + self, run_name: str = "", device_config_name: str = "", snapshot_id: str = "" ) -> 'cg.engine.ProcessorSampler': """Returns a sampler backed by the engine. Args: @@ -97,27 +97,45 @@ def get_sampler( device_config_name: An identifier used to select the processor configuration utilized to run the job. A configuration identifies the set of available qubits, couplers, and supported gates in the processor. + snapshot_id: A unique identifier for an immutable snapshot reference. + A snapshot contains a collection of device configurations for the + processor. Returns: A `cirq.Sampler` instance (specifically a `engine_sampler.ProcessorSampler` that will send circuits to the Quantum Computing Service when sampled. + + Raises: + ValueError: If only one of `run_name` and `device_config_name` are specified. + ValueError: If both `run_name` and `snapshot_id` are specified. + """ processor = self._inner_processor() - # If a run_name or config_alias is not provided, initialize them - # to the Processor's default values. - if not run_name and not device_config_name: + if run_name and snapshot_id: + raise ValueError('Cannot specify both `run_name` and `snapshot_id`') + if (bool(run_name) or bool(snapshot_id)) ^ bool(device_config_name): + raise ValueError( + 'Cannot specify only one of top level identifier and `device_config_name`' + ) + # If not provided, initialize the sampler with the Processor's default values. + if not run_name and not device_config_name and not snapshot_id: run_name = processor.default_device_config_key.run device_config_name = processor.default_device_config_key.config_alias + snapshot_id = processor.default_device_config_key.snapshot_id return processor_sampler.ProcessorSampler( - processor=self, run_name=run_name, device_config_name=device_config_name + processor=self, + run_name=run_name, + snapshot_id=snapshot_id, + device_config_name=device_config_name, ) async def run_sweep_async( self, program: cirq.AbstractCircuit, *, - run_name: str, device_config_name: str, + run_name: str = "", + snapshot_id: str = "", program_id: Optional[str] = None, job_id: Optional[str] = None, params: cirq.Sweepable = None, @@ -141,6 +159,9 @@ async def run_sweep_async( device_config_name: An identifier used to select the processor configuration utilized to run the job. A configuration identifies the set of available qubits, couplers, and supported gates in the processor. + snapshot_id: A unique identifier for an immutable snapshot reference. + A snapshot contains a collection of device configurations for the + processor. program_id: A user-provided identifier for the program. This must be unique within the Google Cloud project being used. If this parameter is not provided, a random id of the format @@ -178,6 +199,7 @@ async def run_sweep_async( job_labels=job_labels, processor_id=self.processor_id, run_name=run_name, + snapshot_id=snapshot_id, device_config_name=device_config_name, ) diff --git a/cirq-google/cirq_google/engine/engine_processor_test.py b/cirq-google/cirq_google/engine/engine_processor_test.py index e1684ca09e8..159edf711c7 100644 --- a/cirq-google/cirq_google/engine/engine_processor_test.py +++ b/cirq-google/cirq_google/engine/engine_processor_test.py @@ -343,8 +343,22 @@ def test_get_sampler_uses_custom_default_device_configuration_key() -> None: assert sampler.device_config_name == "config_alias1" -@pytest.mark.parametrize('run, config_alias', [('run', ''), ('', 'config')]) -def test_get_sampler_with_incomplete_device_configuration_uses_defaults(run, config_alias) -> None: +@pytest.mark.parametrize( + 'run, snapshot_id, config_alias, error_message', + [ + ('run', '', '', 'Cannot specify only one of top level identifier and `device_config_name`'), + ( + '', + '', + 'config', + 'Cannot specify only one of top level identifier and `device_config_name`', + ), + ('run', 'snapshot_id', 'config', 'Cannot specify both `run_name` and `snapshot_id`'), + ], +) +def test_get_sampler_with_incomplete_device_configuration_errors( + run, snapshot_id, config_alias, error_message +) -> None: processor = cg.EngineProcessor( 'a', 'p', @@ -356,10 +370,10 @@ def test_get_sampler_with_incomplete_device_configuration_uses_defaults(run, con ), ) - with pytest.raises( - ValueError, match='Cannot specify only one of `run_name` and `device_config_name`' - ): - processor.get_sampler(run_name=run, device_config_name=config_alias) + with pytest.raises(ValueError, match=error_message): + processor.get_sampler( + run_name=run, device_config_name=config_alias, snapshot_id=snapshot_id + ) def test_get_sampler_loads_processor_with_default_device_configuration() -> None: diff --git a/cirq-google/cirq_google/engine/engine_program.py b/cirq-google/cirq_google/engine/engine_program.py index 7789d7132e6..f35163d169f 100644 --- a/cirq-google/cirq_google/engine/engine_program.py +++ b/cirq-google/cirq_google/engine/engine_program.py @@ -64,8 +64,9 @@ async def run_sweep_async( self, processor_id: str, *, - run_name: str, device_config_name: str, + run_name: str = "", + snapshot_id: str = "", job_id: Optional[str] = None, params: cirq.Sweepable = None, repetitions: int = 1, @@ -82,6 +83,9 @@ async def run_sweep_async( run_name: A unique identifier representing an automation run for the specified processor. An Automation Run contains a collection of device configurations for a processor. + snapshot_id: A unique identifier for an immutable snapshot reference. + A snapshot contains a collection of device configurations for the + processor. device_config_name: An identifier used to select the processor configuration utilized to run the job. A configuration identifies the set of available qubits, couplers, and supported gates in the processor. @@ -119,6 +123,7 @@ async def run_sweep_async( description=description, labels=labels, run_name=run_name, + snapshot_id=snapshot_id, device_config_name=device_config_name, ) return engine_job.EngineJob( @@ -131,7 +136,8 @@ async def run_async( self, *, processor_id: str, - run_name: str, + run_name: str = "", + snapshot_id: str = "", device_config_name: str, job_id: Optional[str] = None, param_resolver: cirq.ParamResolver = cirq.ParamResolver({}), @@ -146,6 +152,9 @@ async def run_async( run_name: A unique identifier representing an automation run for the specified processor. An Automation Run contains a collection of device configurations for a processor. + snapshot_id: A unique identifier for an immutable snapshot reference. + A snapshot contains a collection of device configurations for the + processor. device_config_name: An identifier used to select the processor configuration utilized to run the job. A configuration identifies the set of available qubits, couplers, and supported gates in the processor. @@ -175,6 +184,7 @@ async def run_async( description=description, labels=labels, run_name=run_name, + snapshot_id=snapshot_id, device_config_name=device_config_name, ) results = await job.results_async() diff --git a/cirq-google/cirq_google/engine/engine_test.py b/cirq-google/cirq_google/engine/engine_test.py index 1671d0fd8e1..3d4c328c070 100644 --- a/cirq-google/cirq_google/engine/engine_test.py +++ b/cirq-google/cirq_google/engine/engine_test.py @@ -274,6 +274,7 @@ def test_run_circuit_with_unary_rpcs(client): description=None, labels=None, run_name='', + snapshot_id='', device_config_name='', ) client().get_job_async.assert_called_once_with('proj', 'prog', 'job-id', False) @@ -281,7 +282,7 @@ def test_run_circuit_with_unary_rpcs(client): @mock.patch('cirq_google.engine.engine_client.EngineClient', autospec=True) -def test_run_circuit_with_stream_rpcs(client): +def test_run_circuit_with_stream_rpcs_passes(client): setup_run_circuit_with_result_(client, _A_RESULT) engine = cg.Engine( @@ -310,10 +311,53 @@ def test_run_circuit_with_stream_rpcs(client): job_labels=None, processor_id='mysim', run_name='', + snapshot_id='', device_config_name='', ) +@mock.patch('cirq_google.engine.engine_client.EngineClient', autospec=True) +def test_run_circuit_snapshot_id_with_stream_rpcs(client): + setup_run_circuit_with_result_(client, _A_RESULT) + + engine = cg.Engine( + project_id='proj', + context=EngineContext(service_args={'client_info': 1}, enable_streaming=True), + ) + result = engine.run( + program=_CIRCUIT, + program_id='prog', + job_id='job-id', + processor_id='mysim', + snapshot_id="123", + device_config_name="config", + ) + + assert result.repetitions == 1 + assert result.params.param_dict == {'a': 1} + assert result.measurements == {'q': np.array([[0]], dtype='uint8')} + client.assert_called_with(service_args={'client_info': 1}, verbose=None) + client().run_job_over_stream.assert_called_once_with( + project_id='proj', + program_id='prog', + code=mock.ANY, + job_id='job-id', + run_context=util.pack_any( + v2.run_context_pb2.RunContext( + parameter_sweeps=[v2.run_context_pb2.ParameterSweep(repetitions=1)] + ) + ), + program_description=None, + program_labels=None, + job_description=None, + job_labels=None, + processor_id='mysim', + run_name='', + snapshot_id="123", + device_config_name='config', + ) + + def test_no_gate_set(): engine = cg.Engine(project_id='project-id') assert engine.context.serializer == cg.CIRCUIT_SERIALIZER diff --git a/cirq-google/cirq_google/engine/processor_sampler.py b/cirq-google/cirq_google/engine/processor_sampler.py index 14a09ca48f0..2abc249e011 100644 --- a/cirq-google/cirq_google/engine/processor_sampler.py +++ b/cirq-google/cirq_google/engine/processor_sampler.py @@ -29,12 +29,13 @@ def __init__( *, processor: 'cg.engine.AbstractProcessor', run_name: str = "", + snapshot_id: str = "", device_config_name: str = "", ): """Inits ProcessorSampler. - Either both `run_name` and `device_config_name` must be set, or neither of - them must be set. If none of them are set, a default internal device configuration + Either both (`run_name` or `snapshot_id`) and `device_config_name` must be set, or neither + of them must be set. If none of them are set, a default internal device configuration will be used. Args: @@ -42,6 +43,7 @@ def __init__( run_name: A unique identifier representing an automation run for the specified processor. An Automation Run contains a collection of device configurations for a processor. + snapshot_id: A unique identifier for an immutable snapshot reference. device_config_name: An identifier used to select the processor configuration utilized to run the job. A configuration identifies the set of available qubits, couplers, and supported gates in the processor. @@ -49,11 +51,12 @@ def __init__( Raises: ValueError: If only one of `run_name` and `device_config_name` are specified. """ - if bool(run_name) ^ bool(device_config_name): + if (bool(run_name) or bool(snapshot_id)) ^ bool(device_config_name): raise ValueError('Cannot specify only one of `run_name` and `device_config_name`') self._processor = processor self._run_name = run_name + self._snapshot_id = snapshot_id self._device_config_name = device_config_name async def run_sweep_async( @@ -64,6 +67,7 @@ async def run_sweep_async( params=params, repetitions=repetitions, run_name=self._run_name, + snapshot_id=self._snapshot_id, device_config_name=self._device_config_name, ) return await job.results_async() @@ -91,6 +95,10 @@ def processor(self) -> 'cg.engine.AbstractProcessor': def run_name(self) -> str: return self._run_name + @property + def snapshot_id(self) -> str: + return self._snapshot_id # pragma: no cover + @property def device_config_name(self) -> str: return self._device_config_name diff --git a/cirq-google/cirq_google/engine/processor_sampler_test.py b/cirq-google/cirq_google/engine/processor_sampler_test.py index ce482823664..4e0ca532e3e 100644 --- a/cirq-google/cirq_google/engine/processor_sampler_test.py +++ b/cirq-google/cirq_google/engine/processor_sampler_test.py @@ -23,9 +23,10 @@ @pytest.mark.parametrize('circuit', [cirq.Circuit(), cirq.FrozenCircuit()]) @pytest.mark.parametrize( - 'run_name, device_config_name', [('run_name', 'device_config_alias'), ('', '')] + 'run_name, device_config_name, snapshot_id', + [('run_name', 'device_config_alias', ''), ('', '', '')], ) -def test_run_circuit(circuit, run_name, device_config_name): +def test_run_circuit(circuit, run_name, device_config_name, snapshot_id): processor = mock.create_autospec(AbstractProcessor) sampler = cg.ProcessorSampler( processor=processor, run_name=run_name, device_config_name=device_config_name @@ -37,17 +38,22 @@ def test_run_circuit(circuit, run_name, device_config_name): program=circuit, repetitions=5, run_name=run_name, + snapshot_id=snapshot_id, device_config_name=device_config_name, ) @pytest.mark.parametrize( - 'run_name, device_config_name', [('run_name', 'device_config_alias'), ('', '')] + 'run_name, device_config_name, snapshot_id', + [('run_name', 'device_config_alias', ''), ('', '', ''), ('', 'config_name', 'snapshot_id')], ) -def test_run_batch(run_name, device_config_name): +def test_run_batch(run_name, device_config_name, snapshot_id): processor = mock.create_autospec(AbstractProcessor) sampler = cg.ProcessorSampler( - processor=processor, run_name=run_name, device_config_name=device_config_name + processor=processor, + run_name=run_name, + snapshot_id=snapshot_id, + device_config_name=device_config_name, ) a = cirq.LineQubit(0) circuit1 = cirq.Circuit(cirq.X(a)) @@ -63,6 +69,7 @@ def test_run_batch(run_name, device_config_name): params=params1, repetitions=5, run_name=run_name, + snapshot_id=snapshot_id, device_config_name=device_config_name, ), mock.call().results_async(), @@ -71,6 +78,7 @@ def test_run_batch(run_name, device_config_name): params=params2, repetitions=5, run_name=run_name, + snapshot_id=snapshot_id, device_config_name=device_config_name, ), mock.call().results_async(), @@ -79,9 +87,10 @@ def test_run_batch(run_name, device_config_name): @pytest.mark.parametrize( - 'run_name, device_config_name', [('run_name', 'device_config_alias'), ('', '')] + 'run_name, device_config_name, snapshot_id', + [('run_name', 'device_config_alias', ''), ('', '', '')], ) -def test_run_batch_identical_repetitions(run_name, device_config_name): +def test_run_batch_identical_repetitions(run_name, device_config_name, snapshot_id): processor = mock.create_autospec(AbstractProcessor) sampler = cg.ProcessorSampler( processor=processor, run_name=run_name, device_config_name=device_config_name @@ -100,6 +109,7 @@ def test_run_batch_identical_repetitions(run_name, device_config_name): params=params1, repetitions=5, run_name=run_name, + snapshot_id=snapshot_id, device_config_name=device_config_name, ), mock.call().results_async(), @@ -108,6 +118,7 @@ def test_run_batch_identical_repetitions(run_name, device_config_name): params=params2, repetitions=5, run_name=run_name, + snapshot_id=snapshot_id, device_config_name=device_config_name, ), mock.call().results_async(), @@ -153,6 +164,7 @@ def test_run_batch_differing_repetitions(): program=circuit2, repetitions=2, run_name=run_name, + snapshot_id='', device_config_name=device_config_name, ) diff --git a/cirq-google/cirq_google/engine/simulated_local_processor.py b/cirq-google/cirq_google/engine/simulated_local_processor.py index c8d4cbaec8e..f53d6b3b600 100644 --- a/cirq-google/cirq_google/engine/simulated_local_processor.py +++ b/cirq-google/cirq_google/engine/simulated_local_processor.py @@ -199,6 +199,7 @@ def get_program(self, program_id: str) -> AbstractProgram: async def run_sweep_async( self, program: cirq.AbstractCircuit, + *, program_id: Optional[str] = None, job_id: Optional[str] = None, params: cirq.Sweepable = None, @@ -208,6 +209,7 @@ async def run_sweep_async( job_description: Optional[str] = None, job_labels: Optional[Dict[str, str]] = None, run_name: str = "", + snapshot_id: str = "", device_config_name: str = "", ) -> SimulatedLocalJob: if program_id is None: