1# Copyright 2018 The Cirq Developers
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Classes for running against Google's Quantum Cloud Service.
15
16As an example, to run a circuit against the xmon simulator on the cloud,
17    engine = cirq_google.Engine(project_id='my-project-id')
18    program = engine.create_program(circuit)
19    result0 = program.run(params=params0, repetitions=10)
20    result1 = program.run(params=params1, repetitions=10)
21
22In order to run on must have access to the Quantum Engine API. Access to this
23API is (as of June 22, 2018) restricted to invitation only.
24"""
25
26import datetime
27import enum
28import os
29import random
30import string
31from typing import Dict, Iterable, List, Optional, Sequence, Set, TypeVar, Union, TYPE_CHECKING
32
33from google.protobuf import any_pb2
34
35import cirq
36from cirq_google.api import v2
37from cirq_google.engine import engine_client
38from cirq_google.engine.client import quantum
39from cirq_google.engine.result_type import ResultType
40from cirq_google.serialization import SerializableGateSet, Serializer
41from cirq_google.serialization.arg_func_langs import arg_to_proto
42from cirq_google.engine import (
43    engine_client,
44    engine_program,
45    engine_job,
46    engine_processor,
47    engine_sampler,
48)
49
50if TYPE_CHECKING:
51    import cirq_google
52    import google.protobuf
53
54TYPE_PREFIX = 'type.googleapis.com/'
55
56_R = TypeVar('_R')
57
58
59class ProtoVersion(enum.Enum):
60    """Protocol buffer version to use for requests to the quantum engine."""
61
62    UNDEFINED = 0
63    V1 = 1
64    V2 = 2
65
66
67def _make_random_id(prefix: str, length: int = 16):
68    random_digits = [random.choice(string.ascii_uppercase + string.digits) for _ in range(length)]
69    suffix = ''.join(random_digits)
70    suffix += datetime.datetime.now().strftime('%y%m%d-%H%M%S')
71    return f'{prefix}{suffix}'
72
73
74@cirq.value_equality
75class EngineContext:
76    """Context for running against the Quantum Engine API. Most users should
77    simply create an Engine object instead of working with one of these
78    directly."""
79
80    def __init__(
81        self,
82        proto_version: Optional[ProtoVersion] = None,
83        service_args: Optional[Dict] = None,
84        verbose: Optional[bool] = None,
85        client: 'Optional[engine_client.EngineClient]' = None,
86        timeout: Optional[int] = None,
87    ) -> None:
88        """Context and client for using Quantum Engine.
89
90        Args:
91            proto_version: The version of cirq protos to use. If None, then
92                ProtoVersion.V2 will be used.
93            service_args: A dictionary of arguments that can be used to
94                configure options on the underlying client.
95            verbose: Suppresses stderr messages when set to False. Default is
96                true.
97            timeout: Timeout for polling for results, in seconds.  Default is
98                to never timeout.
99        """
100        if (service_args or verbose) and client:
101            raise ValueError('either specify service_args and verbose or client')
102
103        self.proto_version = proto_version or ProtoVersion.V2
104        if self.proto_version == ProtoVersion.V1:
105            raise ValueError('ProtoVersion V1 no longer supported')
106
107        if not client:
108            client = engine_client.EngineClient(service_args=service_args, verbose=verbose)
109        self.client = client
110        self.timeout = timeout
111
112    def copy(self) -> 'EngineContext':
113        return EngineContext(proto_version=self.proto_version, client=self.client)
114
115    def _value_equality_values_(self):
116        return self.proto_version, self.client
117
118
119class Engine:
120    """Runs programs via the Quantum Engine API.
121
122    This class has methods for creating programs and jobs that execute on
123    Quantum Engine:
124        create_program
125        run
126        run_sweep
127        run_batch
128
129    Another set of methods return information about programs and jobs that
130    have been previously created on the Quantum Engine, as well as metadata
131    about available processors:
132        get_program
133        list_processors
134        get_processor
135    """
136
137    def __init__(
138        self,
139        project_id: str,
140        proto_version: Optional[ProtoVersion] = None,
141        service_args: Optional[Dict] = None,
142        verbose: Optional[bool] = None,
143        timeout: Optional[int] = None,
144        context: Optional[EngineContext] = None,
145    ) -> None:
146        """Supports creating and running programs against the Quantum Engine.
147
148        Args:
149            project_id: A project_id string of the Google Cloud Project to use.
150                API interactions will be attributed to this project and any
151                resources created will be owned by the project. See
152                https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects
153            proto_version: The version of cirq protos to use. If None, then
154                ProtoVersion.V2 will be used.
155            service_args: A dictionary of arguments that can be used to
156                configure options on the underlying client.
157            verbose: Suppresses stderr messages when set to False. Default is
158                true.
159            timeout: Timeout for polling for results, in seconds.  Default is
160                to never timeout.
161            context: Engine configuration and context to use. For most users
162                this should never be specified.
163        """
164        if context and (proto_version or service_args or verbose):
165            raise ValueError('Either provide context or proto_version, service_args and verbose.')
166
167        self.project_id = project_id
168        if not context:
169            context = EngineContext(
170                proto_version=proto_version,
171                service_args=service_args,
172                verbose=verbose,
173                timeout=timeout,
174            )
175        self.context = context
176
177    def __str__(self) -> str:
178        return f'Engine(project_id={self.project_id!r})'
179
180    def run(
181        self,
182        program: cirq.Circuit,
183        program_id: Optional[str] = None,
184        job_id: Optional[str] = None,
185        param_resolver: cirq.ParamResolver = cirq.ParamResolver({}),
186        repetitions: int = 1,
187        processor_ids: Sequence[str] = ('xmonsim',),
188        gate_set: Optional[Serializer] = None,
189        program_description: Optional[str] = None,
190        program_labels: Optional[Dict[str, str]] = None,
191        job_description: Optional[str] = None,
192        job_labels: Optional[Dict[str, str]] = None,
193    ) -> cirq.Result:
194        """Runs the supplied Circuit via Quantum Engine.
195
196        Args:
197            program: The Circuit to execute. If a circuit is
198                provided, a moment by moment schedule will be used.
199            program_id: A user-provided identifier for the program. This must
200                be unique within the Google Cloud project being used. If this
201                parameter is not provided, a random id of the format
202                'prog-################YYMMDD' will be generated, where # is
203                alphanumeric and YYMMDD is the current year, month, and day.
204            job_id: Job identifier to use. If this is not provided, a random id
205                of the format 'job-################YYMMDD' will be generated,
206                where # is alphanumeric and YYMMDD is the current year, month,
207                and day.
208            param_resolver: Parameters to run with the program.
209            repetitions: The number of repetitions to simulate.
210            processor_ids: The engine processors that should be candidates
211                to run the program. Only one of these will be scheduled for
212                execution.
213            gate_set: The gate set used to serialize the circuit. The gate set
214                must be supported by the selected processor.
215            program_description: An optional description to set on the program.
216            program_labels: Optional set of labels to set on the program.
217            job_description: An optional description to set on the job.
218            job_labels: Optional set of labels to set on the job.
219
220        Returns:
221            A single Result for this run.
222        """
223        if not gate_set:
224            raise ValueError('No gate set provided')
225        return list(
226            self.run_sweep(
227                program=program,
228                program_id=program_id,
229                job_id=job_id,
230                params=[param_resolver],
231                repetitions=repetitions,
232                processor_ids=processor_ids,
233                gate_set=gate_set,
234                program_description=program_description,
235                program_labels=program_labels,
236                job_description=job_description,
237                job_labels=job_labels,
238            )
239        )[0]
240
241    def run_sweep(
242        self,
243        program: cirq.Circuit,
244        program_id: Optional[str] = None,
245        job_id: Optional[str] = None,
246        params: cirq.Sweepable = None,
247        repetitions: int = 1,
248        processor_ids: Sequence[str] = ('xmonsim',),
249        gate_set: Optional[Serializer] = None,
250        program_description: Optional[str] = None,
251        program_labels: Optional[Dict[str, str]] = None,
252        job_description: Optional[str] = None,
253        job_labels: Optional[Dict[str, str]] = None,
254    ) -> engine_job.EngineJob:
255        """Runs the supplied Circuit via Quantum Engine.Creates
256
257        In contrast to run, this runs across multiple parameter sweeps, and
258        does not block until a result is returned.
259
260        Args:
261            program: The Circuit to execute. If a circuit is
262                provided, a moment by moment schedule will be used.
263            program_id: A user-provided identifier for the program. This must
264                be unique within the Google Cloud project being used. If this
265                parameter is not provided, a random id of the format
266                'prog-################YYMMDD' will be generated, where # is
267                alphanumeric and YYMMDD is the current year, month, and day.
268            job_id: Job identifier to use. If this is not provided, a random id
269                of the format 'job-################YYMMDD' will be generated,
270                where # is alphanumeric and YYMMDD is the current year, month,
271                and day.
272            params: Parameters to run with the program.
273            repetitions: The number of circuit repetitions to run.
274            processor_ids: The engine processors that should be candidates
275                to run the program. Only one of these will be scheduled for
276                execution.
277            gate_set: The gate set used to serialize the circuit. The gate set
278                must be supported by the selected processor.
279            program_description: An optional description to set on the program.
280            program_labels: Optional set of labels to set on the program.
281            job_description: An optional description to set on the job.
282            job_labels: Optional set of labels to set on the job.
283
284        Returns:
285            An EngineJob. If this is iterated over it returns a list of
286            TrialResults, one for each parameter sweep.
287        """
288        if not gate_set:
289            raise ValueError('No gate set provided')
290        engine_program = self.create_program(
291            program, program_id, gate_set, program_description, program_labels
292        )
293        return engine_program.run_sweep(
294            job_id=job_id,
295            params=params,
296            repetitions=repetitions,
297            processor_ids=processor_ids,
298            description=job_description,
299            labels=job_labels,
300        )
301
302    def run_batch(
303        self,
304        programs: List[cirq.Circuit],
305        program_id: Optional[str] = None,
306        job_id: Optional[str] = None,
307        params_list: List[cirq.Sweepable] = None,
308        repetitions: int = 1,
309        processor_ids: Sequence[str] = (),
310        gate_set: Optional[Serializer] = None,
311        program_description: Optional[str] = None,
312        program_labels: Optional[Dict[str, str]] = None,
313        job_description: Optional[str] = None,
314        job_labels: Optional[Dict[str, str]] = None,
315    ) -> engine_job.EngineJob:
316        """Runs the supplied Circuits via Quantum Engine.Creates
317
318        This will combine each Circuit provided in `programs` into
319        a BatchProgram.  Each circuit will pair with the associated
320        parameter sweep provided in the `params_list`.  The number of
321        programs is required to match the number of sweeps.
322
323        This method does not block until a result is returned.  However,
324        no results will be available until the entire batch is complete.
325
326        Args:
327            programs: The Circuits to execute as a batch.
328            program_id: A user-provided identifier for the program. This must
329                be unique within the Google Cloud project being used. If this
330                parameter is not provided, a random id of the format
331                'prog-################YYMMDD' will be generated, where # is
332                alphanumeric and YYMMDD is the current year, month, and day.
333            job_id: Job identifier to use. If this is not provided, a random id
334                of the format 'job-################YYMMDD' will be generated,
335                where # is alphanumeric and YYMMDD is the current year, month,
336                and day.
337            params_list: Parameter sweeps to use with the circuits. The number
338                of sweeps should match the number of circuits and will be
339                paired in order with the circuits. If this is None, it is
340                assumed that the circuits are not parameterized and do not
341                require sweeps.
342            repetitions: Number of circuit repetitions to run.  Each sweep value
343                of each circuit in the batch will run with the same repetitions.
344            processor_ids: The engine processors that should be candidates
345                to run the program. Only one of these will be scheduled for
346                execution.
347            gate_set: The gate set used to serialize the circuit. The gate set
348                must be supported by the selected processor.
349            program_description: An optional description to set on the program.
350            program_labels: Optional set of labels to set on the program.
351            job_description: An optional description to set on the job.
352            job_labels: Optional set of labels to set on the job.
353
354        Returns:
355            An EngineJob. If this is iterated over it returns a list of
356            TrialResults. All TrialResults for the first circuit are listed
357            first, then the TrialResults for the second, etc. The TrialResults
358            for a circuit are listed in the order imposed by the associated
359            parameter sweep.
360        """
361        if params_list is None:
362            params_list = [None] * len(programs)
363        elif len(programs) != len(params_list):
364            raise ValueError('Number of circuits and sweeps must match')
365        if not processor_ids:
366            raise ValueError('Processor id must be specified.')
367        engine_program = self.create_batch_program(
368            programs, program_id, gate_set, program_description, program_labels
369        )
370        return engine_program.run_batch(
371            job_id=job_id,
372            params_list=params_list,
373            repetitions=repetitions,
374            processor_ids=processor_ids,
375            description=job_description,
376            labels=job_labels,
377        )
378
379    def run_calibration(
380        self,
381        layers: List['cirq_google.CalibrationLayer'],
382        program_id: Optional[str] = None,
383        job_id: Optional[str] = None,
384        processor_id: str = None,
385        processor_ids: Sequence[str] = (),
386        gate_set: Optional[Serializer] = None,
387        program_description: Optional[str] = None,
388        program_labels: Optional[Dict[str, str]] = None,
389        job_description: Optional[str] = None,
390        job_labels: Optional[Dict[str, str]] = None,
391    ) -> engine_job.EngineJob:
392        """Runs the specified calibrations via the Calibration API.
393
394        Each calibration will be specified by a `CalibrationLayer`
395        that contains the type of the calibrations to run, a `Circuit`
396        to optimize, and any arguments needed by the calibration routine.
397
398        Arguments and circuits needed for each layer will vary based on the
399        calibration type.  However, the typical calibration routine may
400        require a single moment defining the gates to optimize, for example.
401
402        Note: this is an experimental API and is not yet fully supported
403        for all users.
404
405        Args:
406            layers: The layers of calibration to execute as a batch.
407            program_id: A user-provided identifier for the program. This must
408                be unique within the Google Cloud project being used. If this
409                parameter is not provided, a random id of the format
410                'calibration-################YYMMDD' will be generated,
411                where # is alphanumeric and YYMMDD is the current year, month,
412                and day.
413            job_id: Job identifier to use. If this is not provided, a random id
414                of the format 'calibration-################YYMMDD' will be
415                generated, where # is alphanumeric and YYMMDD is the current
416                year, month, and day.
417            processor_id: The engine processor that should run the calibration.
418                If this is specified, processor_ids should not be specified.
419            processor_ids: The engine processors that should be candidates
420                to run the program. Only one of these will be scheduled for
421                execution.
422            gate_set: The gate set used to serialize the circuit. The gate set
423                must be supported by the selected processor.
424            program_description: An optional description to set on the program.
425            program_labels: Optional set of labels to set on the program.
426            job_description: An optional description to set on the job.
427            job_labels: Optional set of labels to set on the job.  By defauly,
428                this will add a 'calibration' label to the job.
429
430        Returns:
431            An EngineJob whose results can be retrieved by calling
432            calibration_results().
433        """
434        if processor_id and processor_ids:
435            raise ValueError('Only one of processor_id and processor_ids can be specified.')
436        if not processor_ids and not processor_id:
437            raise ValueError('Processor id must be specified.')
438        if processor_id:
439            processor_ids = [processor_id]
440        if job_labels is None:
441            job_labels = {'calibration': ''}
442        engine_program = self.create_calibration_program(
443            layers, program_id, gate_set, program_description, program_labels
444        )
445        return engine_program.run_calibration(
446            job_id=job_id,
447            processor_ids=processor_ids,
448            description=job_description,
449            labels=job_labels,
450        )
451
452    def create_program(
453        self,
454        program: cirq.Circuit,
455        program_id: Optional[str] = None,
456        gate_set: Optional[Serializer] = None,
457        description: Optional[str] = None,
458        labels: Optional[Dict[str, str]] = None,
459    ) -> engine_program.EngineProgram:
460        """Wraps a Circuit for use with the Quantum Engine.
461
462        Args:
463            program: The Circuit to execute.
464            program_id: A user-provided identifier for the program. This must be
465                unique within the Google Cloud project being used. If this
466                parameter is not provided, a random id of the format
467                'prog-################YYMMDD' will be generated, where # is
468                alphanumeric and YYMMDD is the current year, month, and day.
469            gate_set: The gate set used to serialize the circuit. The gate set
470                must be supported by the selected processor
471            description: An optional description to set on the program.
472            labels: Optional set of labels to set on the program.
473
474        Returns:
475            A EngineProgram for the newly created program.
476        """
477        if not gate_set:
478            raise ValueError('No gate set provided')
479
480        if not program_id:
481            program_id = _make_random_id('prog-')
482
483        new_program_id, new_program = self.context.client.create_program(
484            self.project_id,
485            program_id,
486            code=self._serialize_program(program, gate_set),
487            description=description,
488            labels=labels,
489        )
490
491        return engine_program.EngineProgram(
492            self.project_id, new_program_id, self.context, new_program
493        )
494
495    def create_batch_program(
496        self,
497        programs: List[cirq.Circuit],
498        program_id: Optional[str] = None,
499        gate_set: Optional[Serializer] = None,
500        description: Optional[str] = None,
501        labels: Optional[Dict[str, str]] = None,
502    ) -> engine_program.EngineProgram:
503        """Wraps a list of Circuits into a BatchProgram for the Quantum Engine.
504
505        Args:
506            programs: The Circuits to execute within a batch.
507            program_id: A user-provided identifier for the program. This must be
508                unique within the Google Cloud project being used. If this
509                parameter is not provided, a random id of the format
510                'prog-################YYMMDD' will be generated, where # is
511                alphanumeric and YYMMDD is the current year, month, and day.
512            gate_set: The gate set used to serialize the circuit. The gate set
513                must be supported by the selected processor
514            description: An optional description to set on the program.
515            labels: Optional set of labels to set on the program.
516
517        Returns:
518            A EngineProgram for the newly created program.
519        """
520        if not gate_set:
521            raise ValueError('Gate set must be specified.')
522        if not program_id:
523            program_id = _make_random_id('prog-')
524
525        batch = v2.batch_pb2.BatchProgram()
526        for program in programs:
527            gate_set.serialize(program, msg=batch.programs.add())
528
529        new_program_id, new_program = self.context.client.create_program(
530            self.project_id,
531            program_id,
532            code=self._pack_any(batch),
533            description=description,
534            labels=labels,
535        )
536
537        return engine_program.EngineProgram(
538            self.project_id, new_program_id, self.context, new_program, result_type=ResultType.Batch
539        )
540
541    def create_calibration_program(
542        self,
543        layers: List['cirq_google.CalibrationLayer'],
544        program_id: Optional[str] = None,
545        gate_set: Optional[Serializer] = None,
546        description: Optional[str] = None,
547        labels: Optional[Dict[str, str]] = None,
548    ) -> engine_program.EngineProgram:
549        """Wraps a list of calibration layers into an Any for Quantum Engine.
550
551        Args:
552            layers: The calibration routines to execute.  All layers will be
553                executed within the same API call in the order specified,
554                though some layers may be interleaved together using
555                hardware-specific batching.
556            program_id: A user-provided identifier for the program. This must be
557                unique within the Google Cloud project being used. If this
558                parameter is not provided, a random id of the format
559                'calibration-################YYMMDD' will be generated,
560                where # is alphanumeric and YYMMDD is the current year, month,
561                and day.
562            gate_set: The gate set used to serialize the circuits in each
563                layer.  The gate set must be supported by the processor.
564            description: An optional description to set on the program.
565            labels: Optional set of labels to set on the program.
566
567        Returns:
568            A EngineProgram for the newly created program.
569        """
570        if not gate_set:
571            raise ValueError('Gate set must be specified.')
572        if not program_id:
573            program_id = _make_random_id('calibration-')
574
575        calibration = v2.calibration_pb2.FocusedCalibration()
576        for layer in layers:
577            new_layer = calibration.layers.add()
578            new_layer.calibration_type = layer.calibration_type
579            for arg in layer.args:
580                arg_to_proto(layer.args[arg], out=new_layer.args[arg])
581            gate_set.serialize(layer.program, msg=new_layer.layer)
582
583        new_program_id, new_program = self.context.client.create_program(
584            self.project_id,
585            program_id,
586            code=self._pack_any(calibration),
587            description=description,
588            labels=labels,
589        )
590
591        return engine_program.EngineProgram(
592            self.project_id,
593            new_program_id,
594            self.context,
595            new_program,
596            result_type=ResultType.Calibration,
597        )
598
599    def _serialize_program(self, program: cirq.Circuit, gate_set: Serializer) -> any_pb2.Any:
600        if not isinstance(program, cirq.Circuit):
601            raise TypeError(f'Unrecognized program type: {type(program)}')
602        program.device.validate_circuit(program)
603
604        if self.context.proto_version == ProtoVersion.V2:
605            program = gate_set.serialize(program)
606            return self._pack_any(program)
607        else:
608            raise ValueError(f'invalid program proto version: {self.context.proto_version}')
609
610    def _pack_any(self, message: 'google.protobuf.Message') -> any_pb2.Any:
611        """Packs a message into an Any proto.
612
613        Returns the packed Any proto.
614        """
615        packed = any_pb2.Any()
616        packed.Pack(message)
617        return packed
618
619    def get_program(self, program_id: str) -> engine_program.EngineProgram:
620        """Returns an EngineProgram for an existing Quantum Engine program.
621
622        Args:
623            program_id: Unique ID of the program within the parent project.
624
625        Returns:
626            A EngineProgram for the program.
627        """
628        return engine_program.EngineProgram(self.project_id, program_id, self.context)
629
630    def list_programs(
631        self,
632        created_before: Optional[Union[datetime.datetime, datetime.date]] = None,
633        created_after: Optional[Union[datetime.datetime, datetime.date]] = None,
634        has_labels: Optional[Dict[str, str]] = None,
635    ) -> List[engine_program.EngineProgram]:
636        """Returns a list of previously executed quantum programs.
637
638        Args:
639            created_after: retrieve programs that were created after this date
640                or time.
641            created_before: retrieve programs that were created after this date
642                or time.
643            has_labels: retrieve programs that have labels on them specified by
644                this dict. If the value is set to `*`, filters having the label
645                regardless of the label value will be filtered. For example, to
646                query programs that have the shape label and have the color
647                label with value red can be queried using
648                `{'color: red', 'shape:*'}`
649        """
650
651        client = self.context.client
652        response = client.list_programs(
653            self.project_id,
654            created_before=created_before,
655            created_after=created_after,
656            has_labels=has_labels,
657        )
658        return [
659            engine_program.EngineProgram(
660                project_id=engine_client._ids_from_program_name(p.name)[0],
661                program_id=engine_client._ids_from_program_name(p.name)[1],
662                _program=p,
663                context=self.context,
664            )
665            for p in response
666        ]
667
668    def list_jobs(
669        self,
670        created_before: Optional[Union[datetime.datetime, datetime.date]] = None,
671        created_after: Optional[Union[datetime.datetime, datetime.date]] = None,
672        has_labels: Optional[Dict[str, str]] = None,
673        execution_states: Optional[Set[quantum.enums.ExecutionStatus.State]] = None,
674    ):
675        """Returns the list of jobs in the project.
676
677        All historical jobs can be retrieved using this method and filtering
678        options are available too, to narrow down the search baesd on:
679          * creation time
680          * job labels
681          * execution states
682
683        Args:
684            created_after: retrieve jobs that were created after this date
685                or time.
686            created_before: retrieve jobs that were created after this date
687                or time.
688            has_labels: retrieve jobs that have labels on them specified by
689                this dict. If the value is set to `*`, filters having the label
690                regardless of the label value will be filtered. For example, to
691                query programs that have the shape label and have the color
692                label with value red can be queried using
693
694                {'color': 'red', 'shape':'*'}
695
696            execution_states: retrieve jobs that have an execution state  that
697                 is contained in `execution_states`. See
698                 `quantum.enums.ExecutionStatus.State` enum for accepted values.
699        """
700        client = self.context.client
701        response = client.list_jobs(
702            self.project_id,
703            None,
704            created_before=created_before,
705            created_after=created_after,
706            has_labels=has_labels,
707            execution_states=execution_states,
708        )
709        return [
710            engine_job.EngineJob(
711                project_id=engine_client._ids_from_job_name(j.name)[0],
712                program_id=engine_client._ids_from_job_name(j.name)[1],
713                job_id=engine_client._ids_from_job_name(j.name)[2],
714                context=self.context,
715                _job=j,
716            )
717            for j in response
718        ]
719
720    def list_processors(self) -> List[engine_processor.EngineProcessor]:
721        """Returns a list of Processors that the user has visibility to in the
722        current Engine project. The names of these processors are used to
723        identify devices when scheduling jobs and gathering calibration metrics.
724
725        Returns:
726            A list of EngineProcessors to access status, device and calibration
727            information.
728        """
729        response = self.context.client.list_processors(self.project_id)
730        return [
731            engine_processor.EngineProcessor(
732                self.project_id,
733                engine_client._ids_from_processor_name(p.name)[1],
734                self.context,
735                p,
736            )
737            for p in response
738        ]
739
740    def get_processor(self, processor_id: str) -> engine_processor.EngineProcessor:
741        """Returns an EngineProcessor for a Quantum Engine processor.
742
743        Args:
744            processor_id: The processor unique identifier.
745
746        Returns:
747            A EngineProcessor for the processor.
748        """
749        return engine_processor.EngineProcessor(self.project_id, processor_id, self.context)
750
751    def sampler(
752        self, processor_id: Union[str, List[str]], gate_set: Serializer
753    ) -> engine_sampler.QuantumEngineSampler:
754        """Returns a sampler backed by the engine.
755
756        Args:
757            processor_id: String identifier, or list of string identifiers,
758                determining which processors may be used when sampling.
759            gate_set: Determines how to serialize circuits when requesting
760                samples.
761        """
762        return engine_sampler.QuantumEngineSampler(
763            engine=self, processor_id=processor_id, gate_set=gate_set
764        )
765
766
767def get_engine(project_id: Optional[str] = None) -> Engine:
768    """Get an Engine instance assuming some sensible defaults.
769
770    This uses the environment variable GOOGLE_CLOUD_PROJECT for the Engine
771    project_id, unless set explicitly. By using an environment variable,
772    you can avoid hard-coding the project_id in shared code.
773
774    If the environment variables are set, but incorrect, an authentication
775    failure will occur when attempting to run jobs on the engine.
776
777    Args:
778        project_id: If set overrides the project id obtained from the
779            environment variable `GOOGLE_CLOUD_PROJECT`.
780
781    Returns:
782        The Engine instance.
783
784    Raises:
785        EnvironmentError: If the environment variable GOOGLE_CLOUD_PROJECT is
786            not set.
787    """
788    env_project_id = 'GOOGLE_CLOUD_PROJECT'
789    if not project_id:
790        project_id = os.environ.get(env_project_id)
791    if not project_id:
792        raise EnvironmentError(f'Environment variable {env_project_id} is not set.')
793
794    return Engine(project_id=project_id)
795
796
797def get_engine_device(
798    processor_id: str,
799    project_id: Optional[str] = None,
800    gatesets: Iterable[SerializableGateSet] = (),
801) -> cirq.Device:
802    """Returns a `Device` object for a given processor.
803
804    This is a short-cut for creating an engine object, getting the
805    processor object, and retrieving the device.  Note that the
806    gateset is required in order to match the serialized specification
807    back into cirq objects.
808    """
809    return get_engine(project_id).get_processor(processor_id).get_device(gatesets)
810
811
812def get_engine_calibration(
813    processor_id: str,
814    project_id: Optional[str] = None,
815) -> Optional['cirq_google.Calibration']:
816    """Returns calibration metrics for a given processor.
817
818    This is a short-cut for creating an engine object, getting the
819    processor object, and retrieving the current calibration.
820    May return None if no calibration metrics exist for the device.
821    """
822    return get_engine(project_id).get_processor(processor_id).get_current_calibration()
823