From 3a9fe5f4dfc69209266f0069cce13ad90b124af0 Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Mon, 1 Nov 2021 01:53:39 -0700 Subject: [PATCH 01/16] Validating Sampler - Wrapper around sampler to do device related validation. - This will be used in AbstractEngine in order to centralize and simplify validation. --- cirq-google/cirq_google/__init__.py | 1 + cirq-google/cirq_google/engine/__init__.py | 4 + .../cirq_google/engine/validating_sampler.py | 75 +++++++++++++++++++ .../engine/validating_sampler_test.py | 74 ++++++++++++++++++ .../cirq_google/json_test_data/spec.py | 1 + 5 files changed, 155 insertions(+) create mode 100644 cirq-google/cirq_google/engine/validating_sampler.py create mode 100644 cirq-google/cirq_google/engine/validating_sampler_test.py diff --git a/cirq-google/cirq_google/__init__.py b/cirq-google/cirq_google/__init__.py index 4c66402529a..77a8dd90b51 100644 --- a/cirq-google/cirq_google/__init__.py +++ b/cirq-google/cirq_google/__init__.py @@ -70,6 +70,7 @@ EngineTimeSlot, ProtoVersion, QuantumEngineSampler, + ValidatingSampler, get_engine, get_engine_calibration, get_engine_device, diff --git a/cirq-google/cirq_google/engine/__init__.py b/cirq-google/cirq_google/engine/__init__.py index 35f511df4ac..76e115a7ac7 100644 --- a/cirq-google/cirq_google/engine/__init__.py +++ b/cirq-google/cirq_google/engine/__init__.py @@ -57,3 +57,7 @@ get_engine_sampler, QuantumEngineSampler, ) + +from cirq_google.engine.validating_sampler import ( + ValidatingSampler, +) diff --git a/cirq-google/cirq_google/engine/validating_sampler.py b/cirq-google/cirq_google/engine/validating_sampler.py new file mode 100644 index 00000000000..57b6c9fe8bf --- /dev/null +++ b/cirq-google/cirq_google/engine/validating_sampler.py @@ -0,0 +1,75 @@ +# Copyright 2021 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. +from typing import Callable, List, Optional, Sequence, Union + +import cirq + +VALIDATOR_TYPE = Callable[[Sequence[cirq.AbstractCircuit], Sequence[cirq.Sweepable]], None] + + +class ValidatingSampler(cirq.Sampler): + """Wrapper around cirq.Sampler that performs device validation. + + This sampler will delegate to the wrapping sampler after + performing validation on the circuit(s) given to the sampler. + + Args: + sampler: sampler wrapped by this object. After validating, + samples will be returned by this enclosed cirq.Sampler. + device: cirq.Device that will validate_circuit before sampling. + validator: A callable that will do any additional validation + beyond the device. For instance, this can perform serialization + checks. Note that this function takes a list of circuits and + sweeps so that batch functionality can also be tested. + """ + + def __init__( + self, + *, + device: Optional[cirq.Device] = None, + validator: Optional[VALIDATOR_TYPE] = None, + sampler: cirq.Sampler = cirq.Simulator(), + ): + self._sampler = sampler + self._device = device + self._validator = validator + + def _validate_circuit( + self, circuits: Sequence[cirq.AbstractCircuit], sweeps: List[cirq.Sweepable] + ): + if self._device: + for circuit in circuits: + self._device.validate_circuit(circuit) + if self._validator: + self._validator(circuits, sweeps) + + def run_sweep( + self, + program: cirq.AbstractCircuit, + params: cirq.Sweepable, + repetitions: int = 1, + ) -> List['cirq.Result']: + self._validate_circuit([program], [params]) + return self._sampler.run_sweep(program, params, repetitions) + + def run_batch( + self, + programs: Sequence['cirq.AbstractCircuit'], + params_list: Optional[List['cirq.Sweepable']] = None, + repetitions: Union[int, List[int]] = 1, + ) -> List[List['cirq.Result']]: + if params_list is None: + params_list = [None] * len(programs) + self._validate_circuit(programs, params_list) + return self._sampler.run_batch(programs, params_list, repetitions) diff --git a/cirq-google/cirq_google/engine/validating_sampler_test.py b/cirq-google/cirq_google/engine/validating_sampler_test.py new file mode 100644 index 00000000000..1b54cbbab41 --- /dev/null +++ b/cirq-google/cirq_google/engine/validating_sampler_test.py @@ -0,0 +1,74 @@ +# Copyright 2021 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. +from typing import List +import pytest +import sympy +import numpy as np + +import cirq +import cirq_google as cg + + +def test_device_validation(): + sampler = cg.ValidatingSampler( + device=cg.Sycamore23, validator=lambda c, r: True, sampler=cirq.Simulator() + ) + + # Good qubit + q = cirq.GridQubit(5, 2) + circuit = cirq.Circuit(cirq.X(q) ** sympy.Symbol('t'), cirq.measure(q, key='m')) + sweep = cirq.Points(key='t', points=[1, 0]) + results = sampler.run_sweep(circuit, sweep, repetitions=100) + assert np.all(results[0].measurements['m'] == 1) + assert np.all(results[1].measurements['m'] == 0) + + # Bad qubit + q = cirq.GridQubit(2, 2) + circuit = cirq.Circuit(cirq.X(q) ** sympy.Symbol('t'), cirq.measure(q, key='m')) + with pytest.raises(ValueError, match='Qubit not on device'): + results = sampler.run_sweep(circuit, sweep, repetitions=100) + + +def _batch_size_less_than_two(circuits: List[cirq.Circuit], sweeps: List[cirq.Sweepable]): + if len(circuits) > 2: + raise ValueError('Too many batches') + + +def test_batch_validation(): + sampler = cg.ValidatingSampler( + device=cirq.UNCONSTRAINED_DEVICE, + validator=_batch_size_less_than_two, + sampler=cirq.Simulator(), + ) + q = cirq.GridQubit(2, 2) + circuits = [ + cirq.Circuit(cirq.X(q) ** sympy.Symbol('t'), cirq.measure(q, key='m')), + cirq.Circuit(cirq.X(q) ** sympy.Symbol('x'), cirq.measure(q, key='m2')), + ] + sweeps = [cirq.Points(key='t', points=[1, 0]), cirq.Points(key='x', points=[0, 1])] + results = sampler.run_batch(circuits, sweeps, repetitions=100) + + assert np.all(results[0][0].measurements['m'] == 1) + assert np.all(results[0][1].measurements['m'] == 0) + assert np.all(results[1][0].measurements['m2'] == 0) + assert np.all(results[1][1].measurements['m2'] == 1) + + circuits = [ + cirq.Circuit(cirq.X(q) ** sympy.Symbol('t'), cirq.measure(q, key='m')), + cirq.Circuit(cirq.X(q) ** sympy.Symbol('x'), cirq.measure(q, key='m2')), + cirq.Circuit(cirq.measure(q, key='m3')), + ] + sweeps = [cirq.Points(key='t', points=[1, 0]), cirq.Points(key='x', points=[0, 1]), {}] + with pytest.raises(ValueError, match='Too many batches'): + results = sampler.run_batch(circuits, sweeps, repetitions=100) diff --git a/cirq-google/cirq_google/json_test_data/spec.py b/cirq-google/cirq_google/json_test_data/spec.py index f644efe3ea6..19b80c124bb 100644 --- a/cirq-google/cirq_google/json_test_data/spec.py +++ b/cirq-google/cirq_google/json_test_data/spec.py @@ -49,6 +49,7 @@ 'SerializingArg', 'THETA_ZETA_GAMMA_FLOQUET_PHASED_FSIM_CHARACTERIZATION', 'QuantumEngineSampler', + 'ValidatingSampler', # Abstract: 'ExecutableSpec', ], From 967c1d1d1d524f412ac797dd8fa7b349132681a7 Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Mon, 1 Nov 2021 02:17:48 -0700 Subject: [PATCH 02/16] Add test for coverage --- .../cirq_google/engine/validating_sampler_test.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cirq-google/cirq_google/engine/validating_sampler_test.py b/cirq-google/cirq_google/engine/validating_sampler_test.py index 1b54cbbab41..a63dfe0d3e0 100644 --- a/cirq-google/cirq_google/engine/validating_sampler_test.py +++ b/cirq-google/cirq_google/engine/validating_sampler_test.py @@ -72,3 +72,15 @@ def test_batch_validation(): sweeps = [cirq.Points(key='t', points=[1, 0]), cirq.Points(key='x', points=[0, 1]), {}] with pytest.raises(ValueError, match='Too many batches'): results = sampler.run_batch(circuits, sweeps, repetitions=100) + +def test_batch_default_sweeps(): + sampler = cg.ValidatingSampler() + q=cirq.GridQubit(2, 2) + circuits = [ + cirq.Circuit(cirq.X(q), cirq.measure(q, key='m')), + cirq.Circuit(cirq.measure(q, key='m2')), + ] + results = sampler.run_batch(circuits, None, repetitions=100) + assert np.all(results[0][0].measurements['m'] == 1) + assert np.all(results[1][0].measurements['m2'] == 0) + From 4b3eef8d8bb1810a205a3e1983e70014e43f9450 Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Wed, 3 Nov 2021 10:37:22 -0700 Subject: [PATCH 03/16] Address review comments --- .../cirq_google/engine/validating_sampler.py | 46 ++++++++++--------- .../engine/validating_sampler_test.py | 25 +++++++++- 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/cirq-google/cirq_google/engine/validating_sampler.py b/cirq-google/cirq_google/engine/validating_sampler.py index 57b6c9fe8bf..59c8c6bd994 100644 --- a/cirq-google/cirq_google/engine/validating_sampler.py +++ b/cirq-google/cirq_google/engine/validating_sampler.py @@ -15,25 +15,12 @@ import cirq -VALIDATOR_TYPE = Callable[[Sequence[cirq.AbstractCircuit], Sequence[cirq.Sweepable]], None] +VALIDATOR_TYPE = Callable[ + [Sequence[cirq.AbstractCircuit], Sequence[cirq.Sweepable], Union[int, List[int]]], None +] class ValidatingSampler(cirq.Sampler): - """Wrapper around cirq.Sampler that performs device validation. - - This sampler will delegate to the wrapping sampler after - performing validation on the circuit(s) given to the sampler. - - Args: - sampler: sampler wrapped by this object. After validating, - samples will be returned by this enclosed cirq.Sampler. - device: cirq.Device that will validate_circuit before sampling. - validator: A callable that will do any additional validation - beyond the device. For instance, this can perform serialization - checks. Note that this function takes a list of circuits and - sweeps so that batch functionality can also be tested. - """ - def __init__( self, *, @@ -41,18 +28,35 @@ def __init__( validator: Optional[VALIDATOR_TYPE] = None, sampler: cirq.Sampler = cirq.Simulator(), ): - self._sampler = sampler + """Wrapper around `cirq.Sampler` that performs device validation. + + This sampler will delegate to the wrapping sampler after + performing validation on the circuit(s) given to the sampler. + + Args: + device: `cirq.Device` that will validate_circuit before sampling. + validator: A callable that will do any additional validation + beyond the device. For instance, this can perform serialization + checks. Note that this function takes a list of circuits and + sweeps so that batch functionality can also be tested. + sampler: sampler wrapped by this object. After validating, + samples will be returned by this enclosed `cirq.Sampler`. + """ self._device = device self._validator = validator + self._sampler = sampler def _validate_circuit( - self, circuits: Sequence[cirq.AbstractCircuit], sweeps: List[cirq.Sweepable] + self, + circuits: Sequence[cirq.AbstractCircuit], + sweeps: List[cirq.Sweepable], + repetitions: Union[int, List[int]], ): if self._device: for circuit in circuits: self._device.validate_circuit(circuit) if self._validator: - self._validator(circuits, sweeps) + self._validator(circuits, sweeps, repetitions) def run_sweep( self, @@ -60,7 +64,7 @@ def run_sweep( params: cirq.Sweepable, repetitions: int = 1, ) -> List['cirq.Result']: - self._validate_circuit([program], [params]) + self._validate_circuit([program], [params], repetitions) return self._sampler.run_sweep(program, params, repetitions) def run_batch( @@ -71,5 +75,5 @@ def run_batch( ) -> List[List['cirq.Result']]: if params_list is None: params_list = [None] * len(programs) - self._validate_circuit(programs, params_list) + self._validate_circuit(programs, params_list, repetitions) return self._sampler.run_batch(programs, params_list, repetitions) diff --git a/cirq-google/cirq_google/engine/validating_sampler_test.py b/cirq-google/cirq_google/engine/validating_sampler_test.py index a63dfe0d3e0..03a2a5dab2a 100644 --- a/cirq-google/cirq_google/engine/validating_sampler_test.py +++ b/cirq-google/cirq_google/engine/validating_sampler_test.py @@ -22,7 +22,7 @@ def test_device_validation(): sampler = cg.ValidatingSampler( - device=cg.Sycamore23, validator=lambda c, r: True, sampler=cirq.Simulator() + device=cg.Sycamore23, validator=lambda c, s, r: True, sampler=cirq.Simulator() ) # Good qubit @@ -40,7 +40,9 @@ def test_device_validation(): results = sampler.run_sweep(circuit, sweep, repetitions=100) -def _batch_size_less_than_two(circuits: List[cirq.Circuit], sweeps: List[cirq.Sweepable]): +def _batch_size_less_than_two( + circuits: List[cirq.Circuit], sweeps: List[cirq.Sweepable], repetitions: int +): if len(circuits) > 2: raise ValueError('Too many batches') @@ -73,6 +75,25 @@ def test_batch_validation(): with pytest.raises(ValueError, match='Too many batches'): results = sampler.run_batch(circuits, sweeps, repetitions=100) + +def _too_many_reps(circuits: List[cirq.Circuit], sweeps: List[cirq.Sweepable], repetitions: int): + if repetitions > 10000: + raise ValueError('Too many repetitions') + + +def test_sweeps_validation(): + sampler = cg.ValidatingSampler( + device=cirq.UNCONSTRAINED_DEVICE, + validator=_too_many_reps, + sampler=cirq.Simulator(), + ) + q = cirq.GridQubit(2, 2) + circuit = cirq.Circuit(cirq.X(q) ** sympy.Symbol('t'), cirq.measure(q, key='m')) + sweeps = [cirq.Points(key='t', points=[1, 0]), cirq.Points(key='x', points=[0, 1])] + with pytest.raises(ValueError, match='Too many repetitions'): + _ = sampler.run_sweep(circuit, sweeps, repetitions=20000) + + def test_batch_default_sweeps(): sampler = cg.ValidatingSampler() q=cirq.GridQubit(2, 2) From e707fbcc5aadc1511ec0623b1fcb016cd246c47e Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Mon, 8 Nov 2021 13:20:46 -0800 Subject: [PATCH 04/16] Add AbstractEngine Interface This PR adds three groups of classes to cirq_google. The first four classes define abstract base classes (ABC) for the four main types of the Engine: AbstractJob, AbstractProgram, AbstractProcessor, AbstractEngine These classes closely model the currect EngineJob, EngineProgram, EngineProcessor, and Engine classes with the methods turned into abstract models. The second group is a partially implemented sub-classes that define in-memory objects for functionality unlikely to be implemented differently, such as reservations, labels, and descriptions: AbstractLocalJob, AbstractLocalProgram, AbstractLocalProcessor, AbstractLocalEngine Lastly, the final group of classes defines implementation for an Engine emulator backed by a sampler, such as the cirq simulator or other local simulator. SimulatedLocalJob, SimulatedLocalProgram, SimulatedLocalProcessor, SimulatedLocalEngine --- .../cirq_google/engine/abstract_engine.py | 137 +++++ .../cirq_google/engine/abstract_job.py | 201 +++++++ .../engine/abstract_local_engine.py | 183 +++++++ .../engine/abstract_local_engine_test.py | 148 +++++ .../cirq_google/engine/abstract_local_job.py | 169 ++++++ .../engine/abstract_local_job_test.py | 106 ++++ .../engine/abstract_local_processor.py | 410 ++++++++++++++ .../engine/abstract_local_processor_test.py | 505 ++++++++++++++++++ .../engine/abstract_local_program.py | 206 +++++++ .../engine/abstract_local_program_test.py | 156 ++++++ .../cirq_google/engine/abstract_processor.py | 420 +++++++++++++++ .../cirq_google/engine/abstract_program.py | 180 +++++++ .../engine/local_simulation_type.py | 20 + .../engine/simulated_local_engine.py | 45 ++ .../engine/simulated_local_engine_test.py | 183 +++++++ .../cirq_google/engine/simulated_local_job.py | 125 +++++ .../engine/simulated_local_job_test.py | 138 +++++ .../engine/simulated_local_processor.py | 305 +++++++++++ .../engine/simulated_local_processor_test.py | 219 ++++++++ .../engine/simulated_local_program.py | 57 ++ .../engine/validating_sampler_test.py | 3 +- 21 files changed, 3914 insertions(+), 2 deletions(-) create mode 100644 cirq-google/cirq_google/engine/abstract_engine.py create mode 100644 cirq-google/cirq_google/engine/abstract_job.py create mode 100644 cirq-google/cirq_google/engine/abstract_local_engine.py create mode 100644 cirq-google/cirq_google/engine/abstract_local_engine_test.py create mode 100644 cirq-google/cirq_google/engine/abstract_local_job.py create mode 100644 cirq-google/cirq_google/engine/abstract_local_job_test.py create mode 100644 cirq-google/cirq_google/engine/abstract_local_processor.py create mode 100644 cirq-google/cirq_google/engine/abstract_local_processor_test.py create mode 100644 cirq-google/cirq_google/engine/abstract_local_program.py create mode 100644 cirq-google/cirq_google/engine/abstract_local_program_test.py create mode 100644 cirq-google/cirq_google/engine/abstract_processor.py create mode 100644 cirq-google/cirq_google/engine/abstract_program.py create mode 100644 cirq-google/cirq_google/engine/local_simulation_type.py create mode 100644 cirq-google/cirq_google/engine/simulated_local_engine.py create mode 100644 cirq-google/cirq_google/engine/simulated_local_engine_test.py create mode 100644 cirq-google/cirq_google/engine/simulated_local_job.py create mode 100644 cirq-google/cirq_google/engine/simulated_local_job_test.py create mode 100644 cirq-google/cirq_google/engine/simulated_local_processor.py create mode 100644 cirq-google/cirq_google/engine/simulated_local_processor_test.py create mode 100644 cirq-google/cirq_google/engine/simulated_local_program.py diff --git a/cirq-google/cirq_google/engine/abstract_engine.py b/cirq-google/cirq_google/engine/abstract_engine.py new file mode 100644 index 00000000000..42096dae0de --- /dev/null +++ b/cirq-google/cirq_google/engine/abstract_engine.py @@ -0,0 +1,137 @@ +# Copyright 2021 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. +from abc import ABC, abstractmethod +import datetime +from typing import Dict, List, Optional, Sequence, Set, Union + +import cirq +from cirq_google.engine import abstract_program, abstract_processor +from cirq_google.engine.client import quantum +from cirq_google.serialization import Serializer + + +class AbstractEngine(ABC): + """An abstract object representing a collection of quantum procexsors. + + Each processor within the AbstractEngine can be referenced by a string + identifier through the get_processor interface. + + The Engine interface also includes convenience methods to access + programs, jobs, and sampler. + + This is an abstract interface and inheritors must implement the abstract methods. + + """ + + @abstractmethod + def get_program(self, program_id: str) -> abstract_program.AbstractProgram: + """Returns an exsiting AbstractProgram given an identifier. + + Args: + program_id: Unique ID of the program within the parent project. + + Returns: + An AbstractProgram object for the program. + """ + + @abstractmethod + def list_programs( + self, + created_before: Optional[Union[datetime.datetime, datetime.date]] = None, + created_after: Optional[Union[datetime.datetime, datetime.date]] = None, + has_labels: Optional[Dict[str, str]] = None, + ) -> List[abstract_program.AbstractProgram]: + """Returns a list of previously executed quantum programs. + + Args: + created_after: retrieve programs that were created after this date + or time. + created_before: retrieve programs that were created after this date + or time. + has_labels: retrieve programs that have labels on them specified by + this dict. If the value is set to `*`, filters having the label + regardless of the label value will be filtered. For example, to + query programs that have the shape label and have the color + label with value red can be queried using + `{'color: red', 'shape:*'}` + """ + + @abstractmethod + def list_jobs( + self, + created_before: Optional[Union[datetime.datetime, datetime.date]] = None, + created_after: Optional[Union[datetime.datetime, datetime.date]] = None, + has_labels: Optional[Dict[str, str]] = None, + execution_states: Optional[Set[quantum.enums.ExecutionStatus.State]] = None, + ): + """Returns the list of jobs in the project. + + All historical jobs can be retrieved using this method and filtering + options are available too, to narrow down the search baesd on: + * creation time + * job labels + * execution states + + Args: + created_after: retrieve jobs that were created after this date + or time. + created_before: retrieve jobs that were created after this date + or time. + has_labels: retrieve jobs that have labels on them specified by + this dict. If the value is set to `*`, filters having the label + regardless of the label value will be filtered. For example, to + query programs that have the shape label and have the color + label with value red can be queried using + + {'color': 'red', 'shape':'*'} + + execution_states: retrieve jobs that have an execution state that + is contained in `execution_states`. See + `quantum.enums.ExecutionStatus.State` enum for accepted values. + """ + + @abstractmethod + def list_processors(self) -> Sequence[abstract_processor.AbstractProcessor]: + """Returns a list of Processors that the user has visibility to in the + current Engine project. The names of these processors are used to + identify devices when scheduling jobs and gathering calibration metrics. + + Returns: + A list of EngineProcessors to access status, device and calibration + information. + """ + + @abstractmethod + def get_processor(self, processor_id: str) -> abstract_processor.AbstractProcessor: + """Returns an EngineProcessor for a Quantum Engine processor. + + Args: + processor_id: The processor unique identifier. + + Returns: + A EngineProcessor for the processor. + """ + + @abstractmethod + def get_sampler( + self, processor_id: Union[str, List[str]], gate_set: Serializer + ) -> cirq.Sampler: + """Returns a sampler backed by the engine. + + Args: + processor_id: String identifier, or list of string identifiers, + determining which processors may be used when sampling. + gate_set: Determines how to serialize circuits when requesting + samples. + """ diff --git a/cirq-google/cirq_google/engine/abstract_job.py b/cirq-google/cirq_google/engine/abstract_job.py new file mode 100644 index 00000000000..46204e2f13f --- /dev/null +++ b/cirq-google/cirq_google/engine/abstract_job.py @@ -0,0 +1,201 @@ +# Copyright 2021 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. +"""A helper for jobs that have been created on the Quantum Engine.""" +from abc import ABC, abstractmethod +from typing import Dict, Iterator, List, Optional, overload, Tuple, TYPE_CHECKING + +from cirq_google.engine.client import quantum +from cirq_google.engine.calibration_result import CalibrationResult + +import cirq + +if TYPE_CHECKING: + import datetime + from cirq_google.engine.calibration import Calibration + from cirq_google.engine.abstract_engine import AbstractEngine + from cirq_google.engine.abstract_processor import AbstractProcessor + from cirq_google.engine.abstract_program import AbstractProgram + + +class AbstractJob(ABC): + """An abstract object representing a quantum job execution. + + This represents the state of a possibly asynchronous Job being + executed by a simulator, the cloud Engine service, or other means. + + This is an abstract interface that implementers of services or mocks + should implement. It generally represents the execution of a circuit + using a set of parameters called a sweep. It can also represent the + execution of a batch job (a list of circuit/sweep pairs) or the + execution of a calibration request. + + This job may be in a variety of states. It may be scheduling, it may be + executing on a machine, or it may have entered a terminal state + (either succeeding or failing). + + `AbstractJob`s can be iterated over, returning `Result`s. These + `Result`s can also be accessed by index. Note that this will block + until the results are returned. + + """ + + @abstractmethod + def engine(self) -> 'AbstractEngine': + """Returns the parent `AbstractEngine` object.""" + + @abstractmethod + def id(self) -> str: + """Returns the id of this job.""" + + @abstractmethod + def program(self) -> 'AbstractProgram': + """Returns the parent `AbstractProgram`object.""" + + @abstractmethod + def create_time(self) -> 'datetime.datetime': + """Returns when the job was created.""" + + @abstractmethod + def update_time(self) -> 'datetime.datetime': + """Returns when the job was last updated.""" + + @abstractmethod + def description(self) -> str: + """Returns the description of the job.""" + + @abstractmethod + def set_description(self, description: str) -> 'AbstractJob': + """Sets the description of the job. + + Params: + description: The new description for the job. + + Returns: + This `AbstractJob`. + """ + + @abstractmethod + def labels(self) -> Dict[str, str]: + """Returns the labels of the job.""" + + @abstractmethod + def set_labels(self, labels: Dict[str, str]) -> 'AbstractJob': + """Sets (overwriting) the labels for a previously created quantum job. + + Params: + labels: The entire set of new job labels. + + Returns: + This `AbstractJob`. + """ + + @abstractmethod + def add_labels(self, labels: Dict[str, str]) -> 'AbstractJob': + """Adds new labels to a previously created quantum job. + + Params: + labels: New labels to add to the existing job labels. + + Returns: + This `AbstractJob`. + """ + + @abstractmethod + def remove_labels(self, keys: List[str]) -> 'AbstractJob': + """Removes labels with given keys. + + Params: + label_keys: Label keys to remove from the existing job labels. + + Returns: + This `AbstractJob`. + """ + + @abstractmethod + def processor_ids(self) -> List[str]: + """Returns the processor ids provided when the job was created.""" + + @abstractmethod + def execution_status(self) -> quantum.enums.ExecutionStatus.State: + """Return the execution status of the job.""" + + @abstractmethod + def failure(self) -> Optional[Tuple[str, str]]: + """Return failure code and message of the job if present.""" + + @abstractmethod + def get_repetitions_and_sweeps(self) -> Tuple[int, List[cirq.Sweep]]: + """Returns the repetitions and sweeps for the job. + + Returns: + A tuple of the repetition count and list of sweeps. + """ + + @abstractmethod + def get_processor(self) -> 'Optional[AbstractProcessor]': + """Returns the AbstractProcessor for the processor the job is/was run on, + if available, else None.""" + + @abstractmethod + def get_calibration(self) -> Optional['Calibration']: + """Returns the recorded calibration at the time when the job was run, if + one was captured, else None.""" + + @abstractmethod + def cancel(self) -> None: + """Cancel the job.""" + + @abstractmethod + def delete(self) -> None: + """Deletes the job and result, if any.""" + + @abstractmethod + def batched_results(self) -> List[List[cirq.Result]]: + """Returns the job results, blocking until the job is complete. + + This method is intended for batched jobs. Instead of flattening + results into a single list, this will return a List[Result] + for each circuit in the batch. + """ + + @abstractmethod + def results(self) -> List[cirq.Result]: + """Returns the job results, blocking until the job is complete.""" + + @abstractmethod + def calibration_results(self) -> List[CalibrationResult]: + """Returns the results of a run_calibration() call. + + This function will fail if any other type of results were returned. + """ + + def __iter__(self) -> Iterator[cirq.Result]: + return iter(self.results()) + + # pylint: disable=function-redefined + @overload + def __getitem__(self, item: int) -> cirq.Result: + pass + + @overload + def __getitem__(self, item: slice) -> List[cirq.Result]: + pass + + def __getitem__(self, item): + return self.results()[item] + + # pylint: enable=function-redefined + + def __len__(self) -> int: + return len(self.results()) diff --git a/cirq-google/cirq_google/engine/abstract_local_engine.py b/cirq-google/cirq_google/engine/abstract_local_engine.py new file mode 100644 index 00000000000..b42b842966c --- /dev/null +++ b/cirq-google/cirq_google/engine/abstract_local_engine.py @@ -0,0 +1,183 @@ +# Copyright 2021 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 Dict, List, Optional, Sequence, Set, Union, TYPE_CHECKING + +import cirq +from cirq_google.engine.abstract_job import AbstractJob +from cirq_google.engine.abstract_program import AbstractProgram +from cirq_google.engine.abstract_local_processor import AbstractLocalProcessor +from cirq_google.engine.abstract_engine import AbstractEngine +from cirq_google.engine.client import quantum +from cirq_google.serialization import Serializer + + +if TYPE_CHECKING: + import cirq_google + import google.protobuf + + +class AbstractLocalEngine(AbstractEngine): + """Collection of processors that can execute quantum jobs. + + This class assumes that all processors are local. Processors + are given during initialization. Program and job querying + functionality is done by serially querying all child processors. + + """ + + def __init__(self, processors: List[AbstractLocalProcessor]): + for processor in processors: + processor.set_engine(self) + self._processors = {proc.processor_id: proc for proc in processors} + + def get_program(self, program_id: str) -> AbstractProgram: + """Returns an exsiting AbstractProgram given an identifier. + + Iteratibvely checks each processor for the given id. + + Args: + program_id: Unique ID of the program within the parent project. + + Returns: + An AbstractProgram for the program. + + Raises: + KeyError: if program does not exist + """ + for processor in self._processors.values(): + try: + return processor.get_program(program_id) + except KeyError: + continue + raise KeyError(f'Program {program_id} does not exist') + + def list_programs( + self, + created_before: Optional[Union[datetime.datetime, datetime.date]] = None, + created_after: Optional[Union[datetime.datetime, datetime.date]] = None, + has_labels: Optional[Dict[str, str]] = None, + ) -> List[AbstractProgram]: + """Returns a list of previously executed quantum programs. + + Args: + created_after: retrieve programs that were created after this date + or time. + created_before: retrieve programs that were created after this date + or time. + has_labels: retrieve programs that have labels on them specified by + this dict. If the value is set to `*`, filters having the label + regardless of the label value will be filtered. For example, to + query programs that have the shape label and have the color + label with value red can be queried using + `{'color: red', 'shape:*'}` + """ + valid_programs: List[AbstractProgram] = [] + for processor in self._processors.values(): + valid_programs.extend( + processor.list_programs( + created_before=created_before, + created_after=created_after, + has_labels=has_labels, + ) + ) + return valid_programs + + def list_jobs( + self, + created_before: Optional[Union[datetime.datetime, datetime.date]] = None, + created_after: Optional[Union[datetime.datetime, datetime.date]] = None, + has_labels: Optional[Dict[str, str]] = None, + execution_states: Optional[Set[quantum.enums.ExecutionStatus.State]] = None, + ) -> List[AbstractJob]: + """Returns the list of jobs in the project. + + All historical jobs can be retrieved using this method and filtering + options are available too, to narrow down the search baesd on: + * creation time + * job labels + * execution states + + Args: + created_after: retrieve jobs that were created after this date + or time. + created_before: retrieve jobs that were created after this date + or time. + has_labels: retrieve jobs that have labels on them specified by + this dict. If the value is set to `*`, filters having the label + regardless of the label value will be filtered. For example, to + query programs that have the shape label and have the color + label with value red can be queried using + + {'color': 'red', 'shape':'*'} + + execution_states: retrieve jobs that have an execution state that + is contained in `execution_states`. See + `quantum.enums.ExecutionStatus.State` enum for accepted values. + """ + valid_jobs: List[AbstractJob] = [] + for processor in self._processors.values(): + programs = processor.list_programs( + created_before=created_before, created_after=created_after, has_labels=has_labels + ) + for program in programs: + valid_jobs.extend( + program.list_jobs( + created_before=created_before, + created_after=created_after, + has_labels=has_labels, + execution_states=execution_states, + ) + ) + return valid_jobs + + def list_processors(self) -> Sequence[AbstractLocalProcessor]: + """Returns a list of Processors that the user has visibility to in the + current Engine project. The names of these processors are used to + identify devices when scheduling jobs and gathering calibration metrics. + + Returns: + A list of EngineProcessors to access status, device and calibration + information. + """ + return list(self._processors.values()) + + def get_processor(self, processor_id: str) -> AbstractLocalProcessor: + """Returns an EngineProcessor for a Quantum Engine processor. + + Args: + processor_id: The processor unique identifier. + + Returns: + A EngineProcessor for the processor. + """ + return self._processors[processor_id] + + def get_sampler( + self, processor_id: Union[str, List[str]], gate_set: Optional[Serializer] = None + ) -> cirq.Sampler: + """Returns a sampler backed by the engine. + + Args: + processor_id: String identifier, or list of string identifiers, + determining which processors may be used when sampling. + gate_set: Determines how to serialize circuits when requesting + samples. + + Raises: + ValueError: if multiple processor ids are given. + """ + if not isinstance(processor_id, str): + raise ValueError(f'Invalid processor {processor_id}') + return self._processors[processor_id].get_sampler(gate_set=gate_set) diff --git a/cirq-google/cirq_google/engine/abstract_local_engine_test.py b/cirq-google/cirq_google/engine/abstract_local_engine_test.py new file mode 100644 index 00000000000..76927107580 --- /dev/null +++ b/cirq-google/cirq_google/engine/abstract_local_engine_test.py @@ -0,0 +1,148 @@ +# Copyright 2021 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 Dict, List, Optional, Union +import pytest + +import cirq + +from cirq_google.engine.abstract_local_job_test import NothingJob +from cirq_google.engine.abstract_local_program_test import NothingProgram +from cirq_google.engine.abstract_local_engine import AbstractLocalEngine +from cirq_google.engine.abstract_local_processor import AbstractLocalProcessor +from cirq_google.engine.abstract_program import AbstractProgram + + +class ProgramDictProcessor(AbstractLocalProcessor): + """A processor that has a dictionary of programs for testing.""" + + def __init__(self, programs: Dict[str, AbstractProgram], **kwargs): + super().__init__(**kwargs) + self._programs = programs + + def get_calibration(self, *args, **kwargs): + pass + + def get_latest_calibration(self, *args, **kwargs): + pass + + def get_current_calibration(self, *args, **kwargs): + pass + + def get_device(self, *args, **kwargs): + pass + + def get_device_specification(self, *args, **kwargs): + pass + + def health(self, *args, **kwargs): + pass + + def list_calibrations(self, *args, **kwargs): + pass + + def run(self, *args, **kwargs): + pass + + def run_batch(self, *args, **kwargs): + pass + + def run_calibration(self, *args, **kwargs): + pass + + def run_sweep(self, *args, **kwargs): + pass + + def get_sampler(self, *args, **kwargs): + return cirq.Simulator() + + def supported_languages(self, *args, **kwargs): + pass + + def list_programs( + self, + created_before: Optional[Union[datetime.datetime, datetime.date]] = None, + created_after: Optional[Union[datetime.datetime, datetime.date]] = None, + has_labels: Optional[Dict[str, str]] = None, + ): + """Lists all programs regardless of filters. + + This isn't really correct, but we don't want to test test functionality.""" + return self._programs.values() + + def get_program(self, program_id: str) -> AbstractProgram: + return self._programs[program_id] + + +class NothingEngine(AbstractLocalEngine): + """Engine for Testing.""" + + def __init__(self, processors: List[AbstractLocalProcessor]): + super().__init__(processors) + + +def test_get_processor(): + processor1 = ProgramDictProcessor(programs=[], processor_id='test') + engine = NothingEngine([processor1]) + assert engine.get_processor('test') == processor1 + assert engine.get_processor('test').engine() == engine + + +def test_list_processor(): + processor1 = ProgramDictProcessor(programs=[], processor_id='proc') + processor2 = ProgramDictProcessor(programs=[], processor_id='crop') + engine = NothingEngine([processor1, processor2]) + assert engine.get_processor('proc') == processor1 + assert engine.get_processor('crop') == processor2 + assert engine.get_processor('proc').engine() == engine + assert engine.get_processor('crop').engine() == engine + assert set(engine.list_processors()) == {processor1, processor2} + + +def test_get_programs(): + program1 = NothingProgram([cirq.Circuit()], None) + job1 = NothingJob( + job_id='test3', processor_id='test1', parent_program=program1, repetitions=100, sweeps=[] + ) + program1.add_job('jerb', job1) + job1.add_labels({'color': 'blue'}) + + program2 = NothingProgram([cirq.Circuit()], None) + job2 = NothingJob( + job_id='test4', processor_id='test2', parent_program=program2, repetitions=100, sweeps=[] + ) + program2.add_job('jerb2', job2) + job2.add_labels({'color': 'red'}) + + processor1 = ProgramDictProcessor(programs={'prog1': program1}, processor_id='proc') + processor2 = ProgramDictProcessor(programs={'prog2': program2}, processor_id='crop') + engine = NothingEngine([processor1, processor2]) + + assert engine.get_program('prog1') == program1 + + with pytest.raises(KeyError, match='does not exist'): + engine.get_program('invalid_id') + + assert set(engine.list_programs()) == {program1, program2} + assert set(engine.list_jobs()) == {job1, job2} + assert engine.list_jobs(has_labels={'color': 'blue'}) == [job1] + assert engine.list_jobs(has_labels={'color': 'red'}) == [job2] + + +def test_get_sampler(): + processor = ProgramDictProcessor(programs={}, processor_id='grocery') + engine = NothingEngine([processor]) + assert isinstance(engine.get_sampler('grocery'), cirq.Sampler) + with pytest.raises(ValueError, match='Invalid processor'): + engine.get_sampler(['blah']) diff --git a/cirq-google/cirq_google/engine/abstract_local_job.py b/cirq-google/cirq_google/engine/abstract_local_job.py new file mode 100644 index 00000000000..6fc8788e093 --- /dev/null +++ b/cirq-google/cirq_google/engine/abstract_local_job.py @@ -0,0 +1,169 @@ +# Copyright 2021 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. +"""A helper for jobs that have been created on the Quantum Engine.""" +import copy +import datetime + +from typing import Dict, List, Optional, Tuple, TYPE_CHECKING + +import cirq +from cirq_google.engine import calibration +from cirq_google.engine.abstract_job import AbstractJob + +if TYPE_CHECKING: + from cirq_google.engine.abstract_local_program import AbstractLocalProgram + from cirq_google.engine.abstract_local_processor import AbstractLocalProcessor + from cirq_google.engine.abstract_local_engine import AbstractLocalEngine + + +class AbstractLocalJob(AbstractJob): + """A job that handles labels and descriptions locally in-memory. + + This class is designed to make writing custom AbstractJob objects + that function in-memory easier. This class will handle basic functionality + expected to be common across all local implementations. + + Implementors of this class should write the following functions: + - Status functions: execution_status, failure + - Action functions: cancel, delete + - Result functions: results, batched_results, calibration_results + ` + Attributes: + processor_ids: A string list of processor ids that this job can be run on. + processor_id: If provided, the processor id that the job was run on. + If not provided, assumed to be the first element of processor_ids + parent_program: Program containing this job + repetitions: number of repetitions for each parameter set + sweeps: list of Sweeps that this job should iterate through. + """ + + def __init__( + self, + *, + job_id: str, + parent_program: 'AbstractLocalProgram', + repetitions: int, + sweeps: List[cirq.Sweep], + processor_id: str = '', + ): + self._id = job_id + self._processor_id = processor_id + self._parent_program = parent_program + self._repetitions = repetitions + self._sweeps = sweeps + self._create_time = datetime.datetime.now() + self._update_time = datetime.datetime.now() + self._description = '' + self._labels: Dict[str, str] = {} + + def engine(self) -> 'AbstractLocalEngine': + """Returns the parent program's `AbstractEngine` object.""" + return self._parent_program.engine() + + def id(self) -> str: + """Returns the identifier of this job.""" + return self._id + + def program(self) -> 'AbstractLocalProgram': + """Returns the parent `AbstractLocalProgram` object.""" + return self._parent_program + + def create_time(self) -> 'datetime.datetime': + """Returns when the job was created.""" + return self._create_time + + def update_time(self) -> 'datetime.datetime': + """Returns when the job was last updated.""" + return self._update_time + + def description(self) -> str: + """Returns the description of the job.""" + return self._description + + def set_description(self, description: str) -> 'AbstractJob': + """Sets the description of the job. + + Params: + description: The new description for the job. + + Returns: + This AbstractJob. + """ + self._description = description + return self + + def labels(self) -> Dict[str, str]: + """Returns the labels of the job.""" + return copy.copy(self._labels) + + def set_labels(self, labels: Dict[str, str]) -> 'AbstractJob': + """Sets (overwriting) the labels for a previously created quantum job. + + Params: + labels: The entire set of new job labels. + + Returns: + This AbstractJob. + """ + self._labels = copy.copy(labels) + return self + + def add_labels(self, labels: Dict[str, str]) -> 'AbstractJob': + """Adds new labels to a previously created quantum job. + + Params: + labels: New labels to add to the existing job labels. + + Returns: + This AbstractJob. + """ + for key in labels: + self._labels[key] = labels[key] + return self + + def remove_labels(self, keys: List[str]) -> 'AbstractJob': + """Removes labels with given keys from the labels of a previously + created quantum job. + + Params: + label_keys: Label keys to remove from the existing job labels. + + Returns: + This AbstractJob. + """ + for key in keys: + del self._labels[key] + return self + + def processor_ids(self) -> List[str]: + """Returns the processor ids provided when the job was created.""" + return [self._processor_id] + + def get_repetitions_and_sweeps(self) -> Tuple[int, List[cirq.Sweep]]: + """Returns the repetitions and sweeps for the job. + + Returns: + A tuple of the repetition count and list of sweeps. + """ + return (self._repetitions, self._sweeps) + + def get_processor(self) -> 'AbstractLocalProcessor': + """Returns the AbstractProcessor for the processor the job is/was run on, + if available, else None.""" + return self.engine().get_processor(self._processor_id) + + def get_calibration(self) -> Optional[calibration.Calibration]: + """Returns the recorded calibration at the time when the job was created, + from the parent Engine object.""" + return self.get_processor().get_latest_calibration(int(self._create_time.timestamp())) diff --git a/cirq-google/cirq_google/engine/abstract_local_job_test.py b/cirq-google/cirq_google/engine/abstract_local_job_test.py new file mode 100644 index 00000000000..d0a1d953cc3 --- /dev/null +++ b/cirq-google/cirq_google/engine/abstract_local_job_test.py @@ -0,0 +1,106 @@ +# Copyright 2021 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. +"""A helper for jobs that have been created on the Quantum Engine.""" +from typing import List, Optional, Tuple +import datetime +import cirq + +from cirq_google.engine.client import quantum +from cirq_google.engine.calibration_result import CalibrationResult +from cirq_google.engine.abstract_local_job import AbstractLocalJob + + +class NothingJob(AbstractLocalJob): + """Blank version of AbstractLocalJob for testing.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._status = quantum.enums.ExecutionStatus.State.READY + + def execution_status(self) -> quantum.enums.ExecutionStatus.State: + return self._status + + def failure(self) -> Optional[Tuple[str, str]]: + return ('failed', 'failure code') # coverage: ignore + + def cancel(self) -> None: + pass + + def delete(self) -> None: + pass + + def batched_results(self) -> List[List[cirq.Result]]: + return [] # coverage: ignore + + def results(self) -> List[cirq.Result]: + return [] # coverage: ignore + + def calibration_results(self) -> List[CalibrationResult]: + return [] # coverage: ignore + + +def test_description_and_labels(): + job = NothingJob( + job_id='test', processor_id='pot_of_gold', parent_program=None, repetitions=100, sweeps=[] + ) + assert job.processor_ids() == ['pot_of_gold'] + assert not job.description() + job.set_description('nothing much') + assert job.description() == 'nothing much' + job.set_description('other desc') + assert job.description() == 'other desc' + assert job.labels() == {} + job.set_labels({'key': 'green'}) + assert job.labels() == {'key': 'green'} + job.add_labels({'door': 'blue', 'curtains': 'white'}) + assert job.labels() == {'key': 'green', 'door': 'blue', 'curtains': 'white'} + job.remove_labels(['key', 'door']) + assert job.labels() == {'curtains': 'white'} + job.set_labels({'walls': 'gray'}) + assert job.labels() == {'walls': 'gray'} + + +def test_reps_and_Sweeps(): + job = NothingJob( + job_id='test', + processor_id='grill', + parent_program=None, + repetitions=100, + sweeps=[cirq.Linspace('t', 0, 10, 0.1)], + ) + assert job.get_repetitions_and_sweeps() == (100, [cirq.Linspace('t', 0, 10, 0.1)]) + + +def test_create_update_time(): + job = NothingJob( + job_id='test', processor_id='pot_of_gold', parent_program=None, repetitions=100, sweeps=[] + ) + create_time = datetime.datetime.fromtimestamp(1000) + update_time = datetime.datetime.fromtimestamp(2000) + job._create_time = create_time + job._update_time = update_time + assert job.create_time() == create_time + assert job.update_time() == update_time + + +def test_engineand_processor_calibration(): + pass + + +def test_cwprogramengine(): + pass + + +def test_abstract_functions(): + pass diff --git a/cirq-google/cirq_google/engine/abstract_local_processor.py b/cirq-google/cirq_google/engine/abstract_local_processor.py new file mode 100644 index 00000000000..3553a7f3321 --- /dev/null +++ b/cirq-google/cirq_google/engine/abstract_local_processor.py @@ -0,0 +1,410 @@ +# Copyright 2021 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. +from abc import abstractmethod +import copy +import datetime + +from typing import Dict, List, Optional, TYPE_CHECKING, Union +from google.protobuf.timestamp_pb2 import Timestamp + +from cirq_google.engine import calibration +from cirq_google.engine.client.quantum import types as qtypes +from cirq_google.engine.client.quantum import enums as qenums +from cirq_google.engine.abstract_processor import AbstractProcessor +from cirq_google.engine.abstract_program import AbstractProgram + +if TYPE_CHECKING: + from cirq_google.engine.abstract_engine import AbstractEngine + from cirq_google.engine.abstract_local_program import AbstractLocalProgram + + +def _to_timestamp(union_time: Union[None, datetime.datetime, datetime.timedelta]): + """Translate a datetime or timedelta into a number of seconds since epoch.""" + if isinstance(union_time, datetime.timedelta): + return int((datetime.datetime.now() + union_time).timestamp()) + elif isinstance(union_time, datetime.datetime): + return int(union_time.timestamp()) + return None + + +class AbstractLocalProcessor(AbstractProcessor): + """Partial implementation of AbstractProcessor using in-memory objects. + + This implements reservation creation and scheduling using an in-memory + list for time slots and reservations. Any time slot not specified by + initialization is assumed to be UNALLOCATED (available for reservation). + + Attributes: + processor_id: Unique string id of the processor. + engine: The parent `AbstractEngine` object, if available. + expected_down_time: Optional datetime of the next expected downtime. + For informational purpose only.yy + expected_recovery_time: Optional datetime when the processor is + expected to be available again. For informational purpose only. + schedule: List of time slots that the scheduling/reservation should + use. All time slots must be non-overlapping. + project_name: A project_name for resource naming. + """ + + def __init__( + self, + *, + processor_id: str, + engine: Optional['AbstractEngine'] = None, + expected_down_time: Optional[datetime.datetime] = None, + expected_recovery_time: Optional[datetime.datetime] = None, + schedule: Optional[List[qtypes.QuantumTimeSlot]] = None, + project_name: str = 'fake_project', + ): + self._engine = engine + self._expected_recovery_time = expected_recovery_time + self._expected_down_time = expected_down_time + self._reservations: Dict[str, qtypes.QuantumReservation] = {} + self._reservation_id_counter = 0 + self._processor_id = processor_id + self._project_name = project_name + + if schedule is None: + self._schedule = [ + qtypes.QuantumTimeSlot( + processor_name=self._processor_id, + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ) + ] + else: + self._schedule = copy.copy(schedule) + self._schedule.sort(key=lambda t: t.start_time.seconds or -1) + + for idx in range(len(self._schedule) - 1): + if self._schedule[idx].end_time.seconds > self._schedule[idx + 1].start_time.seconds: + raise ValueError('Time slots cannot overlap!') + + @property + def processor_id(self) -> str: + """Unique string id of the processor.""" + return self._processor_id + + def engine(self) -> Optional['AbstractEngine']: + """Returns the parent Engine object. + + Returns: + The program's parent Engine. + + Raises: + ValueError: if no engine has been defined for this processor. + """ + return self._engine + + def set_engine(self, engine): + """Sets the parent processor.""" + self._engine = engine + + def expected_down_time(self) -> 'Optional[datetime.datetime]': + """Returns the start of the next expected down time of the processor, if + set.""" + return self._expected_down_time + + def expected_recovery_time(self) -> 'Optional[datetime.datetime]': + """Returns the expected the processor should be available, if set.""" + return self._expected_recovery_time + + def _create_id(self, id_type: str = 'reservation') -> str: + """Creates a unique resource id for child objects.""" + self._reservation_id_counter += 1 + return ( + f'projects/{self._project_name}/' + f'processors/{self._processor_id}/' + f'{id_type}/{self._reservation_id_counter}' + ) + + def _reservation_to_time_slot( + self, reservation: qtypes.QuantumReservation + ) -> qtypes.QuantumTimeSlot: + """Changes a reservation object into a time slot object.""" + return qtypes.QuantumTimeSlot( + processor_name=self._processor_id, + start_time=Timestamp(seconds=reservation.start_time.seconds), + end_time=Timestamp(seconds=reservation.end_time.seconds), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, + ) + + def _insert_reservation_into(self, time_slot: qtypes.QuantumTimeSlot) -> None: + """Inserts a new reservation time slot into the ordered schedule. + + If this reservation overlaps with existing time slots, these slots will be + shortened, removed, or split to insert the new reservation. + """ + new_schedule = [] + time_slot_inserted = False + for t in self._schedule: + if t.end_time.seconds and t.end_time.seconds <= time_slot.start_time.seconds: + # [--time_slot--] + # [--t--] + new_schedule.append(t) + continue + if t.start_time.seconds and t.start_time.seconds >= time_slot.end_time.seconds: + # [--time_slot--] + # [--t--] + new_schedule.append(t) + continue + if t.start_time.seconds and time_slot.start_time.seconds <= t.start_time.seconds: + if not time_slot_inserted: + new_schedule.append(time_slot) + time_slot_inserted = True + if not t.end_time.seconds or t.end_time.seconds > time_slot.end_time.seconds: + # [--time_slot---] + # [----t-----] + t.start_time.seconds = time_slot.end_time.seconds + new_schedule.append(t) + # if t.end_time < time_slot.end_time + # [------time_slot-----] + # [-----t-----] + # t should be removed + else: + if not t.end_time.seconds or t.end_time.seconds > time_slot.end_time.seconds: + # [-------------t---------] + # [---time_slot---] + # t should be split + start = qtypes.QuantumTimeSlot( + processor_name=self._processor_id, + end_time=Timestamp(seconds=time_slot.start_time.seconds), + slot_type=t.slot_type, + ) + if t.start_time.seconds: + start.start_time.seconds = t.start_time.seconds + end = qtypes.QuantumTimeSlot( + processor_name=self._processor_id, + start_time=Timestamp(seconds=time_slot.end_time.seconds), + slot_type=t.slot_type, + ) + if t.end_time.seconds: + end.end_time.seconds = t.end_time.seconds + + new_schedule.append(start) + new_schedule.append(time_slot) + new_schedule.append(end) + + else: + # [----t-----] + # [---time_slot---] + t.end_time.seconds = time_slot.start_time.seconds + new_schedule.append(t) + new_schedule.append(time_slot) + time_slot_inserted = True + + if not time_slot_inserted: + new_schedule.append(time_slot) + self._schedule = new_schedule + + def _is_available(self, time_slot: qtypes.QuantumTimeSlot) -> bool: + """Returns True if the slot is available for reservation.""" + for t in self._schedule: + if t.slot_type == qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED: + continue + if t.end_time.seconds and t.end_time.seconds <= time_slot.start_time.seconds: + continue + if t.start_time.seconds and t.start_time.seconds >= time_slot.end_time.seconds: + continue + return False + return True + + def create_reservation( + self, + start_time: datetime.datetime, + end_time: datetime.datetime, + whitelisted_users: Optional[List[str]] = None, + ) -> qtypes.QuantumReservation: + """Creates a reservation on this processor. + + Args: + start_time: the starting date/time of the reservation. + end_time: the ending date/time of the reservation. + whitelisted_users: a list of emails that are allowed + to send programs during this reservation (in addition to users + with permission "quantum.reservations.use" on the project). + + Raises: + ValueError: if start_time is after end_time. + """ + if end_time < start_time: + raise ValueError('End time of reservation must be after the start time') + reservation_id = self._create_id() + new_reservation = qtypes.QuantumReservation( + name=reservation_id, + start_time=Timestamp(seconds=int(start_time.timestamp())), + end_time=Timestamp(seconds=int(end_time.timestamp())), + whitelisted_users=whitelisted_users, + ) + time_slot = self._reservation_to_time_slot(new_reservation) + if not self._is_available(time_slot): + raise ValueError('Time slot is not available for reservations') + + self._reservations[reservation_id] = new_reservation + self._insert_reservation_into(time_slot) + return new_reservation + + def remove_reservation(self, reservation_id: str): + """Removes a reservation on this processor.""" + if reservation_id in self._reservations: + del self._reservations[reservation_id] + + def get_reservation(self, reservation_id: str): + """Retrieve a reservation given its id.""" + if reservation_id in self._reservations: + return self._reservations[reservation_id] + else: + return None + + def update_reservation( + self, + reservation_id: str, + start_time: datetime.datetime = None, + end_time: datetime.datetime = None, + whitelisted_users: List[str] = None, + ): + """Updates a reservation with new information. + + Updates a reservation with a new start date, end date, or + list of additional users. For each field, it the argument is left as + None, it will not be updated. + """ + if reservation_id not in self._reservations: + raise ValueError(f'Reservation id {reservation_id} does not exist.') + if start_time: + self._reservations[reservation_id].start_time.seconds = _to_timestamp(start_time) + if end_time: + self._reservations[reservation_id].end_time.seconds = _to_timestamp(end_time) + if whitelisted_users: + del self._reservations[reservation_id].whitelisted_users[:] + self._reservations[reservation_id].whitelisted_users.extend(whitelisted_users) + + def list_reservations( + self, + from_time: Union[None, datetime.datetime, datetime.timedelta] = datetime.timedelta(), + to_time: Union[None, datetime.datetime, datetime.timedelta] = datetime.timedelta(weeks=2), + ): + """Retrieves the reservations from a processor. + + Only reservations from this processor and project will be + returned. The schedule may be filtered by starting and ending time. + + Args: + from_time: Filters the returned reservations to only include entries + that end no earlier than the given value. Specified either as an + absolute time (datetime.datetime) or as a time relative to now + (datetime.timedelta). Defaults to now (a relative time of 0). + Set to None to omit this filter. + to_time: Filters the returned reservations to only include entries + that start no later than the given value. Specified either as an + absolute time (datetime.datetime) or as a time relative to now + (datetime.timedelta). Defaults to two weeks from now (a relative + time of two weeks). Set to None to omit this filter. + + Returns: + A list of reservations. + """ + start_timestamp = _to_timestamp(from_time) + end_timestamp = _to_timestamp(to_time) + reservation_list = [] + for reservation in self._reservations.values(): + if start_timestamp and reservation.start_time.seconds < start_timestamp: + continue + if end_timestamp and reservation.end_time.seconds > end_timestamp: + continue + reservation_list.append(reservation) + return reservation_list + + def get_schedule( + self, + from_time: Union[None, datetime.datetime, datetime.timedelta] = datetime.timedelta(), + to_time: Union[None, datetime.datetime, datetime.timedelta] = datetime.timedelta(weeks=2), + time_slot_type: Optional[qenums.QuantumTimeSlot.TimeSlotType] = None, + ) -> List[qtypes.QuantumTimeSlot]: + """Retrieves the schedule for a processor. + + The schedule may be filtered by time. + + Args: + from_time: Filters the returned schedule to only include entries + that end no earlier than the given value. Specified either as an + absolute time (datetime.datetime) or as a time relative to now + (datetime.timedelta). Defaults to now (a relative time of 0). + Set to None to omit this filter. + to_time: Filters the returned schedule to only include entries + that start no later than the given value. Specified either as an + absolute time (datetime.datetime) or as a time relative to now + (datetime.timedelta). Defaults to two weeks from now (a relative + time of two weeks). Set to None to omit this filter. + time_slot_type: Filters the returned schedule to only include + entries with a given type (e.g. maintenance, open swim). + Defaults to None. Set to None to omit this filter. + + Returns: + Time slots that fit the criteria. + """ + time_slots: List[qtypes.QuantumTimeSlot] = [] + start_timestamp = _to_timestamp(from_time) + end_timestamp = _to_timestamp(to_time) + for slot in self._schedule: + if ( + start_timestamp + and slot.end_time.seconds + and slot.end_time.seconds < start_timestamp + ): + continue + if ( + end_timestamp + and slot.start_time.seconds + and slot.start_time.seconds > end_timestamp + ): + continue + time_slots.append(slot) + return time_slots + + @abstractmethod + def get_latest_calibration(self, timestamp: int) -> Optional[calibration.Calibration]: + """Returns the latest calibration with the provided timestamp or earlier.""" + + @abstractmethod + def get_program(self, program_id: str) -> AbstractProgram: + """Returns an AbstractProgram for an existing Quantum Engine program. + + Args: + program_id: Unique ID of the program within the parent project. + + Returns: + An AbstractProgram for the program. + """ + + @abstractmethod + def list_programs( + self, + created_before: Optional[Union[datetime.datetime, datetime.date]] = None, + created_after: Optional[Union[datetime.datetime, datetime.date]] = None, + has_labels: Optional[Dict[str, str]] = None, + ) -> List['AbstractLocalProgram']: + """Returns a list of previously executed quantum programs. + + Args: + created_after: retrieve programs that were created after this date + or time. + created_before: retrieve programs that were created after this date + or time. + has_labels: retrieve programs that have labels on them specified by + this dict. If the value is set to `*`, filters having the label + regardless of the label value will be filtered. For example, to + query programs that have the shape label and have the color + label with value red can be queried using + `{'color: red', 'shape:*'}` + """ diff --git a/cirq-google/cirq_google/engine/abstract_local_processor_test.py b/cirq-google/cirq_google/engine/abstract_local_processor_test.py new file mode 100644 index 00000000000..ab033e02bb8 --- /dev/null +++ b/cirq-google/cirq_google/engine/abstract_local_processor_test.py @@ -0,0 +1,505 @@ +# Copyright 2021 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 +import pytest + +from google.protobuf.timestamp_pb2 import Timestamp + +from cirq_google.engine.client.quantum import types as qtypes +from cirq_google.engine.client.quantum import enums as qenums +from cirq_google.engine.abstract_local_processor import AbstractLocalProcessor + + +def _time(seconds_from_epoch: int): + """Shorthand to abbreviate datetimes from epochs.""" + return datetime.datetime.fromtimestamp(seconds_from_epoch) + + +class NothingProcessor(AbstractLocalProcessor): + """A processor for testing.""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def get_calibration(self, *args, **kwargs): + pass + + def get_latest_calibration(self, *args, **kwargs): + pass + + def get_current_calibration(self, *args, **kwargs): + pass + + def get_device(self, *args, **kwargs): + pass + + def get_device_specification(self, *args, **kwargs): + pass + + def health(self, *args, **kwargs): + pass + + def list_calibrations(self, *args, **kwargs): + pass + + def run(self, *args, **kwargs): + pass + + def run_batch(self, *args, **kwargs): + pass + + def run_calibration(self, *args, **kwargs): + pass + + def run_sweep(self, *args, **kwargs): + pass + + def get_sampler(self, *args, **kwargs): + pass + + def supported_languages(self, *args, **kwargs): + pass + + def list_programs(self, *args, **kwargs): + pass + + def get_program(self, *args, **kwargs): + pass + + +def test_datetime(): + recovery_time = datetime.datetime.now() + down_time = datetime.datetime.now() - datetime.timedelta(hours=2) + + p = NothingProcessor( + processor_id='test', expected_down_time=down_time, expected_recovery_time=recovery_time + ) + assert p.expected_down_time() == down_time + assert p.expected_recovery_time() == recovery_time + + +def test_bad_reservation(): + p = NothingProcessor(processor_id='test') + with pytest.raises(ValueError, match='after the start time'): + _ = p.create_reservation( + start_time=_time(2000), + end_time=_time(1000), + ) + + +def test_reservations(): + p = NothingProcessor(processor_id='test') + start_reservation = datetime.datetime.now() + end_reservation = datetime.datetime.now() + datetime.timedelta(hours=2) + users = ['dstrain@google.com'] + + # Create Reservation + reservation = p.create_reservation( + start_time=start_reservation, end_time=end_reservation, whitelisted_users=users + ) + assert reservation.start_time.seconds == int(start_reservation.timestamp()) + assert reservation.end_time.seconds == int(end_reservation.timestamp()) + assert reservation.whitelisted_users == users + + # Get Reservation + assert p.get_reservation(reservation.name) == reservation + assert p.get_reservation('nothing_to_see_here') is None + + # Update reservation + end_reservation = datetime.datetime.now() + datetime.timedelta(hours=3) + p.update_reservation(reservation_id=reservation.name, end_time=end_reservation) + reservation = p.get_reservation(reservation.name) + assert reservation.end_time.seconds == int(end_reservation.timestamp()) + start_reservation = datetime.datetime.now() + datetime.timedelta(hours=1) + p.update_reservation(reservation_id=reservation.name, start_time=start_reservation) + reservation = p.get_reservation(reservation.name) + assert reservation.start_time.seconds == int(start_reservation.timestamp()) + users = ['dstrain@google.com', 'dabacon@google.com'] + p.update_reservation(reservation_id=reservation.name, whitelisted_users=users) + reservation = p.get_reservation(reservation.name) + assert reservation.whitelisted_users == users + + with pytest.raises(ValueError, match='does not exist'): + p.update_reservation(reservation_id='invalid', whitelisted_users=users) + + +def test_list_reservations(): + p = NothingProcessor(processor_id='test') + now = datetime.datetime.now() + hour = datetime.timedelta(hours=1) + users = ['abc@def.com'] + + reservation1 = p.create_reservation( + start_time=now - hour, end_time=now, whitelisted_users=users + ) + reservation2 = p.create_reservation( + start_time=now, end_time=now + hour, whitelisted_users=users + ) + reservation3 = p.create_reservation( + start_time=now + hour, end_time=now + 2 * hour, whitelisted_users=users + ) + + assert p.list_reservations(now - 2 * hour, now + 3 * hour) == [ + reservation1, + reservation2, + reservation3, + ] + assert p.list_reservations(now - 0.5 * hour, now + 3 * hour) == [reservation2, reservation3] + assert p.list_reservations(now + 0.5 * hour, now + 3 * hour) == [reservation3] + assert p.list_reservations(now - 0.5 * hour, now + 1.5 * hour) == [reservation2] + assert p.list_reservations(now - 1.5 * hour, now + 1.5 * hour) == [reservation1, reservation2] + + assert p.list_reservations(-0.5 * hour, 3 * hour) == [reservation2, reservation3] + assert p.list_reservations(0.5 * hour, 3 * hour) == [reservation3] + assert p.list_reservations(-0.5 * hour, 1.5 * hour) == [reservation2] + assert p.list_reservations(-1.5 * hour, 1.5 * hour) == [reservation1, reservation2] + + assert p.list_reservations(now - 2 * hour, None) == [reservation1, reservation2, reservation3] + + p.remove_reservation(reservation1.name) + assert p.list_reservations(now - 2 * hour, None) == [reservation2, reservation3] + + +def test_bad_schedule(): + time_slot1 = qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1000), + end_time=Timestamp(seconds=3000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, + ) + time_slot2 = qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=2000), + end_time=Timestamp(seconds=4000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, + ) + with pytest.raises(ValueError, match='cannot overlap'): + _ = NothingProcessor(processor_id='test', schedule=[time_slot1, time_slot2]) + + +def test_get_schedule(): + time_slot = qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1000), + end_time=Timestamp(seconds=2000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, + ) + p = NothingProcessor(processor_id='test', schedule=[time_slot]) + assert p.get_schedule(from_time=_time(500), to_time=_time(2500)) == [time_slot] + assert p.get_schedule(from_time=_time(1500), to_time=_time(2500)) == [time_slot] + assert p.get_schedule(from_time=_time(500), to_time=_time(1500)) == [time_slot] + assert p.get_schedule(from_time=_time(500), to_time=_time(750)) == [] + assert p.get_schedule(from_time=_time(2500), to_time=_time(3000)) == [] + # check unbounded cases + unbounded_start = qtypes.QuantumTimeSlot( + processor_name='test', + end_time=Timestamp(seconds=1000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, + ) + unbounded_end = qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=2000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, + ) + p = NothingProcessor(processor_id='test', schedule=[unbounded_start, unbounded_end]) + assert ( + p.get_schedule( + from_time=_time(500), + to_time=_time(2500), + ) + == [unbounded_start, unbounded_end] + ) + assert ( + p.get_schedule( + from_time=_time(1500), + to_time=_time(2500), + ) + == [unbounded_end] + ) + assert ( + p.get_schedule( + from_time=_time(500), + to_time=_time(1500), + ) + == [unbounded_start] + ) + assert ( + p.get_schedule( + from_time=_time(1200), + to_time=_time(1500), + ) + == [] + ) + + +@pytest.mark.parametrize( + ('time_slot'), + ( + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1000), + end_time=Timestamp(seconds=2000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + end_time=Timestamp(seconds=2000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, + ), + ), +) +def test_create_reservation_not_available(time_slot): + p = NothingProcessor(processor_id='test', schedule=[time_slot]) + with pytest.raises(ValueError, match='Time slot is not available for reservations'): + p.create_reservation( + start_time=_time(500), + end_time=_time(1500), + ) + + +def test_create_reservation_open_time_slots(): + time_slot = qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1000), + end_time=Timestamp(seconds=2000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ) + p = NothingProcessor(processor_id='test', schedule=[time_slot]) + p.create_reservation( + start_time=_time(500), + end_time=_time(1500), + ) + assert p.get_schedule(from_time=_time(0), to_time=_time(2500)) == [ + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=500), + end_time=Timestamp(seconds=1500), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1500), + end_time=Timestamp(seconds=2000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ), + ] + + +def test_create_reservation_split_time_slots(): + time_slot = qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1000), + end_time=Timestamp(seconds=2000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ) + p = NothingProcessor(processor_id='test', schedule=[time_slot]) + p.create_reservation( + start_time=_time(1200), + end_time=_time(1500), + ) + assert p.get_schedule(from_time=_time(0), to_time=_time(2500)) == [ + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1000), + end_time=Timestamp(seconds=1200), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1200), + end_time=Timestamp(seconds=1500), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1500), + end_time=Timestamp(seconds=2000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ), + ] + + +def test_create_reservation_add_at_end(): + time_slot = qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1000), + end_time=Timestamp(seconds=2000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ) + p = NothingProcessor(processor_id='test', schedule=[time_slot]) + p.create_reservation( + start_time=_time(2500), + end_time=_time(3500), + ) + assert p.get_schedule(from_time=_time(0), to_time=_time(2500)) == [ + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1000), + end_time=Timestamp(seconds=2000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=2500), + end_time=Timestamp(seconds=3500), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, + ), + ] + + +def test_create_reservation_border_conditions(): + time_slot = qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1000), + end_time=Timestamp(seconds=2000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ) + p = NothingProcessor(processor_id='test', schedule=[time_slot]) + p.create_reservation( + start_time=_time(1900), + end_time=_time(2000), + ) + p.create_reservation( + start_time=_time(1000), + end_time=_time(1100), + ) + assert p.get_schedule(from_time=_time(0), to_time=_time(2500)) == [ + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1000), + end_time=Timestamp(seconds=1100), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1100), + end_time=Timestamp(seconds=1900), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1900), + end_time=Timestamp(seconds=2000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, + ), + ] + + +def test_create_reservation_unbounded(): + time_slot_begin = qtypes.QuantumTimeSlot( + processor_name='test', + end_time=Timestamp(seconds=2000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ) + time_slot_end = qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=5000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ) + p = NothingProcessor(processor_id='test', schedule=[time_slot_begin, time_slot_end]) + p.create_reservation( + start_time=_time(1000), + end_time=_time(3000), + ) + p.create_reservation( + start_time=_time(4000), + end_time=_time(6000), + ) + assert p.get_schedule(from_time=_time(0), to_time=_time(10000)) == [ + qtypes.QuantumTimeSlot( + processor_name='test', + end_time=Timestamp(seconds=1000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1000), + end_time=Timestamp(seconds=3000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=4000), + end_time=Timestamp(seconds=6000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=6000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ), + ] + + +def test_create_reservation_splitunbounded(): + time_slot_begin = qtypes.QuantumTimeSlot( + processor_name='test', + end_time=Timestamp(seconds=3000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ) + time_slot_end = qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=5000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ) + p = NothingProcessor(processor_id='test', schedule=[time_slot_begin, time_slot_end]) + p.create_reservation( + start_time=_time(1000), + end_time=_time(2000), + ) + p.create_reservation( + start_time=_time(6000), + end_time=_time(7000), + ) + assert p.get_schedule(from_time=_time(0), to_time=_time(10000)) == [ + qtypes.QuantumTimeSlot( + processor_name='test', + end_time=Timestamp(seconds=1000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1000), + end_time=Timestamp(seconds=2000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=2000), + end_time=Timestamp(seconds=3000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=5000), + end_time=Timestamp(seconds=6000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=6000), + end_time=Timestamp(seconds=7000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=7000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ), + ] diff --git a/cirq-google/cirq_google/engine/abstract_local_program.py b/cirq-google/cirq_google/engine/abstract_local_program.py new file mode 100644 index 00000000000..fa9445d85ac --- /dev/null +++ b/cirq-google/cirq_google/engine/abstract_local_program.py @@ -0,0 +1,206 @@ +# Copyright 2021 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 copy +import datetime +from typing import Dict, List, Optional, Sequence, Set, TYPE_CHECKING, Union +import cirq +from cirq_google.engine.client import quantum +from cirq_google.engine.abstract_program import AbstractProgram + +if TYPE_CHECKING: + from cirq_google.engine.abstract_local_job import AbstractLocalJob + from cirq_google.engine.abstract_local_engine import AbstractLocalEngine + + +class AbstractLocalProgram(AbstractProgram): + """A quantum program designed for local in-memory computation. + + This implements all the methods in `AbstractProgram` using + in-memory structions. Labels, descriptions, and time are all + stored using dictionaries. + + This is a partially implemented instance. Inheritors will still + A program created via the Engine interface. + """ + + def __init__(self, circuits: List[cirq.Circuit], engine: 'AbstractLocalEngine'): + if not circuits: + raise ValueError('No circuits provided to program.') + self._create_time = datetime.datetime.now() + self._update_time = datetime.datetime.now() + self._description = '' + self._labels: Dict[str, str] = {} + self._engine = engine + self._jobs: Dict[str, 'AbstractLocalJob'] = {} + self._circuits = circuits + + def engine(self) -> 'AbstractLocalEngine': + """Returns the parent Engine object. + + Returns: + The program's parent Engine. + """ + return self._engine + + def add_job(self, job_id: str, job: 'AbstractLocalJob') -> None: + self._jobs[job_id] = job + + def get_job(self, job_id: str) -> 'AbstractLocalJob': + """Returns an AbstractLocalJob for an existing Quantum Engine job. + + Args: + job_id: Unique ID of the job within the parent program. + + Returns: + A AbstractLocalJob for this program. + + Raises: + KeyError: if job is not found. + """ + if job_id in self._jobs: + return self._jobs[job_id] + raise KeyError(f'job {job_id} not found') + + def list_jobs( + self, + created_before: Optional[Union[datetime.datetime, datetime.date]] = None, + created_after: Optional[Union[datetime.datetime, datetime.date]] = None, + has_labels: Optional[Dict[str, str]] = None, + execution_states: Optional[Set[quantum.enums.ExecutionStatus.State]] = None, + ) -> Sequence['AbstractLocalJob']: + """Returns the list of jobs for this program. + + Args: + created_after: retrieve jobs that were created after this date + or time. + created_before: retrieve jobs that were created after this date + or time. + has_labels: retrieve jobs that have labels on them specified by + this dict. If the value is set to `*`, filters having the label + regardless of the label value will be filtered. For example, to + query programs that have the shape label and have the color + label with value red can be queried using + + {'color': 'red', 'shape':'*'} + + execution_states: retrieve jobs that have an execution state that + is contained in `execution_states`. See + `quantum.enums.ExecutionStatus.State` enum for accepted values. + """ + job_list = [] + for job in self._jobs.values(): + if created_before and job.create_time() > created_before: + continue + if created_after and job.create_time() < created_after: + continue + if execution_states: + if job.execution_status() not in execution_states: + continue + if has_labels: + job_labels = job.labels() + if not all( + label in job_labels and job_labels[label] == has_labels[label] + for label in has_labels + ): + continue + job_list.append(job) + return job_list + + def create_time(self) -> 'datetime.datetime': + """Returns when the program was created.""" + return self._create_time + + def update_time(self) -> 'datetime.datetime': + """Returns when the program was last updated.""" + return self._update_time + + def description(self) -> str: + """Returns the description of the program.""" + return self._description + + def set_description(self, description: str) -> 'AbstractProgram': + """Sets the description of the program. + + Params: + description: The new description for the program. + + Returns: + This AbstractProgram. + """ + self._description = description + return self + + def labels(self) -> Dict[str, str]: + """Returns the labels of the program.""" + return copy.copy(self._labels) + + def set_labels(self, labels: Dict[str, str]) -> 'AbstractProgram': + """Sets (overwriting) the labels for a previously created quantum + program. + + Params: + labels: The entire set of new program labels. + + Returns: + This AbstractProgram. + """ + self._labels = copy.copy(labels) + return self + + def add_labels(self, labels: Dict[str, str]) -> 'AbstractProgram': + """Adds new labels to a previously created quantum program. + + Params: + labels: New labels to add to the existing program labels. + + Returns: + This AbstractProgram. + """ + for key in labels: + self._labels[key] = labels[key] + return self + + def remove_labels(self, keys: List[str]) -> 'AbstractProgram': + """Removes labels with given keys from the labels of a previously + created quantum program. + + Params: + label_keys: Label keys to remove from the existing program labels. + + Returns: + This AbstractProgram. + """ + for key in keys: + del self._labels[key] + return self + + def get_circuit(self, program_num: Optional[int] = None) -> cirq.Circuit: + """Returns the cirq Circuit for the program. This is only + supported if the program was created with the V2 protos. + + Args: + program_num: if this is a batch program, the index of the circuit in + the batch. This argument is zero-indexed. Negative values + indexing from the end of the list. + + Returns: + The program's cirq Circuit. + """ + if program_num: + return self._circuits[program_num] + return self._circuits[0] + + def batch_size(self) -> int: + """Returns the number of programs in a batch program. """ + return len(self._circuits) diff --git a/cirq-google/cirq_google/engine/abstract_local_program_test.py b/cirq-google/cirq_google/engine/abstract_local_program_test.py new file mode 100644 index 00000000000..2f26454029e --- /dev/null +++ b/cirq-google/cirq_google/engine/abstract_local_program_test.py @@ -0,0 +1,156 @@ +# Copyright 2021 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 +import pytest + +import cirq +from cirq_google.engine.client import quantum +from cirq_google.engine.abstract_local_job_test import NothingJob +from cirq_google.engine.abstract_local_program import AbstractLocalProgram + + +class NothingProgram(AbstractLocalProgram): + def delete(self, delete_jobs: bool = False) -> None: + pass + + def delete_job(self, job_id: str) -> None: + pass + + +def test_delete(): + program = NothingProgram([cirq.Circuit()], None) + program.delete() + + +def test_init(): + with pytest.raises(ValueError, match='No circuits provided'): + _ = NothingProgram([], None) + + +def test_jobs(): + program = NothingProgram([cirq.Circuit()], None) + job1 = NothingJob( + job_id='test', processor_id='test1', parent_program=program, repetitions=100, sweeps=[] + ) + job2 = NothingJob( + job_id='test', processor_id='test2', parent_program=program, repetitions=100, sweeps=[] + ) + job3 = NothingJob( + job_id='test', processor_id='test3', parent_program=program, repetitions=100, sweeps=[] + ) + job2.set_labels({'color': 'blue', 'shape': 'square'}) + job3.set_labels({'color': 'green', 'shape': 'square'}) + + # Use private variables for deterministic searches + job1._create_time = datetime.datetime.fromtimestamp(1000) + job2._create_time = datetime.datetime.fromtimestamp(2000) + job3._create_time = datetime.datetime.fromtimestamp(3000) + failure = quantum.enums.ExecutionStatus.State.FAILURE + success = quantum.enums.ExecutionStatus.State.SUCCESS + job1._status = failure + job2._status = failure + job3._status = success + + with pytest.raises(KeyError): + program.get_job('jerb') + program.add_job('jerb', job1) + program.add_job('employ', job2) + program.add_job('jobbies', job3) + assert program.get_job('jerb') == job1 + assert program.get_job('employ') == job2 + assert program.get_job('jobbies') == job3 + + assert set(program.list_jobs(has_labels={'shape': 'square'})) == {job2, job3} + assert program.list_jobs(has_labels={'color': 'blue'}) == [job2] + assert program.list_jobs(has_labels={'color': 'green'}) == [job3] + assert program.list_jobs(has_labels={'color': 'yellow'}) == [] + + assert set(program.list_jobs(created_before=datetime.datetime.fromtimestamp(3500))) == { + job1, + job2, + job3, + } + assert set(program.list_jobs(created_before=datetime.datetime.fromtimestamp(2500))) == { + job1, + job2, + } + assert set(program.list_jobs(created_before=datetime.datetime.fromtimestamp(1500))) == {job1} + assert program.list_jobs(created_before=datetime.datetime.fromtimestamp(500)) == [] + + assert set(program.list_jobs(created_after=datetime.datetime.fromtimestamp(500))) == { + job1, + job2, + job3, + } + assert set(program.list_jobs(created_after=datetime.datetime.fromtimestamp(1500))) == { + job2, + job3, + } + assert set(program.list_jobs(created_after=datetime.datetime.fromtimestamp(2500))) == {job3} + assert program.list_jobs(created_after=datetime.datetime.fromtimestamp(3500)) == [] + + assert set(program.list_jobs(execution_states={failure, success})) == {job1, job2, job3} + assert program.list_jobs(execution_states={success}) == [job3] + assert set(program.list_jobs(execution_states={failure})) == {job1, job2} + ready = quantum.enums.ExecutionStatus.State.READY + assert program.list_jobs(execution_states={ready}) == [] + assert set(program.list_jobs(execution_states={})) == {job1, job2, job3} + + assert set(program.list_jobs(has_labels={'shape': 'square'}, execution_states={failure})) == { + job2 + } + + +def test_create_update_time(): + program = NothingProgram([cirq.Circuit()], None) + create_time = datetime.datetime.fromtimestamp(1000) + update_time = datetime.datetime.fromtimestamp(2000) + + program._create_time = create_time + program._update_time = update_time + + assert program.create_time() == create_time + assert program.update_time() == update_time + + +def test_description_and_labels(): + program = NothingProgram([cirq.Circuit()], None) + assert not program.description() + program.set_description('nothing much') + assert program.description() == 'nothing much' + program.set_description('other desc') + assert program.description() == 'other desc' + assert program.labels() == {} + program.set_labels({'key': 'green'}) + assert program.labels() == {'key': 'green'} + program.add_labels({'door': 'blue', 'curtains': 'white'}) + assert program.labels() == {'key': 'green', 'door': 'blue', 'curtains': 'white'} + program.remove_labels(['key', 'door']) + assert program.labels() == {'curtains': 'white'} + program.set_labels({'walls': 'gray'}) + assert program.labels() == {'walls': 'gray'} + + +def test_circuit(): + circuit1 = cirq.Circuit(cirq.X(cirq.LineQubit(1))) + circuit2 = cirq.Circuit(cirq.Y(cirq.LineQubit(2))) + program = NothingProgram([circuit1], None) + assert program.batch_size() == 1 + assert program.get_circuit() == circuit1 + assert program.get_circuit(0) == circuit1 + assert program.batch_size() == 1 + program = NothingProgram([circuit1, circuit2], None) + assert program.batch_size() == 2 + assert program.get_circuit(0) == circuit1 + assert program.get_circuit(1) == circuit2 diff --git a/cirq-google/cirq_google/engine/abstract_processor.py b/cirq-google/cirq_google/engine/abstract_processor.py new file mode 100644 index 00000000000..7e814133498 --- /dev/null +++ b/cirq-google/cirq_google/engine/abstract_processor.py @@ -0,0 +1,420 @@ +# Copyright 2021 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. +from abc import ABC, abstractmethod +import datetime + +from typing import Dict, Iterable, List, Optional, Sequence, TYPE_CHECKING, Union + +import cirq +from cirq_google.engine.client.quantum import types as qtypes +from cirq_google.engine.client.quantum import enums as qenums +from cirq_google.api import v2 +from cirq_google.engine import calibration + +if TYPE_CHECKING: + import cirq_google + from cirq_google.engine.abstract_engine import AbstractEngine + from cirq_google.engine.abstract_job import AbstractJob + from cirq_google.engine.abstract_program import AbstractProgram + from cirq_google.serialization.serializer import Serializer + + +class AbstractProcessor(ABC): + """An abstract interface for a quantum processor. + + This quantum processor has the ability to execute single circuits + (via the run method), parameter sweeps (via run_sweep), batched + lists of circuits (via run_batch), and calibration + requests (via run_calibration). Running circuits can also be + done using the `cirq.Sampler` by calling get_sampler. + + The processor interface also includes methods to create, list, + and remove reservations on the processor for dedicated access. + The processor can also list calibration metrics for the processor + given a time period. + + This is an abstract class. Inheritors should implement abstract methods. + """ + + def run( + self, + program: cirq.Circuit, + program_id: Optional[str] = None, + job_id: Optional[str] = None, + param_resolver: cirq.ParamResolver = cirq.ParamResolver({}), + repetitions: int = 1, + gate_set: Optional['Serializer'] = None, + program_description: Optional[str] = None, + program_labels: Optional[Dict[str, str]] = None, + job_description: Optional[str] = None, + job_labels: Optional[Dict[str, str]] = None, + ) -> cirq.Result: + """Runs the supplied Circuit on this processor. + + Args: + program: The Circuit to execute. If a circuit is + provided, a moment by moment schedule will be used. + 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 + 'prog-################YYMMDD' will be generated, where # is + alphanumeric and YYMMDD is the current year, month, and day. + job_id: Job identifier to use. If this is not provided, a random id + of the format 'job-################YYMMDD' will be generated, + where # is alphanumeric and YYMMDD is the current year, month, + and day. + param_resolver: Parameters to run with the program. + repetitions: The number of repetitions to simulate. + gate_set: The gate set used to serialize the circuit. The gate set + must be supported by the selected processor. + program_description: An optional description to set on the program. + program_labels: Optional set of labels to set on the program. + job_description: An optional description to set on the job. + job_labels: Optional set of labels to set on the job. + Returns: + A single Result for this run. + """ + + @abstractmethod + def run_sweep( + self, + program: cirq.Circuit, + program_id: Optional[str] = None, + job_id: Optional[str] = None, + params: cirq.Sweepable = None, + repetitions: int = 1, + gate_set: Optional['Serializer'] = None, + program_description: Optional[str] = None, + program_labels: Optional[Dict[str, str]] = None, + job_description: Optional[str] = None, + job_labels: Optional[Dict[str, str]] = None, + ) -> 'AbstractJob': + """Runs the supplied Circuit on this processor. + + In contrast to run, this runs across multiple parameter sweeps, and + does not block until a result is returned. + Args: + program: The Circuit to execute. If a circuit is + provided, a moment by moment schedule will be used. + 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 + 'prog-################YYMMDD' will be generated, where # is + alphanumeric and YYMMDD is the current year, month, and day. + job_id: Job identifier to use. If this is not provided, a random id + of the format 'job-################YYMMDD' will be generated, + where # is alphanumeric and YYMMDD is the current year, month, + and day. + params: Parameters to run with the program. + repetitions: The number of circuit repetitions to run. + gate_set: The gate set used to serialize the circuit. The gate set + must be supported by the selected processor. + program_description: An optional description to set on the program. + program_labels: Optional set of labels to set on the program. + job_description: An optional description to set on the job. + job_labels: Optional set of labels to set on the job. + Returns: + An AbstractJob. If this is iterated over it returns a list of + TrialResults, one for each parameter sweep. + """ + + @abstractmethod + def run_batch( + self, + programs: Sequence[cirq.AbstractCircuit], + program_id: Optional[str] = None, + job_id: Optional[str] = None, + params_list: List[cirq.Sweepable] = None, + repetitions: int = 1, + gate_set: Optional['Serializer'] = None, + program_description: Optional[str] = None, + program_labels: Optional[Dict[str, str]] = None, + job_description: Optional[str] = None, + job_labels: Optional[Dict[str, str]] = None, + ) -> 'AbstractJob': + """Runs the supplied Circuits on this processor. + + This will combine each Circuit provided in `programs` into + a BatchProgram. Each circuit will pair with the associated + parameter sweep provided in the `params_list`. The number of + programs is required to match the number of sweeps. + This method does not block until a result is returned. However, + no results will be available until the entire batch is complete. + Args: + programs: The Circuits to execute as a batch. + 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 + 'prog-################YYMMDD' will be generated, where # is + alphanumeric and YYMMDD is the current year, month, and day. + job_id: Job identifier to use. If this is not provided, a random id + of the format 'job-################YYMMDD' will be generated, + where # is alphanumeric and YYMMDD is the current year, month, + and day. + params_list: Parameter sweeps to use with the circuits. The number + of sweeps should match the number of circuits and will be + paired in order with the circuits. If this is None, it is + assumed that the circuits are not parameterized and do not + require sweeps. + repetitions: Number of circuit repetitions to run. Each sweep value + of each circuit in the batch will run with the same repetitions. + gate_set: The gate set used to serialize the circuit. The gate set + must be supported by the selected processor. + program_description: An optional description to set on the program. + program_labels: Optional set of labels to set on the program. + job_description: An optional description to set on the job. + job_labels: Optional set of labels to set on the job. + Returns: + An AbstractJob. If this is iterated over it returns a list of + TrialResults. All TrialResults for the first circuit are listed + first, then the TrialResults for the second, etc. The TrialResults + for a circuit are listed in the order imposed by the associated + parameter sweep. + """ + + @abstractmethod + def run_calibration( + self, + layers: List['cirq_google.CalibrationLayer'], + program_id: Optional[str] = None, + job_id: Optional[str] = None, + gate_set: Optional['Serializer'] = None, + program_description: Optional[str] = None, + program_labels: Optional[Dict[str, str]] = None, + job_description: Optional[str] = None, + job_labels: Optional[Dict[str, str]] = None, + ) -> 'AbstractJob': + """Runs the specified calibrations on the processor. + + Each calibration will be specified by a `CalibrationLayer` + that contains the type of the calibrations to run, a `Circuit` + to optimize, and any arguments needed by the calibration routine. + Arguments and circuits needed for each layer will vary based on the + calibration type. However, the typical calibration routine may + require a single moment defining the gates to optimize, for example. + Note: this is an experimental API and is not yet fully supported + for all users. + Args: + layers: The layers of calibration to execute as a batch. + 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 + 'calibration-################YYMMDD' will be generated, + where # is alphanumeric and YYMMDD is the current year, month, + and day. + job_id: Job identifier to use. If this is not provided, a random id + of the format 'calibration-################YYMMDD' will be + generated, where # is alphanumeric and YYMMDD is the current + year, month, and day. + gate_set: The gate set used to serialize the circuit. The gate set + must be supported by the selected processor. + program_description: An optional description to set on the program. + program_labels: Optional set of labels to set on the program. + job_description: An optional description to set on the job. + job_labels: Optional set of labels to set on the job. By defauly, + this will add a 'calibration' label to the job. + Returns: + An AbstractJob whose results can be retrieved by calling + calibration_results(). + """ + + @abstractmethod + def get_sampler(self, gate_set: Optional['Serializer']) -> cirq.Sampler: + """Returns a sampler backed by the processor. + + Args: + gate_set: Determines how to serialize circuits if needed. + """ + + @abstractmethod + def engine(self) -> Optional['AbstractEngine']: + """Returns the parent Engine object. + + Returns: + The program's parent Engine. + """ + + @abstractmethod + def health(self) -> str: + """Returns the current health of processor.""" + + @abstractmethod + def expected_down_time(self) -> 'Optional[datetime.datetime]': + """Returns the start of the next expected down time of the processor, if + set.""" + + @abstractmethod + def expected_recovery_time(self) -> 'Optional[datetime.datetime]': + """Returns the expected the processor should be available, if set.""" + + @abstractmethod + def supported_languages(self) -> List[str]: + """Returns the list of processor supported program languages.""" + + @abstractmethod + def get_device_specification(self) -> Optional[v2.device_pb2.DeviceSpecification]: + """Returns a device specification proto for use in determining + information about the device. + + Returns: + Device specification proto if present. + """ + + @abstractmethod + def get_device(self, gate_sets: Iterable['Serializer']) -> cirq.Device: + """Returns a `Device` created from the processor's device specification. + + This method queries the processor to retrieve the device specification, + which is then use to create a `SerializableDevice` that will validate + that operations are supported and use the correct qubits. + """ + + @abstractmethod + def list_calibrations( + self, + earliest_timestamp_seconds: Optional[int] = None, + latest_timestamp_seconds: Optional[int] = None, + ) -> List[calibration.Calibration]: + """Retrieve metadata about a specific calibration run. + + Params: + earliest_timestamp_seconds: The earliest timestamp of a calibration + to return in UTC. + latest_timestamp_seconds: The latest timestamp of a calibration to + return in UTC. + + Returns: + The list of calibration data with the most recent first. + """ + + @abstractmethod + def get_calibration(self, calibration_timestamp_seconds: int) -> calibration.Calibration: + """Retrieve metadata about a specific calibration run. + + Params: + calibration_timestamp_seconds: The timestamp of the calibration in + seconds since epoch. + + Returns: + The calibration data. + """ + + @abstractmethod + def get_current_calibration( + self, + ) -> Optional[calibration.Calibration]: + """Returns metadata about the current calibration for a processor. + + Returns: + The calibration data or None if there is no current calibration. + """ + + @abstractmethod + def create_reservation( + self, + start_time: datetime.datetime, + end_time: datetime.datetime, + whitelisted_users: Optional[List[str]] = None, + ) -> qtypes.QuantumReservation: + """Creates a reservation on this processor. + + Args: + start_time: the starting date/time of the reservation. + end_time: the ending date/time of the reservation. + whitelisted_users: a list of emails that are allowed + to send programs during this reservation (in addition to users + with permission "quantum.reservations.use" on the project). + """ + + @abstractmethod + def remove_reservation(self, reservation_id: str): + """Removes a reservation on this processor.""" + + @abstractmethod + def get_reservation(self, reservation_id: str) -> qtypes.QuantumReservation: + """Retrieve a reservation given its id.""" + + @abstractmethod + def update_reservation( + self, + reservation_id: str, + start_time: datetime.datetime = None, + end_time: datetime.datetime = None, + whitelisted_users: List[str] = None, + ): + """Updates a reservation with new information. + + Updates a reservation with a new start date, end date, or + list of additional users. For each field, it the argument is left as + None, it will not be updated. + """ + + @abstractmethod + def list_reservations( + self, + from_time: Union[None, datetime.datetime, datetime.timedelta] = datetime.timedelta(), + to_time: Union[None, datetime.datetime, datetime.timedelta] = datetime.timedelta(weeks=2), + ) -> List[qtypes.QuantumReservation]: + """Retrieves the reservations from a processor. + + Only reservations from this processor and project will be + returned. The schedule may be filtered by starting and ending time. + + Args: + from_time: Filters the returned reservations to only include entries + that end no earlier than the given value. Specified either as an + absolute time (datetime.datetime) or as a time relative to now + (datetime.timedelta). Defaults to now (a relative time of 0). + Set to None to omit this filter. + to_time: Filters the returned reservations to only include entries + that start no later than the given value. Specified either as an + absolute time (datetime.datetime) or as a time relative to now + (datetime.timedelta). Defaults to two weeks from now (a relative + time of two weeks). Set to None to omit this filter. + + Returns: + A list of reservations. + """ + + @abstractmethod + def get_schedule( + self, + from_time: Union[None, datetime.datetime, datetime.timedelta] = datetime.timedelta(), + to_time: Union[None, datetime.datetime, datetime.timedelta] = datetime.timedelta(weeks=2), + time_slot_type: Optional[qenums.QuantumTimeSlot.TimeSlotType] = None, + ) -> List[qenums.QuantumTimeSlot]: + """Retrieves the schedule for a processor. + + The schedule may be filtered by time. + + Time slot type will be supported in the future. + + Args: + from_time: Filters the returned schedule to only include entries + that end no earlier than the given value. Specified either as an + absolute time (datetime.datetime) or as a time relative to now + (datetime.timedelta). Defaults to now (a relative time of 0). + Set to None to omit this filter. + to_time: Filters the returned schedule to only include entries + that start no later than the given value. Specified either as an + absolute time (datetime.datetime) or as a time relative to now + (datetime.timedelta). Defaults to two weeks from now (a relative + time of two weeks). Set to None to omit this filter. + time_slot_type: Filters the returned schedule to only include + entries with a given type (e.g. maintenance, open swim). + Defaults to None. Set to None to omit this filter. + + Returns: + Schedule time slots. + """ diff --git a/cirq-google/cirq_google/engine/abstract_program.py b/cirq-google/cirq_google/engine/abstract_program.py new file mode 100644 index 00000000000..50e8e4e99b4 --- /dev/null +++ b/cirq-google/cirq_google/engine/abstract_program.py @@ -0,0 +1,180 @@ +# Copyright 2021 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. +from abc import ABC, abstractmethod +import datetime +from typing import Dict, List, Optional, Sequence, Set, TYPE_CHECKING, Union +import cirq +from cirq_google.engine.client import quantum + +if TYPE_CHECKING: + from cirq_google.engine.abstract_job import AbstractJob + from cirq_google.engine.abstract_engine import AbstractEngine + + +class AbstractProgram(ABC): + """An abstract object representing a quantum program. + + This program generally wraps a `Circuit` with additional metadata. + When combined with an appropriate RunContext, this becomes a + Job that can run on either an Engine service or simulator. + Programs can also be a batch (list of circuits) or calibration + requests. + + This is an abstract class that inheritors should implement. + """ + + @abstractmethod + def engine(self) -> 'AbstractEngine': + """Returns the parent Engine object. + + Returns: + The program's parent Engine. + """ + + @abstractmethod + def get_job(self, job_id: str) -> 'AbstractJob': + """Returns an AbstractJob for an existing id. + + Args: + job_id: Unique ID of the job within the parent program. + + Returns: + A AbstractJob for this program. + """ + + @abstractmethod + def list_jobs( + self, + created_before: Optional[Union[datetime.datetime, datetime.date]] = None, + created_after: Optional[Union[datetime.datetime, datetime.date]] = None, + has_labels: Optional[Dict[str, str]] = None, + execution_states: Optional[Set[quantum.enums.ExecutionStatus.State]] = None, + ) -> Sequence['AbstractJob']: + """Returns the list of jobs for this program. + + Args: + project_id: A project_id of the parent Google Cloud Project. + program_id: Unique ID of the program within the parent project. + created_after: retrieve jobs that were created after this date + or time. + created_before: retrieve jobs that were created after this date + or time. + has_labels: retrieve jobs that have labels on them specified by + this dict. If the value is set to `*`, filters having the label + regardless of the label value will be filtered. For example, to + query programs that have the shape label and have the color + label with value red can be queried using + + {'color': 'red', 'shape':'*'} + + execution_states: retrieve jobs that have an execution state that + is contained in `execution_states`. See + `quantum.enums.ExecutionStatus.State` enum for accepted values. + """ + + @abstractmethod + def create_time(self) -> 'datetime.datetime': + """Returns when the program was created.""" + + @abstractmethod + def update_time(self) -> 'datetime.datetime': + """Returns when the program was last updated.""" + + @abstractmethod + def description(self) -> str: + """Returns the description of the program.""" + + @abstractmethod + def set_description(self, description: str) -> 'AbstractProgram': + """Sets the description of the program. + + Params: + description: The new description for the program. + + Returns: + This AbstractProgram. + """ + + @abstractmethod + def labels(self) -> Dict[str, str]: + """Returns the labels of the program.""" + + @abstractmethod + def set_labels(self, labels: Dict[str, str]) -> 'AbstractProgram': + """Sets (overwriting) the labels for a previously created quantum program. + + Params: + labels: The entire set of new program labels. + + Returns: + This AbstractProgram. + """ + + @abstractmethod + def add_labels(self, labels: Dict[str, str]) -> 'AbstractProgram': + """Adds new labels to a previously created quantum program. + + Params: + labels: New labels to add to the existing program labels. + + Returns: + This AbstractProgram. + """ + + @abstractmethod + def remove_labels(self, keys: List[str]) -> 'AbstractProgram': + """Removes labels with given keys from the labels of a previously + created quantum program. + + Params: + label_keys: Label keys to remove from the existing program labels. + + Returns: + This AbstractProgram. + """ + + @abstractmethod + def get_circuit(self, program_num: Optional[int] = None) -> cirq.Circuit: + """Returns the cirq Circuit for the program. This is only + supported if the program was created with the V2 protos. + + Args: + program_num: if this is a batch program, the index of the circuit in + the batch. This argument is zero-indexed. Negative values + indexing from the end of the list. + + Returns: + The program's cirq Circuit. + """ + + @abstractmethod + def batch_size(self) -> int: + """Returns the number of programs in a batch program. + + Raises: + ValueError: if the program created was not a batch program. + """ + + @abstractmethod + def delete(self, delete_jobs: bool = False) -> None: + """Deletes a previously created quantum program. + + Params: + delete_jobs: If True will delete all the program's jobs, other this + will fail if the program contains any jobs. + """ + + @abstractmethod + def delete_job(self, job_id: str) -> None: + """Removes a child job from this program.""" diff --git a/cirq-google/cirq_google/engine/local_simulation_type.py b/cirq-google/cirq_google/engine/local_simulation_type.py new file mode 100644 index 00000000000..bbb69eb28d9 --- /dev/null +++ b/cirq-google/cirq_google/engine/local_simulation_type.py @@ -0,0 +1,20 @@ +# Copyright 2021 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 enum + + +class LocalSimulationType(enum.Enum): + SYNCHRONOUS = 1 + ASYNCHRONOUS = 2 + ASYNCHRONOUS_WITH_DELAY = 3 diff --git a/cirq-google/cirq_google/engine/simulated_local_engine.py b/cirq-google/cirq_google/engine/simulated_local_engine.py new file mode 100644 index 00000000000..8d23dc361bf --- /dev/null +++ b/cirq-google/cirq_google/engine/simulated_local_engine.py @@ -0,0 +1,45 @@ +# Copyright 2021 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. +"""Classes for running against Google's Quantum Cloud Service. + +As an example, to run a circuit against the xmon simulator on the cloud, + engine = cirq_google.Engine(project_id='my-project-id') + program = engine.create_program(circuit) + result0 = program.run(params=params0, repetitions=10) + result1 = program.run(params=params1, repetitions=10) + +In order to run on must have access to the Quantum Engine API. Access to this +API is (as of June 22, 2018) restricted to invitation only. +""" +from typing import List + +from cirq_google.engine.abstract_local_engine import AbstractLocalEngine +from cirq_google.engine.abstract_local_processor import AbstractLocalProcessor + + +class SimulatedLocalEngine(AbstractLocalEngine): + """Collection of processors backed by local samplers. + + This class is a wrapper around `AbstractLocalEngine` and + adds no additional functionality and exists for naming consistency + and for possible future extension. + + This class assumes that all processors are local. Processors + are given during initialization. Program and job querying + functionality is done by serially querying all child processors. + + """ + + def __init__(self, processors: List[AbstractLocalProcessor]): + super().__init__(processors) diff --git a/cirq-google/cirq_google/engine/simulated_local_engine_test.py b/cirq-google/cirq_google/engine/simulated_local_engine_test.py new file mode 100644 index 00000000000..46135de93b6 --- /dev/null +++ b/cirq-google/cirq_google/engine/simulated_local_engine_test.py @@ -0,0 +1,183 @@ +# Copyright 2021 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 Dict, Optional, Union +import pytest + +import cirq +import cirq_google +import sympy +import numpy as np + +from cirq_google.api import v2 +from cirq_google.engine.abstract_local_job_test import NothingJob +from cirq_google.engine.abstract_local_program_test import NothingProgram +from cirq_google.engine.abstract_local_processor import AbstractLocalProcessor +from cirq_google.engine.abstract_program import AbstractProgram +from cirq_google.engine.simulated_local_engine import SimulatedLocalEngine +from cirq_google.engine.simulated_local_processor import SimulatedLocalProcessor + + +class ProgramDictProcessor(AbstractLocalProcessor): + """A processor that has a dictionary of programs for testing.""" + + def __init__(self, programs: Dict[str, AbstractProgram], **kwargs): + super().__init__(**kwargs) + self._programs = programs + + def get_calibration(self, *args, **kwargs): + pass + + def get_latest_calibration(self, *args, **kwargs): + pass + + def get_current_calibration(self, *args, **kwargs): + pass + + def get_device(self, *args, **kwargs): + pass + + def get_device_specification(self, *args, **kwargs): + pass + + def health(self, *args, **kwargs): + pass + + def list_calibrations(self, *args, **kwargs): + pass + + def run(self, *args, **kwargs): + pass + + def run_batch(self, *args, **kwargs): + pass + + def run_calibration(self, *args, **kwargs): + pass + + def run_sweep(self, *args, **kwargs): + pass + + def get_sampler(self, *args, **kwargs): + pass + + def supported_languages(self, *args, **kwargs): + pass + + def list_programs( + self, + created_before: Optional[Union[datetime.datetime, datetime.date]] = None, + created_after: Optional[Union[datetime.datetime, datetime.date]] = None, + has_labels: Optional[Dict[str, str]] = None, + ): + """Lists all programs regardless of filters. + + This isn't really correct, but we don't want to test test functionality.""" + return self._programs.values() + + def get_program(self, program_id: str) -> AbstractProgram: + return self._programs[program_id] + + +def test_get_processor(): + processor1 = ProgramDictProcessor(programs=[], processor_id='test') + engine = SimulatedLocalEngine([processor1]) + assert engine.get_processor('test') == processor1 + assert engine.get_processor('test').engine() == engine + with pytest.raises(KeyError): + engine.get_processor('abracadabra') + with pytest.raises(ValueError, match='Invalid processor'): + engine.get_sampler(processor_id=['a', 'b', 'c']) + + +def test_list_processor(): + processor1 = ProgramDictProcessor(programs=[], processor_id='proc') + processor2 = ProgramDictProcessor(programs=[], processor_id='crop') + engine = SimulatedLocalEngine([processor1, processor2]) + assert engine.get_processor('proc') == processor1 + assert engine.get_processor('crop') == processor2 + assert engine.get_processor('proc').engine() == engine + assert engine.get_processor('crop').engine() == engine + assert set(engine.list_processors()) == {processor1, processor2} + + +def test_get_programs(): + program1 = NothingProgram([cirq.Circuit()], None) + job1 = NothingJob( + job_id='test', processor_id='test1', parent_program=program1, repetitions=100, sweeps=[] + ) + program1.add_job('jerb', job1) + job1.add_labels({'color': 'blue'}) + + program2 = NothingProgram([cirq.Circuit()], None) + job2 = NothingJob( + job_id='test', processor_id='test2', parent_program=program2, repetitions=100, sweeps=[] + ) + program2.add_job('jerb2', job2) + job2.add_labels({'color': 'red'}) + + processor1 = ProgramDictProcessor(programs={'prog1': program1}, processor_id='proc') + processor2 = ProgramDictProcessor(programs={'prog2': program2}, processor_id='crop') + engine = SimulatedLocalEngine([processor1, processor2]) + + assert engine.get_program('prog1') == program1 + + with pytest.raises(KeyError, match='does not exis'): + _ = engine.get_program('yoyo') + + assert set(engine.list_programs()) == {program1, program2} + assert set(engine.list_jobs()) == {job1, job2} + assert engine.list_jobs(has_labels={'color': 'blue'}) == [job1] + assert engine.list_jobs(has_labels={'color': 'red'}) == [job2] + + +def test_full_simulation(): + engine = SimulatedLocalEngine([SimulatedLocalProcessor(processor_id='tester')]) + q = cirq.GridQubit(5, 4) + circuit = cirq.Circuit(cirq.X(q) ** sympy.Symbol('t'), cirq.measure(q, key='m')) + sweep = cirq.Points(key='t', points=[1, 0]) + job = engine.get_processor('tester').run_sweep(circuit, params=sweep, repetitions=100) + assert job.engine() == engine + assert job.program().engine() == engine + results = job.results() + assert np.all(results[0].measurements['m'] == 1) + assert np.all(results[1].measurements['m'] == 0) + + +def test_sampler(): + engine = SimulatedLocalEngine([SimulatedLocalProcessor(processor_id='tester')]) + q = cirq.GridQubit(5, 4) + circuit = cirq.Circuit(cirq.X(q) ** sympy.Symbol('t'), cirq.measure(q, key='m')) + sweep = cirq.Points(key='t', points=[1, 0]) + results = engine.get_sampler('tester').run_sweep(circuit, params=sweep, repetitions=100) + assert np.all(results[0].measurements['m'] == 1) + assert np.all(results[1].measurements['m'] == 0) + + +def test_get_calibration_from_job(): + cal_proto = v2.metrics_pb2.MetricsSnapshot(timestamp_ms=10000) + cal = cirq_google.Calibration(cal_proto) + proc = SimulatedLocalProcessor(processor_id='test_proc', calibrations={10000: cal}) + engine = SimulatedLocalEngine([proc]) + job = engine.get_processor('test_proc').run_sweep(cirq.Circuit(), params={}, repetitions=100) + assert job.get_processor() == proc + assert job.get_calibration() == cal + + +def test_no_calibration_from_job(): + proc = SimulatedLocalProcessor(processor_id='test_proc') + engine = SimulatedLocalEngine([proc]) + job = engine.get_processor('test_proc').run_sweep(cirq.Circuit(), params={}, repetitions=100) + assert job.get_processor() == proc + assert job.get_calibration() is None diff --git a/cirq-google/cirq_google/engine/simulated_local_job.py b/cirq-google/cirq_google/engine/simulated_local_job.py new file mode 100644 index 00000000000..207da701156 --- /dev/null +++ b/cirq-google/cirq_google/engine/simulated_local_job.py @@ -0,0 +1,125 @@ +# Copyright 2021 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. +"""A helper for jobs that have been created on the Quantum Engine.""" +from typing import cast, List, Optional, Tuple + +import cirq +from cirq_google.engine.client import quantum +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 + + +class SimulatedLocalJob(AbstractLocalJob): + """A quantum job backed by a (local) sampler. + + This class is designed to execute a local simulator using the + `AbstractEngine` and `AbstractJob` interface. This class will + keep track of the status based on the sampler's results. + + If the simulation type is SYNCHRONOUS, the sampler will be called + once the appropriate results method is called. Other methods will + be added later. + + This does not support calibration requests. + ` + Attributes: + sampler: Sampler to call for results. + simulation_type: Whether sampler execution should be + synchronous or asynchronous. + """ + + def __init__( + self, + *args, + sampler: cirq.Sampler = cirq.Simulator(), + simulation_type: LocalSimulationType = LocalSimulationType.SYNCHRONOUS, + **kwargs, + ): + super().__init__(*args, **kwargs) + self._sampler = sampler + self._simulation_type = simulation_type + self._state = quantum.enums.ExecutionStatus.State.READY + self._type = simulation_type + self._failure_code = '' + self._failure_message = '' + + def execution_status(self) -> quantum.enums.ExecutionStatus.State: + """Return the execution status of the job.""" + return self._state + + def failure(self) -> Optional[Tuple[str, str]]: + """Return failure code and message of the job if present.""" + return (self._failure_code, self._failure_message) + + def cancel(self) -> None: + """Cancel the job.""" + self._state = quantum.enums.ExecutionStatus.State.CANCELLED + + def delete(self) -> None: + """Deletes the job and result, if any.""" + self.program().delete_job(self.id()) + self._state = quantum.enums.ExecutionStatus.State.STATE_UNSPECIFIED + + def batched_results(self) -> List[List[cirq.Result]]: + """Returns the job results, blocking until the job is complete. + + This method is intended for batched jobs. Instead of flattening + results into a single list, this will return a List[Result] + for each circuit in the batch. + """ + if self._type == LocalSimulationType.SYNCHRONOUS: + reps, sweeps = self.get_repetitions_and_sweeps() + parent = self.program() + programs = [parent.get_circuit(n) for n in range(parent.batch_size())] + try: + self._state = quantum.enums.ExecutionStatus.State.SUCCESS + return self._sampler.run_batch( + programs=programs, + params_list=cast(List[cirq.Sweepable], sweeps), + repetitions=reps, + ) + except Exception as e: + self._failure_code = '500' + self._failure_message = str(e) + self._state = quantum.enums.ExecutionStatus.State.FAILURE + raise e + raise ValueError('Unsupported simulation type {self._type}') + + def results(self) -> List[cirq.Result]: + """Returns the job results, blocking until the job is complete.""" + if self._type == LocalSimulationType.SYNCHRONOUS: + reps, sweeps = self.get_repetitions_and_sweeps() + program = self.program().get_circuit() + try: + self._state = quantum.enums.ExecutionStatus.State.SUCCESS + if sweeps: + return self._sampler.run_sweep( + program=program, params=sweeps[0], repetitions=reps + ) + else: + return [self._sampler.run(program=program, repetitions=reps)] + except Exception as e: + self._failure_code = '500' + self._failure_message = str(e) + self._state = quantum.enums.ExecutionStatus.State.FAILURE + raise e + raise ValueError('Unsupported simulation type {self._type}') + + def calibration_results(self) -> List[CalibrationResult]: + """Returns the results of a run_calibration() call. + + This function will fail if any other type of results were returned. + """ + raise NotImplemented # coverage: ignore diff --git a/cirq-google/cirq_google/engine/simulated_local_job_test.py b/cirq-google/cirq_google/engine/simulated_local_job_test.py new file mode 100644 index 00000000000..f44bbfc34dd --- /dev/null +++ b/cirq-google/cirq_google/engine/simulated_local_job_test.py @@ -0,0 +1,138 @@ +# Copyright 2021 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. +"""A helper for jobs that have been created on the Quantum Engine.""" +import pytest +import numpy as np +import sympy + +import cirq +from cirq_google.engine.client import quantum +from cirq_google.engine.abstract_local_program import AbstractLocalProgram +from cirq_google.engine.simulated_local_job import SimulatedLocalJob +from cirq_google.engine.local_simulation_type import LocalSimulationType + +Q = cirq.GridQubit(2, 2) + + +class ParentProgram(AbstractLocalProgram): + def delete(self, delete_jobs: bool = False) -> None: + pass + + def delete_job(self, program_id: str) -> None: + pass + + +def test_run(): + program = ParentProgram([cirq.Circuit(cirq.X(Q), cirq.measure(Q, key='m'))], None) + job = SimulatedLocalJob( + job_id='test_job', processor_id='test1', parent_program=program, repetitions=100, sweeps=[] + ) + assert job.id() == 'test_job' + assert job.execution_status() == quantum.enums.ExecutionStatus.State.READY + results = job.results() + assert np.all(results[0].measurements['m'] == 1) + assert job.execution_status() == quantum.enums.ExecutionStatus.State.SUCCESS + + +def test_run_sweep(): + program = ParentProgram( + [cirq.Circuit(cirq.X(Q) ** sympy.Symbol('t'), cirq.measure(Q, key='m'))], None + ) + job = SimulatedLocalJob( + job_id='test_job', + processor_id='test1', + parent_program=program, + repetitions=100, + sweeps=[cirq.Points(key='t', points=[1, 0])], + ) + assert job.execution_status() == quantum.enums.ExecutionStatus.State.READY + results = job.results() + assert np.all(results[0].measurements['m'] == 1) + assert np.all(results[1].measurements['m'] == 0) + assert job.execution_status() == quantum.enums.ExecutionStatus.State.SUCCESS + + +def test_run_batch(): + program = ParentProgram( + [ + cirq.Circuit(cirq.X(Q) ** sympy.Symbol('t'), cirq.measure(Q, key='m')), + cirq.Circuit(cirq.X(Q) ** sympy.Symbol('x'), cirq.measure(Q, key='m2')), + ], + None, + ) + job = SimulatedLocalJob( + job_id='test_job', + processor_id='test1', + parent_program=program, + repetitions=100, + sweeps=[cirq.Points(key='t', points=[1, 0]), cirq.Points(key='x', points=[0, 1])], + ) + assert job.execution_status() == quantum.enums.ExecutionStatus.State.READY + results = job.batched_results() + assert np.all(results[0][0].measurements['m'] == 1) + assert np.all(results[0][1].measurements['m'] == 0) + assert np.all(results[1][0].measurements['m2'] == 0) + assert np.all(results[1][1].measurements['m2'] == 1) + assert job.execution_status() == quantum.enums.ExecutionStatus.State.SUCCESS + + +def test_cancel(): + program = ParentProgram([cirq.Circuit(cirq.X(Q), cirq.measure(Q, key='m'))], None) + job = SimulatedLocalJob( + job_id='test_job', processor_id='test1', parent_program=program, repetitions=100, sweeps=[] + ) + job.cancel() + assert job.execution_status() == quantum.enums.ExecutionStatus.State.CANCELLED + + +def test_unsupported_types(): + program = ParentProgram([cirq.Circuit(cirq.X(Q), cirq.measure(Q, key='m'))], None) + job = SimulatedLocalJob( + job_id='test_job', + processor_id='test1', + parent_program=program, + repetitions=100, + sweeps=[], + simulation_type=LocalSimulationType.ASYNCHRONOUS, + ) + with pytest.raises(ValueError, match='Unsupported simulation type'): + job.results() + with pytest.raises(ValueError, match='Unsupported simulation type'): + job.batched_results() + + +def test_failure(): + program = ParentProgram( + [cirq.Circuit(cirq.X(Q) ** sympy.Symbol('t'), cirq.measure(Q, key='m'))], None + ) + job = SimulatedLocalJob( + job_id='test_job', + processor_id='test1', + parent_program=program, + repetitions=100, + sweeps=[cirq.Points(key='x', points=[1, 0])], + ) + try: + _ = job.results() + except ValueError: + code, message = job.failure() + assert code == '500' + assert 'Circuit contains ops whose symbols were not specified' in message + + try: + _ = job.batched_results() + except ValueError: + code, message = job.failure() + assert code == '500' + assert 'Circuit contains ops whose symbols were not specified' in message diff --git a/cirq-google/cirq_google/engine/simulated_local_processor.py b/cirq-google/cirq_google/engine/simulated_local_processor.py new file mode 100644 index 00000000000..31b8c5c2aee --- /dev/null +++ b/cirq-google/cirq_google/engine/simulated_local_processor.py @@ -0,0 +1,305 @@ +# Copyright 2021 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 Dict, Iterable, List, Optional, Sequence, TYPE_CHECKING, Union + +import cirq + +from cirq_google.api import v2 +from cirq_google.engine import calibration +from cirq_google.engine.abstract_local_processor import AbstractLocalProcessor +from cirq_google.engine.abstract_local_program import AbstractLocalProgram +from cirq_google.engine.abstract_program import AbstractProgram +from cirq_google.engine.local_simulation_type import LocalSimulationType +from cirq_google.engine.simulated_local_job import SimulatedLocalJob +from cirq_google.engine.simulated_local_program import SimulatedLocalProgram + +import cirq_google.engine.validating_sampler as validating_sampler + +if TYPE_CHECKING: + from cirq_google.serialization.serializer import Serializer + +VALID_LANGUAGES = [ + 'type.googleapis.com/cirq.google.api.v2.Program', + 'type.googleapis.com/cirq.google.api.v2.FocusedCalibration', + 'type.googleapis.com/cirq.google.api.v2.BatchProgram', +] + + +class SimulatedLocalProcessor(AbstractLocalProcessor): + """A processor backed by a sampler and device. + + Intended for local simulation testing, this processor will + create a VAlidationSampler that will validate requests based on + the provided device and an additional Callable (that can verify + serialization constraints, for instance). Jobs will then be + executed using the provided sampler. + + This class also supports a list of calibration metrics that are + stored in-memory to replicate Quantum Engine calibration metrics. + + This class can be used as a local emulator for the Quantum Engine + API or for testing or mocking. + + Attributes: + sammpler: A `cirq.Sampler` that can execute the quantum jobs. + device: An optional device, for validation of qubit connectivity. + validator: A Callable that can validate additional characteristics + beyond the device, such as serialization, repetition limits, etc. + calibrations: A dictionary of calibration metrics keyed by epoch seconds + that can be returned by the processor. + """ + + def __init__( + self, + *args, + sampler: cirq.Sampler = cirq.Simulator(), + device: cirq.Device = cirq.UNCONSTRAINED_DEVICE, + validator: validating_sampler.VALIDATOR_TYPE = None, + simulation_type: LocalSimulationType = LocalSimulationType.SYNCHRONOUS, + calibrations: Optional[Dict[int, calibration.Calibration]] = None, + **kwargs, + ): + super().__init__(*args, **kwargs) + self._calibrations = calibrations or {} + self._device = device + self._simulation_type = simulation_type + self._validator = validator + self._sampler = validating_sampler.ValidatingSampler( + device=self._device, validator=self._validator, sampler=sampler + ) + self._programs: Dict[str, AbstractLocalProgram] = {} + + def remove_program(self, program_id: str): + """Remove reference to a child program.""" + if program_id in self._programs: + del self._programs[program_id] + + def get_calibration(self, calibration_timestamp_seconds: int) -> calibration.Calibration: + return self._calibrations[calibration_timestamp_seconds] + + def get_latest_calibration(self, timestamp: int) -> Optional[calibration.Calibration]: + latest = None + current_calibration = None + for calibration_seconds in self._calibrations: + if calibration_seconds <= timestamp and ( + latest is None or latest < calibration_seconds + ): + latest = calibration_seconds + current_calibration = self._calibrations[latest] + return current_calibration + + def get_current_calibration(self) -> Optional[calibration.Calibration]: + return self.get_latest_calibration(int(datetime.datetime.now().timestamp())) + + def get_device(self, gate_sets: Optional[Iterable['Serializer']] = None) -> cirq.Device: + """Returns a `Device` created from the processor's device specification. + + This method queries the processor to retrieve the device specification, + which is then use to create a `SerializableDevice` that will validate + that operations are supported and use the correct qubits. + """ + return self._device + + def get_device_specification(self) -> Optional[v2.device_pb2.DeviceSpecification]: + raise NotImplemented # coverage: ignore + + def health(self): + return 'OK' + + def list_calibrations( + self, + earliest_timestamp_seconds: Optional[int] = None, + latest_timestamp_seconds: Optional[int] = None, + ) -> List[calibration.Calibration]: + calibration_list: List[calibration.Calibration] = [] + for calibration_seconds in self._calibrations: + if ( + earliest_timestamp_seconds is not None + and earliest_timestamp_seconds > calibration_seconds + ): + continue + if ( + latest_timestamp_seconds is not None + and latest_timestamp_seconds < calibration_seconds + ): + continue + calibration_list.append(self._calibrations[calibration_seconds]) + return calibration_list + + def get_sampler(self, gate_set: Optional['Serializer'] = None) -> cirq.Sampler: + return self._sampler + + def supported_languages(self) -> List[str]: + return VALID_LANGUAGES + + def list_programs( + self, + created_before: Optional[Union[datetime.datetime, datetime.date]] = None, + created_after: Optional[Union[datetime.datetime, datetime.date]] = None, + has_labels: Optional[Dict[str, str]] = None, + ) -> List[AbstractLocalProgram]: + programs: List[AbstractLocalProgram] = [] + for program in self._programs.values(): + if created_before is not None and created_before < program.create_time(): + continue + if created_after is not None and created_after > program.create_time(): + continue + if has_labels is not None: + labels = program.labels() + if any(key not in labels or labels[key] != has_labels[key] for key in has_labels): + continue + programs.append(program) + return programs + + def get_program(self, program_id: str) -> AbstractProgram: + """Returns an AbstractProgram for an existing Quantum Engine program. + + Args: + program_id: Unique ID of the program within the parent project. + + Returns: + An AbstractProgram for the program. + + Raises: + KeyError: if program is not found + """ + return self._programs[program_id] + + def run_batch( + self, + programs: Sequence[cirq.AbstractCircuit], + program_id: Optional[str] = None, + job_id: Optional[str] = None, + params_list: List[cirq.Sweepable] = None, + repetitions: int = 1, + gate_set: Optional['Serializer'] = None, + program_description: Optional[str] = None, + program_labels: Optional[Dict[str, str]] = None, + job_description: Optional[str] = None, + job_labels: Optional[Dict[str, str]] = None, + ) -> SimulatedLocalJob: + if program_id is None: + program_id = self._create_id(id_type='program') + if job_id is None: + job_id = self._create_id(id_type='job') + self._programs[program_id] = SimulatedLocalProgram( + program_id=program_id, + simulation_type=self._simulation_type, + circuits=programs, + engine=self.engine(), + processor=self, + ) + job = SimulatedLocalJob( + job_id=job_id, + processor_id=self.processor_id, + parent_program=self._programs[program_id], + repetitions=repetitions, + sweeps=params_list, + sampler=self._sampler, + simulation_type=self._simulation_type, + ) + self._programs[program_id].add_job(job_id, job) + return job + + def run( + self, + program: cirq.Circuit, + program_id: Optional[str] = None, + job_id: Optional[str] = None, + param_resolver: cirq.ParamResolver = cirq.ParamResolver({}), + repetitions: int = 1, + gate_set: Optional['Serializer'] = None, + program_description: Optional[str] = None, + program_labels: Optional[Dict[str, str]] = None, + job_description: Optional[str] = None, + job_labels: Optional[Dict[str, str]] = None, + ) -> cirq.Result: + """Runs the supplied Circuit on this processor. + + Args: + program: The Circuit to execute. If a circuit is + provided, a moment by moment schedule will be used. + 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 + 'prog-################YYMMDD' will be generated, where # is + alphanumeric and YYMMDD is the current year, month, and day. + job_id: Job identifier to use. If this is not provided, a random id + of the format 'job-################YYMMDD' will be generated, + where # is alphanumeric and YYMMDD is the current year, month, + and day. + param_resolver: Parameters to run with the program. + repetitions: The number of repetitions to simulate. + gate_set: The gate set used to serialize the circuit. The gate set + must be supported by the selected processor. + program_description: An optional description to set on the program. + program_labels: Optional set of labels to set on the program. + job_description: An optional description to set on the job. + job_labels: Optional set of labels to set on the job. + Returns: + A single Result for this run. + """ + return self.run_sweep( + program=program, + program_id=program_id, + job_id=job_id, + params=[param_resolver], + repetitions=repetitions, + gate_set=gate_set, + program_description=program_description, + program_labels=program_labels, + job_description=job_description, + job_labels=job_labels, + ).results()[0] + + def run_sweep( + self, + program: cirq.Circuit, + program_id: Optional[str] = None, + job_id: Optional[str] = None, + params: cirq.Sweepable = None, + repetitions: int = 1, + gate_set: Optional['Serializer'] = None, + program_description: Optional[str] = None, + program_labels: Optional[Dict[str, str]] = None, + job_description: Optional[str] = None, + job_labels: Optional[Dict[str, str]] = None, + ) -> SimulatedLocalJob: + if program_id is None: + program_id = self._create_id(id_type='program') + if job_id is None: + job_id = self._create_id(id_type='job') + self._programs[program_id] = SimulatedLocalProgram( + program_id=program_id, + simulation_type=self._simulation_type, + circuits=[program], + processor=self, + engine=self.engine(), + ) + job = SimulatedLocalJob( + job_id=job_id, + processor_id=self.processor_id, + parent_program=self._programs[program_id], + repetitions=repetitions, + sweeps=[params], + sampler=self._sampler, + simulation_type=self._simulation_type, + ) + self._programs[program_id].add_job(job_id, job) + return job + + def run_calibration(self, *args, **kwargs): + raise NotImplemented # coverage: ignore diff --git a/cirq-google/cirq_google/engine/simulated_local_processor_test.py b/cirq-google/cirq_google/engine/simulated_local_processor_test.py new file mode 100644 index 00000000000..6c93bb20f6d --- /dev/null +++ b/cirq-google/cirq_google/engine/simulated_local_processor_test.py @@ -0,0 +1,219 @@ +# Copyright 2021 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. +"""A helper for jobs that have been created on the Quantum Engine.""" +from typing import List +import datetime +import pytest + +import numpy as np +import sympy + +import cirq +import cirq_google +from cirq_google.api import v2 +from cirq_google.engine.client import quantum +from cirq_google.engine.simulated_local_processor import SimulatedLocalProcessor, VALID_LANGUAGES + + +def test_calibrations(): + future = int((datetime.datetime.now() + datetime.timedelta(hours=2)).timestamp()) + cal_proto1 = v2.metrics_pb2.MetricsSnapshot(timestamp_ms=10000) + cal_proto2 = v2.metrics_pb2.MetricsSnapshot(timestamp_ms=20000) + cal_proto3 = v2.metrics_pb2.MetricsSnapshot(timestamp_ms=future * 1000) + cal1 = cirq_google.Calibration(cal_proto1) + cal2 = cirq_google.Calibration(cal_proto2) + cal3 = cirq_google.Calibration(cal_proto3) + proc = SimulatedLocalProcessor( + processor_id='test_proc', calibrations={10000: cal1, 20000: cal2, future: cal3} + ) + assert proc.get_calibration(10000) == cal1 + assert proc.get_calibration(20000) == cal2 + assert proc.get_calibration(future) == cal3 + assert proc.get_current_calibration() == cal2 + assert proc.list_calibrations( + earliest_timestamp_seconds=5000, latest_timestamp_seconds=15000 + ) == [cal1] + assert proc.list_calibrations( + earliest_timestamp_seconds=15000, latest_timestamp_seconds=25000 + ) == [cal2] + assert proc.list_calibrations( + earliest_timestamp_seconds=future - 1, latest_timestamp_seconds=future + 1 + ) == [cal3] + cal_list = proc.list_calibrations(latest_timestamp_seconds=25000) + assert len(cal_list) == 2 + assert cal1 in cal_list + assert cal2 in cal_list + cal_list = proc.list_calibrations(earliest_timestamp_seconds=15000) + assert len(cal_list) == 2 + assert cal2 in cal_list + assert cal3 in cal_list + cal_list = proc.list_calibrations() + assert len(cal_list) == 3 + assert cal1 in cal_list + assert cal2 in cal_list + assert cal3 in cal_list + + +def test_accessors(): + proc = SimulatedLocalProcessor(processor_id='test_proc', device=cirq_google.Sycamore23) + assert proc.health() + assert proc.get_device() == cirq_google.Sycamore23 + assert proc.supported_languages() == VALID_LANGUAGES + + +def test_list_jobs(): + proc = SimulatedLocalProcessor(processor_id='test_proc') + job1 = proc.run_sweep(cirq.Circuit(), params={}, repetitions=100) + job2 = proc.run_sweep(cirq.Circuit(), params={}, repetitions=100) + + program1 = job1.program() + program2 = job2.program() + program1.set_labels({'color': 'green'}) + program2.set_labels({'color': 'red', 'shape': 'blue'}) + + # Modify creation times in order to make search deterministic + program1._create_time = datetime.datetime.fromtimestamp(1000) + program2._create_time = datetime.datetime.fromtimestamp(2000) + + assert proc.list_programs(created_before=datetime.datetime.fromtimestamp(1500)) == [program1] + assert proc.list_programs(created_after=datetime.datetime.fromtimestamp(1500)) == [program2] + program_list = proc.list_programs(created_after=datetime.datetime.fromtimestamp(500)) + assert len(program_list) == 2 + assert program1 in program_list + assert program2 in program_list + assert proc.list_programs(has_labels={'color': 'yellow'}) == [] + assert proc.list_programs(has_labels={'color': 'green'}) == [program1] + assert proc.list_programs(has_labels={'color': 'red'}) == [program2] + assert proc.list_programs(has_labels={'shape': 'blue'}) == [program2] + assert proc.list_programs(has_labels={'color': 'red', 'shape': 'blue'}) == [program2] + + +def test_delete(): + proc = SimulatedLocalProcessor(processor_id='test_proc') + job1 = proc.run_sweep(cirq.Circuit(), params={}, repetitions=100) + job2 = proc.run_sweep(cirq.Circuit(), params={}, repetitions=200) + program1 = job1.program() + program2 = job2.program() + job1_id = job1.id() + job2_id = job2.id() + program1_id = program1.id() + program2_id = program2.id() + assert program1.get_job(job1_id) == job1 + assert program2.get_job(job2_id) == job2 + assert proc.get_program(program1_id) == program1 + assert proc.get_program(program2_id) == program2 + job1.delete() + assert proc.get_program(program1_id) == program1 + with pytest.raises(KeyError, match='not found'): + _ = program1.get_job(job1_id) + program2.delete(delete_jobs=True) + with pytest.raises(KeyError, match='not found'): + _ = program2.get_job(job2_id) + with pytest.raises(KeyError, match='not found'): + _ = program2.get_job(program2_id) + + +def test_run(): + proc = SimulatedLocalProcessor(processor_id='test_proc') + q = cirq.GridQubit(5, 4) + circuit = cirq.Circuit(cirq.X(q), cirq.measure(q, key='m')) + result = proc.run(circuit, repetitions=100) + assert np.all(result.measurements['m'] == 1) + + +def test_run_sweep(): + proc = SimulatedLocalProcessor(processor_id='test_proc') + q = cirq.GridQubit(5, 4) + circuit = cirq.Circuit(cirq.X(q) ** sympy.Symbol('t'), cirq.measure(q, key='m')) + sweep = cirq.Points(key='t', points=[1, 0]) + job = proc.run_sweep(circuit, params=sweep, repetitions=100, program_id='abc', job_id='def') + assert proc.get_program('abc') == job.program() + assert proc.get_program('abc').get_job('def') == job + assert job.execution_status() == quantum.enums.ExecutionStatus.State.READY + assert len(job) == 2 + assert np.all(job[0].measurements['m'] == 1) + assert np.all(job[1].measurements['m'] == 0) + # Test iteration + for idx, result in enumerate(job): + assert np.all(result.measurements['m'] == 1 - idx) + + assert job.execution_status() == quantum.enums.ExecutionStatus.State.SUCCESS + + # with default program_id and job_id + job = proc.run_sweep(circuit, params=sweep, repetitions=100) + assert job.execution_status() == quantum.enums.ExecutionStatus.State.READY + results = job.results() + assert np.all(results[0].measurements['m'] == 1) + assert np.all(results[1].measurements['m'] == 0) + assert job.execution_status() == quantum.enums.ExecutionStatus.State.SUCCESS + + # iteration + + +def test_run_batch(): + q = cirq.GridQubit(5, 4) + proc = SimulatedLocalProcessor(processor_id='test_proc') + circuits = [ + cirq.Circuit(cirq.X(q) ** sympy.Symbol('t'), cirq.measure(q, key='m')), + cirq.Circuit(cirq.X(q) ** sympy.Symbol('x'), cirq.measure(q, key='m2')), + ] + sweeps = [cirq.Points(key='t', points=[1, 0]), cirq.Points(key='x', points=[0, 1])] + job = proc.run_batch(circuits, params_list=sweeps, repetitions=100) + assert job.execution_status() == quantum.enums.ExecutionStatus.State.READY + results = job.batched_results() + assert np.all(results[0][0].measurements['m'] == 1) + assert np.all(results[0][1].measurements['m'] == 0) + assert np.all(results[1][0].measurements['m2'] == 0) + assert np.all(results[1][1].measurements['m2'] == 1) + assert job.execution_status() == quantum.enums.ExecutionStatus.State.SUCCESS + + +def _no_y_gates(circuits: List[cirq.Circuit], sweeps: List[cirq.Sweepable], repetitions: int): + for circuit in circuits: + for moment in circuit: + for op in moment: + if op.gate == cirq.Y: + raise ValueError('No Y gates allowed!') + + +def test_device_validation(): + proc = SimulatedLocalProcessor( + processor_id='test_proc', device=cirq_google.Sycamore23, validator=_no_y_gates + ) + + q = cirq.GridQubit(2, 2) + circuit = cirq.Circuit(cirq.X(q) ** sympy.Symbol('t'), cirq.measure(q, key='m')) + sweep = cirq.Points(key='t', points=[1, 0]) + job = proc.run_sweep(circuit, params=sweep, repetitions=100) + with pytest.raises(ValueError, match='Qubit not on device'): + job.results() + # Test validation through sampler + with pytest.raises(ValueError, match='Qubit not on device'): + _ = proc.get_sampler().run_sweep(circuit, params=sweep, repetitions=100) + + +def test_additional_validation(): + proc = SimulatedLocalProcessor( + processor_id='test_proc', device=cirq_google.Sycamore23, validator=_no_y_gates + ) + q = cirq.GridQubit(5, 4) + circuit = cirq.Circuit(cirq.X(q) ** sympy.Symbol('t'), cirq.Y(q), cirq.measure(q, key='m')) + sweep = cirq.Points(key='t', points=[1, 0]) + job = proc.run_sweep(circuit, params=sweep, repetitions=100) + with pytest.raises(ValueError, match='No Y gates allowed!'): + job.results() + + # Test validation through sampler + with pytest.raises(ValueError, match='No Y gates allowed!'): + _ = proc.get_sampler().run_sweep(circuit, params=sweep, repetitions=100) diff --git a/cirq-google/cirq_google/engine/simulated_local_program.py b/cirq-google/cirq_google/engine/simulated_local_program.py new file mode 100644 index 00000000000..272b1c442ff --- /dev/null +++ b/cirq-google/cirq_google/engine/simulated_local_program.py @@ -0,0 +1,57 @@ +# Copyright 2021 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. +from typing import Optional, TYPE_CHECKING + +from cirq_google.engine.abstract_local_program import AbstractLocalProgram +from cirq_google.engine.local_simulation_type import LocalSimulationType + +if TYPE_CHECKING: + from cirq_google.engine.abstract_job import AbstractJob + from cirq_google.engine.abstract_engine import AbstractEngine + from cirq_google.engine.simulated_local_processor import SimulatedLocalProcessor + + +class SimulatedLocalProgram(AbstractLocalProgram): + """A program backed by a (local) sampler. + + This class functions as a parent class for a `SimulatedLocalJob` + object. + + This is an abstract class that inheritors should implement. + """ + + def __init__( + self, + *args, + program_id: str, + simulation_type: LocalSimulationType = LocalSimulationType.SYNCHRONOUS, + processor: Optional['SimulatedLocalProcessor'] = None, + **kwargs, + ): + super().__init__(*args, **kwargs) + self._id = program_id + self._processor = processor + + def delete(self, delete_jobs: bool = False) -> None: + if self._processor: + self._processor.remove_program(self._id) + if delete_jobs: + for job in list(self._jobs.values()): + job.delete() + + def delete_job(self, job_id: str) -> None: + del self._jobs[job_id] + + def id(self) -> str: + return self._id diff --git a/cirq-google/cirq_google/engine/validating_sampler_test.py b/cirq-google/cirq_google/engine/validating_sampler_test.py index 03a2a5dab2a..673fca6abc0 100644 --- a/cirq-google/cirq_google/engine/validating_sampler_test.py +++ b/cirq-google/cirq_google/engine/validating_sampler_test.py @@ -96,7 +96,7 @@ def test_sweeps_validation(): def test_batch_default_sweeps(): sampler = cg.ValidatingSampler() - q=cirq.GridQubit(2, 2) + q = cirq.GridQubit(2, 2) circuits = [ cirq.Circuit(cirq.X(q), cirq.measure(q, key='m')), cirq.Circuit(cirq.measure(q, key='m2')), @@ -104,4 +104,3 @@ def test_batch_default_sweeps(): results = sampler.run_batch(circuits, None, repetitions=100) assert np.all(results[0][0].measurements['m'] == 1) assert np.all(results[1][0].measurements['m2'] == 0) - From 5c3ef59656fa78c7d3e5a91b4bd4f26f0b6f42c6 Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Mon, 8 Nov 2021 13:53:56 -0800 Subject: [PATCH 05/16] Add more zeros to make windows happy. --- .../engine/abstract_local_processor_test.py | 202 +++++++++--------- 1 file changed, 101 insertions(+), 101 deletions(-) diff --git a/cirq-google/cirq_google/engine/abstract_local_processor_test.py b/cirq-google/cirq_google/engine/abstract_local_processor_test.py index ab033e02bb8..74f708ec4ff 100644 --- a/cirq-google/cirq_google/engine/abstract_local_processor_test.py +++ b/cirq-google/cirq_google/engine/abstract_local_processor_test.py @@ -93,8 +93,8 @@ def test_bad_reservation(): p = NothingProcessor(processor_id='test') with pytest.raises(ValueError, match='after the start time'): _ = p.create_reservation( - start_time=_time(2000), - end_time=_time(1000), + start_time=_time(200000), + end_time=_time(100000), ) @@ -174,14 +174,14 @@ def test_list_reservations(): def test_bad_schedule(): time_slot1 = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=1000), - end_time=Timestamp(seconds=3000), + start_time=Timestamp(seconds=100000), + end_time=Timestamp(seconds=300000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, ) time_slot2 = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=2000), - end_time=Timestamp(seconds=4000), + start_time=Timestamp(seconds=200000), + end_time=Timestamp(seconds=400000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, ) with pytest.raises(ValueError, match='cannot overlap'): @@ -191,53 +191,53 @@ def test_bad_schedule(): def test_get_schedule(): time_slot = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=1000), - end_time=Timestamp(seconds=2000), + start_time=Timestamp(seconds=100000), + end_time=Timestamp(seconds=200000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, ) p = NothingProcessor(processor_id='test', schedule=[time_slot]) - assert p.get_schedule(from_time=_time(500), to_time=_time(2500)) == [time_slot] - assert p.get_schedule(from_time=_time(1500), to_time=_time(2500)) == [time_slot] - assert p.get_schedule(from_time=_time(500), to_time=_time(1500)) == [time_slot] - assert p.get_schedule(from_time=_time(500), to_time=_time(750)) == [] - assert p.get_schedule(from_time=_time(2500), to_time=_time(3000)) == [] + assert p.get_schedule(from_time=_time(50000), to_time=_time(250000)) == [time_slot] + assert p.get_schedule(from_time=_time(150000), to_time=_time(250000)) == [time_slot] + assert p.get_schedule(from_time=_time(50000), to_time=_time(150000)) == [time_slot] + assert p.get_schedule(from_time=_time(50000), to_time=_time(75000)) == [] + assert p.get_schedule(from_time=_time(250000), to_time=_time(300000)) == [] # check unbounded cases unbounded_start = qtypes.QuantumTimeSlot( processor_name='test', - end_time=Timestamp(seconds=1000), + end_time=Timestamp(seconds=100000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, ) unbounded_end = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=2000), + start_time=Timestamp(seconds=200000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, ) p = NothingProcessor(processor_id='test', schedule=[unbounded_start, unbounded_end]) assert ( p.get_schedule( - from_time=_time(500), - to_time=_time(2500), + from_time=_time(50000), + to_time=_time(250000), ) == [unbounded_start, unbounded_end] ) assert ( p.get_schedule( - from_time=_time(1500), - to_time=_time(2500), + from_time=_time(150000), + to_time=_time(250000), ) == [unbounded_end] ) assert ( p.get_schedule( - from_time=_time(500), - to_time=_time(1500), + from_time=_time(50000), + to_time=_time(150000), ) == [unbounded_start] ) assert ( p.get_schedule( - from_time=_time(1200), - to_time=_time(1500), + from_time=_time(120000), + to_time=_time(150000), ) == [] ) @@ -248,18 +248,18 @@ def test_get_schedule(): ( qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=1000), - end_time=Timestamp(seconds=2000), + start_time=Timestamp(seconds=100000), + end_time=Timestamp(seconds=200000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=1000), + start_time=Timestamp(seconds=100000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, ), qtypes.QuantumTimeSlot( processor_name='test', - end_time=Timestamp(seconds=2000), + end_time=Timestamp(seconds=200000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, ), ), @@ -268,34 +268,34 @@ def test_create_reservation_not_available(time_slot): p = NothingProcessor(processor_id='test', schedule=[time_slot]) with pytest.raises(ValueError, match='Time slot is not available for reservations'): p.create_reservation( - start_time=_time(500), - end_time=_time(1500), + start_time=_time(50000), + end_time=_time(150000), ) def test_create_reservation_open_time_slots(): time_slot = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=1000), - end_time=Timestamp(seconds=2000), + start_time=Timestamp(seconds=100000), + end_time=Timestamp(seconds=200000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ) p = NothingProcessor(processor_id='test', schedule=[time_slot]) p.create_reservation( - start_time=_time(500), - end_time=_time(1500), + start_time=_time(50000), + end_time=_time(150000), ) - assert p.get_schedule(from_time=_time(0), to_time=_time(2500)) == [ + assert p.get_schedule(from_time=_time(0), to_time=_time(250000)) == [ qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=500), - end_time=Timestamp(seconds=1500), + start_time=Timestamp(seconds=50000), + end_time=Timestamp(seconds=150000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=1500), - end_time=Timestamp(seconds=2000), + start_time=Timestamp(seconds=150000), + end_time=Timestamp(seconds=200000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), ] @@ -304,32 +304,32 @@ def test_create_reservation_open_time_slots(): def test_create_reservation_split_time_slots(): time_slot = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=1000), - end_time=Timestamp(seconds=2000), + start_time=Timestamp(seconds=100000), + end_time=Timestamp(seconds=200000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ) p = NothingProcessor(processor_id='test', schedule=[time_slot]) p.create_reservation( - start_time=_time(1200), - end_time=_time(1500), + start_time=_time(120000), + end_time=_time(150000), ) - assert p.get_schedule(from_time=_time(0), to_time=_time(2500)) == [ + assert p.get_schedule(from_time=_time(0), to_time=_time(250000)) == [ qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=1000), - end_time=Timestamp(seconds=1200), + start_time=Timestamp(seconds=100000), + end_time=Timestamp(seconds=120000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=1200), - end_time=Timestamp(seconds=1500), + start_time=Timestamp(seconds=120000), + end_time=Timestamp(seconds=150000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=1500), - end_time=Timestamp(seconds=2000), + start_time=Timestamp(seconds=150000), + end_time=Timestamp(seconds=200000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), ] @@ -338,26 +338,26 @@ def test_create_reservation_split_time_slots(): def test_create_reservation_add_at_end(): time_slot = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=1000), - end_time=Timestamp(seconds=2000), + start_time=Timestamp(seconds=100000), + end_time=Timestamp(seconds=200000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ) p = NothingProcessor(processor_id='test', schedule=[time_slot]) p.create_reservation( - start_time=_time(2500), - end_time=_time(3500), + start_time=_time(250000), + end_time=_time(350000), ) - assert p.get_schedule(from_time=_time(0), to_time=_time(2500)) == [ + assert p.get_schedule(from_time=_time(50000), to_time=_time(250000)) == [ qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=1000), - end_time=Timestamp(seconds=2000), + start_time=Timestamp(seconds=100000), + end_time=Timestamp(seconds=200000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=2500), - end_time=Timestamp(seconds=3500), + start_time=Timestamp(seconds=250000), + end_time=Timestamp(seconds=350000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, ), ] @@ -366,36 +366,36 @@ def test_create_reservation_add_at_end(): def test_create_reservation_border_conditions(): time_slot = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=1000), - end_time=Timestamp(seconds=2000), + start_time=Timestamp(seconds=100000), + end_time=Timestamp(seconds=200000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ) p = NothingProcessor(processor_id='test', schedule=[time_slot]) p.create_reservation( - start_time=_time(1900), - end_time=_time(2000), + start_time=_time(190000), + end_time=_time(200000), ) p.create_reservation( - start_time=_time(1000), - end_time=_time(1100), + start_time=_time(100000), + end_time=_time(110000), ) - assert p.get_schedule(from_time=_time(0), to_time=_time(2500)) == [ + assert p.get_schedule(from_time=_time(0), to_time=_time(250000)) == [ qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=1000), - end_time=Timestamp(seconds=1100), + start_time=Timestamp(seconds=100000), + end_time=Timestamp(seconds=110000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=1100), - end_time=Timestamp(seconds=1900), + start_time=Timestamp(seconds=110000), + end_time=Timestamp(seconds=190000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=1900), - end_time=Timestamp(seconds=2000), + start_time=Timestamp(seconds=190000), + end_time=Timestamp(seconds=200000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, ), ] @@ -404,44 +404,44 @@ def test_create_reservation_border_conditions(): def test_create_reservation_unbounded(): time_slot_begin = qtypes.QuantumTimeSlot( processor_name='test', - end_time=Timestamp(seconds=2000), + end_time=Timestamp(seconds=200000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ) time_slot_end = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=5000), + start_time=Timestamp(seconds=500000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ) p = NothingProcessor(processor_id='test', schedule=[time_slot_begin, time_slot_end]) p.create_reservation( - start_time=_time(1000), - end_time=_time(3000), + start_time=_time(100000), + end_time=_time(300000), ) p.create_reservation( - start_time=_time(4000), - end_time=_time(6000), + start_time=_time(400000), + end_time=_time(600000), ) - assert p.get_schedule(from_time=_time(0), to_time=_time(10000)) == [ + assert p.get_schedule(from_time=_time(0), to_time=_time(1000000)) == [ qtypes.QuantumTimeSlot( processor_name='test', - end_time=Timestamp(seconds=1000), + end_time=Timestamp(seconds=100000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=1000), - end_time=Timestamp(seconds=3000), + start_time=Timestamp(seconds=100000), + end_time=Timestamp(seconds=300000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=4000), - end_time=Timestamp(seconds=6000), + start_time=Timestamp(seconds=400000), + end_time=Timestamp(seconds=600000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=6000), + start_time=Timestamp(seconds=600000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), ] @@ -450,56 +450,56 @@ def test_create_reservation_unbounded(): def test_create_reservation_splitunbounded(): time_slot_begin = qtypes.QuantumTimeSlot( processor_name='test', - end_time=Timestamp(seconds=3000), + end_time=Timestamp(seconds=300000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ) time_slot_end = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=5000), + start_time=Timestamp(seconds=500000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ) p = NothingProcessor(processor_id='test', schedule=[time_slot_begin, time_slot_end]) p.create_reservation( - start_time=_time(1000), - end_time=_time(2000), + start_time=_time(100000), + end_time=_time(200000), ) p.create_reservation( - start_time=_time(6000), - end_time=_time(7000), + start_time=_time(600000), + end_time=_time(700000), ) - assert p.get_schedule(from_time=_time(0), to_time=_time(10000)) == [ + assert p.get_schedule(from_time=_time(0), to_time=_time(1000000)) == [ qtypes.QuantumTimeSlot( processor_name='test', - end_time=Timestamp(seconds=1000), + end_time=Timestamp(seconds=100000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=1000), - end_time=Timestamp(seconds=2000), + start_time=Timestamp(seconds=100000), + end_time=Timestamp(seconds=200000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=2000), - end_time=Timestamp(seconds=3000), + start_time=Timestamp(seconds=200000), + end_time=Timestamp(seconds=300000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=5000), - end_time=Timestamp(seconds=6000), + start_time=Timestamp(seconds=500000), + end_time=Timestamp(seconds=600000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=6000), - end_time=Timestamp(seconds=7000), + start_time=Timestamp(seconds=600000), + end_time=Timestamp(seconds=700000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=7000), + start_time=Timestamp(seconds=700000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), ] From 2fb3b3816910a69e84302df21ea15cc75f7e371a Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Mon, 8 Nov 2021 15:40:52 -0800 Subject: [PATCH 06/16] Try again. Windows really does not like small timestamps. --- .../engine/abstract_local_processor_test.py | 202 +++++++++--------- 1 file changed, 101 insertions(+), 101 deletions(-) diff --git a/cirq-google/cirq_google/engine/abstract_local_processor_test.py b/cirq-google/cirq_google/engine/abstract_local_processor_test.py index 74f708ec4ff..461cc224da9 100644 --- a/cirq-google/cirq_google/engine/abstract_local_processor_test.py +++ b/cirq-google/cirq_google/engine/abstract_local_processor_test.py @@ -93,8 +93,8 @@ def test_bad_reservation(): p = NothingProcessor(processor_id='test') with pytest.raises(ValueError, match='after the start time'): _ = p.create_reservation( - start_time=_time(200000), - end_time=_time(100000), + start_time=_time(2000000), + end_time=_time(1000000), ) @@ -174,14 +174,14 @@ def test_list_reservations(): def test_bad_schedule(): time_slot1 = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=100000), - end_time=Timestamp(seconds=300000), + start_time=Timestamp(seconds=1000000), + end_time=Timestamp(seconds=3000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, ) time_slot2 = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=200000), - end_time=Timestamp(seconds=400000), + start_time=Timestamp(seconds=2000000), + end_time=Timestamp(seconds=4000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, ) with pytest.raises(ValueError, match='cannot overlap'): @@ -191,53 +191,53 @@ def test_bad_schedule(): def test_get_schedule(): time_slot = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=100000), - end_time=Timestamp(seconds=200000), + start_time=Timestamp(seconds=1000000), + end_time=Timestamp(seconds=2000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, ) p = NothingProcessor(processor_id='test', schedule=[time_slot]) - assert p.get_schedule(from_time=_time(50000), to_time=_time(250000)) == [time_slot] - assert p.get_schedule(from_time=_time(150000), to_time=_time(250000)) == [time_slot] - assert p.get_schedule(from_time=_time(50000), to_time=_time(150000)) == [time_slot] - assert p.get_schedule(from_time=_time(50000), to_time=_time(75000)) == [] - assert p.get_schedule(from_time=_time(250000), to_time=_time(300000)) == [] + assert p.get_schedule(from_time=_time(500000), to_time=_time(2500000)) == [time_slot] + assert p.get_schedule(from_time=_time(1500000), to_time=_time(2500000)) == [time_slot] + assert p.get_schedule(from_time=_time(500000), to_time=_time(1500000)) == [time_slot] + assert p.get_schedule(from_time=_time(500000), to_time=_time(750000)) == [] + assert p.get_schedule(from_time=_time(2500000), to_time=_time(300000)) == [] # check unbounded cases unbounded_start = qtypes.QuantumTimeSlot( processor_name='test', - end_time=Timestamp(seconds=100000), + end_time=Timestamp(seconds=1000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, ) unbounded_end = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=200000), + start_time=Timestamp(seconds=2000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, ) p = NothingProcessor(processor_id='test', schedule=[unbounded_start, unbounded_end]) assert ( p.get_schedule( - from_time=_time(50000), - to_time=_time(250000), + from_time=_time(500000), + to_time=_time(2500000), ) == [unbounded_start, unbounded_end] ) assert ( p.get_schedule( - from_time=_time(150000), - to_time=_time(250000), + from_time=_time(1500000), + to_time=_time(2500000), ) == [unbounded_end] ) assert ( p.get_schedule( - from_time=_time(50000), - to_time=_time(150000), + from_time=_time(500000), + to_time=_time(1500000), ) == [unbounded_start] ) assert ( p.get_schedule( - from_time=_time(120000), - to_time=_time(150000), + from_time=_time(1200000), + to_time=_time(1500000), ) == [] ) @@ -248,18 +248,18 @@ def test_get_schedule(): ( qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=100000), - end_time=Timestamp(seconds=200000), + start_time=Timestamp(seconds=1000000), + end_time=Timestamp(seconds=2000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=100000), + start_time=Timestamp(seconds=1000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, ), qtypes.QuantumTimeSlot( processor_name='test', - end_time=Timestamp(seconds=200000), + end_time=Timestamp(seconds=2000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, ), ), @@ -268,34 +268,34 @@ def test_create_reservation_not_available(time_slot): p = NothingProcessor(processor_id='test', schedule=[time_slot]) with pytest.raises(ValueError, match='Time slot is not available for reservations'): p.create_reservation( - start_time=_time(50000), - end_time=_time(150000), + start_time=_time(500000), + end_time=_time(1500000), ) def test_create_reservation_open_time_slots(): time_slot = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=100000), - end_time=Timestamp(seconds=200000), + start_time=Timestamp(seconds=1000000), + end_time=Timestamp(seconds=2000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ) p = NothingProcessor(processor_id='test', schedule=[time_slot]) p.create_reservation( - start_time=_time(50000), - end_time=_time(150000), + start_time=_time(500000), + end_time=_time(1500000), ) - assert p.get_schedule(from_time=_time(0), to_time=_time(250000)) == [ + assert p.get_schedule(from_time=_time(200000), to_time=_time(2500000)) == [ qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=50000), - end_time=Timestamp(seconds=150000), + start_time=Timestamp(seconds=500000), + end_time=Timestamp(seconds=1500000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=150000), - end_time=Timestamp(seconds=200000), + start_time=Timestamp(seconds=1500000), + end_time=Timestamp(seconds=2000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), ] @@ -304,32 +304,32 @@ def test_create_reservation_open_time_slots(): def test_create_reservation_split_time_slots(): time_slot = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=100000), - end_time=Timestamp(seconds=200000), + start_time=Timestamp(seconds=1000000), + end_time=Timestamp(seconds=2000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ) p = NothingProcessor(processor_id='test', schedule=[time_slot]) p.create_reservation( - start_time=_time(120000), - end_time=_time(150000), + start_time=_time(1200000), + end_time=_time(1500000), ) - assert p.get_schedule(from_time=_time(0), to_time=_time(250000)) == [ + assert p.get_schedule(from_time=_time(200000), to_time=_time(2500000)) == [ qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=100000), - end_time=Timestamp(seconds=120000), + start_time=Timestamp(seconds=1000000), + end_time=Timestamp(seconds=1200000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=120000), - end_time=Timestamp(seconds=150000), + start_time=Timestamp(seconds=1200000), + end_time=Timestamp(seconds=1500000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=150000), - end_time=Timestamp(seconds=200000), + start_time=Timestamp(seconds=1500000), + end_time=Timestamp(seconds=2000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), ] @@ -338,26 +338,26 @@ def test_create_reservation_split_time_slots(): def test_create_reservation_add_at_end(): time_slot = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=100000), - end_time=Timestamp(seconds=200000), + start_time=Timestamp(seconds=1000000), + end_time=Timestamp(seconds=2000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ) p = NothingProcessor(processor_id='test', schedule=[time_slot]) p.create_reservation( - start_time=_time(250000), - end_time=_time(350000), + start_time=_time(2500000), + end_time=_time(3500000), ) - assert p.get_schedule(from_time=_time(50000), to_time=_time(250000)) == [ + assert p.get_schedule(from_time=_time(500000), to_time=_time(2500000)) == [ qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=100000), - end_time=Timestamp(seconds=200000), + start_time=Timestamp(seconds=1000000), + end_time=Timestamp(seconds=2000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=250000), - end_time=Timestamp(seconds=350000), + start_time=Timestamp(seconds=2500000), + end_time=Timestamp(seconds=3500000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, ), ] @@ -366,36 +366,36 @@ def test_create_reservation_add_at_end(): def test_create_reservation_border_conditions(): time_slot = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=100000), - end_time=Timestamp(seconds=200000), + start_time=Timestamp(seconds=1000000), + end_time=Timestamp(seconds=2000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ) p = NothingProcessor(processor_id='test', schedule=[time_slot]) p.create_reservation( - start_time=_time(190000), - end_time=_time(200000), + start_time=_time(1900000), + end_time=_time(2000000), ) p.create_reservation( - start_time=_time(100000), - end_time=_time(110000), + start_time=_time(1000000), + end_time=_time(1100000), ) - assert p.get_schedule(from_time=_time(0), to_time=_time(250000)) == [ + assert p.get_schedule(from_time=_time(200000), to_time=_time(2500000)) == [ qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=100000), - end_time=Timestamp(seconds=110000), + start_time=Timestamp(seconds=1000000), + end_time=Timestamp(seconds=1100000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=110000), - end_time=Timestamp(seconds=190000), + start_time=Timestamp(seconds=1100000), + end_time=Timestamp(seconds=1900000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=190000), - end_time=Timestamp(seconds=200000), + start_time=Timestamp(seconds=1900000), + end_time=Timestamp(seconds=2000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, ), ] @@ -404,44 +404,44 @@ def test_create_reservation_border_conditions(): def test_create_reservation_unbounded(): time_slot_begin = qtypes.QuantumTimeSlot( processor_name='test', - end_time=Timestamp(seconds=200000), + end_time=Timestamp(seconds=2000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ) time_slot_end = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=500000), + start_time=Timestamp(seconds=5000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ) p = NothingProcessor(processor_id='test', schedule=[time_slot_begin, time_slot_end]) p.create_reservation( - start_time=_time(100000), - end_time=_time(300000), + start_time=_time(1000000), + end_time=_time(3000000), ) p.create_reservation( - start_time=_time(400000), - end_time=_time(600000), + start_time=_time(4000000), + end_time=_time(6000000), ) - assert p.get_schedule(from_time=_time(0), to_time=_time(1000000)) == [ + assert p.get_schedule(from_time=_time(200000), to_time=_time(10000000)) == [ qtypes.QuantumTimeSlot( processor_name='test', - end_time=Timestamp(seconds=100000), + end_time=Timestamp(seconds=1000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=100000), - end_time=Timestamp(seconds=300000), + start_time=Timestamp(seconds=1000000), + end_time=Timestamp(seconds=3000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=400000), - end_time=Timestamp(seconds=600000), + start_time=Timestamp(seconds=4000000), + end_time=Timestamp(seconds=6000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=600000), + start_time=Timestamp(seconds=6000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), ] @@ -450,56 +450,56 @@ def test_create_reservation_unbounded(): def test_create_reservation_splitunbounded(): time_slot_begin = qtypes.QuantumTimeSlot( processor_name='test', - end_time=Timestamp(seconds=300000), + end_time=Timestamp(seconds=3000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ) time_slot_end = qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=500000), + start_time=Timestamp(seconds=5000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ) p = NothingProcessor(processor_id='test', schedule=[time_slot_begin, time_slot_end]) p.create_reservation( - start_time=_time(100000), - end_time=_time(200000), + start_time=_time(1000000), + end_time=_time(2000000), ) p.create_reservation( - start_time=_time(600000), - end_time=_time(700000), + start_time=_time(6000000), + end_time=_time(7000000), ) - assert p.get_schedule(from_time=_time(0), to_time=_time(1000000)) == [ + assert p.get_schedule(from_time=_time(200000), to_time=_time(10000000)) == [ qtypes.QuantumTimeSlot( processor_name='test', - end_time=Timestamp(seconds=100000), + end_time=Timestamp(seconds=1000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=100000), - end_time=Timestamp(seconds=200000), + start_time=Timestamp(seconds=1000000), + end_time=Timestamp(seconds=2000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=200000), - end_time=Timestamp(seconds=300000), + start_time=Timestamp(seconds=2000000), + end_time=Timestamp(seconds=3000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=500000), - end_time=Timestamp(seconds=600000), + start_time=Timestamp(seconds=5000000), + end_time=Timestamp(seconds=6000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=600000), - end_time=Timestamp(seconds=700000), + start_time=Timestamp(seconds=6000000), + end_time=Timestamp(seconds=7000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, ), qtypes.QuantumTimeSlot( processor_name='test', - start_time=Timestamp(seconds=700000), + start_time=Timestamp(seconds=7000000), slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, ), ] From fc9e7f60c3e83c3db69a6d40198cae93ec1c9de2 Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Fri, 12 Nov 2021 11:24:25 -0800 Subject: [PATCH 07/16] Address review comments on abstract interface PR. --- .../cirq_google/engine/abstract_engine.py | 64 ++++----- .../cirq_google/engine/abstract_job.py | 77 +++++------ .../cirq_google/engine/abstract_job_test.py | 99 ++++++++++++++ .../cirq_google/engine/abstract_processor.py | 124 ++++++++++-------- .../cirq_google/engine/abstract_program.py | 56 ++++---- 5 files changed, 271 insertions(+), 149 deletions(-) create mode 100644 cirq-google/cirq_google/engine/abstract_job_test.py diff --git a/cirq-google/cirq_google/engine/abstract_engine.py b/cirq-google/cirq_google/engine/abstract_engine.py index 42096dae0de..39bc748bfd4 100644 --- a/cirq-google/cirq_google/engine/abstract_engine.py +++ b/cirq-google/cirq_google/engine/abstract_engine.py @@ -11,18 +11,25 @@ # 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. -from abc import ABC, abstractmethod +"""Interface for Engine objects. + +This class is an abstract class which all Engine implementations +(production API or locally simulated) should follow. +""" +import abc import datetime from typing import Dict, List, Optional, Sequence, Set, Union import cirq -from cirq_google.engine import abstract_program, abstract_processor +from cirq_google.engine import abstract_job, abstract_program, abstract_processor from cirq_google.engine.client import quantum from cirq_google.serialization import Serializer +VALID_DATE_TYPE = Union[datetime.datetime, datetime.date] + -class AbstractEngine(ABC): - """An abstract object representing a collection of quantum procexsors. +class AbstractEngine(abc.ABC): + """An abstract object representing a collection of quantum processors. Each processor within the AbstractEngine can be referenced by a string identifier through the get_processor interface. @@ -34,22 +41,22 @@ class AbstractEngine(ABC): """ - @abstractmethod + @abc.abstractmethod def get_program(self, program_id: str) -> abstract_program.AbstractProgram: - """Returns an exsiting AbstractProgram given an identifier. + """Returns an existing AbstractProgram given an identifier. Args: - program_id: Unique ID of the program within the parent project. + program_id: Unique ID of the program. Returns: An AbstractProgram object for the program. """ - @abstractmethod + @abc.abstractmethod def list_programs( self, - created_before: Optional[Union[datetime.datetime, datetime.date]] = None, - created_after: Optional[Union[datetime.datetime, datetime.date]] = None, + created_before: Optional[VALID_DATE_TYPE] = None, + created_after: Optional[VALID_DATE_TYPE] = None, has_labels: Optional[Dict[str, str]] = None, ) -> List[abstract_program.AbstractProgram]: """Returns a list of previously executed quantum programs. @@ -60,25 +67,25 @@ def list_programs( created_before: retrieve programs that were created after this date or time. has_labels: retrieve programs that have labels on them specified by - this dict. If the value is set to `*`, filters having the label - regardless of the label value will be filtered. For example, to + this dict. If the value is set to `*`, programs having the label + regardless of the label value will be returned. For example, to query programs that have the shape label and have the color label with value red can be queried using - `{'color: red', 'shape:*'}` + `{'color': 'red', 'shape': '*'}` """ - @abstractmethod + @abc.abstractmethod def list_jobs( self, - created_before: Optional[Union[datetime.datetime, datetime.date]] = None, - created_after: Optional[Union[datetime.datetime, datetime.date]] = None, + created_before: Optional[VALID_DATE_TYPE] = None, + created_after: Optional[VALID_DATE_TYPE] = None, has_labels: Optional[Dict[str, str]] = None, execution_states: Optional[Set[quantum.enums.ExecutionStatus.State]] = None, - ): - """Returns the list of jobs in the project. + ) -> List[abstract_job.AbstractJob]: + """Returns the list of jobs that match the specified criteria. All historical jobs can be retrieved using this method and filtering - options are available too, to narrow down the search baesd on: + options are available too, to narrow down the search based on: * creation time * job labels * execution states @@ -89,8 +96,8 @@ def list_jobs( created_before: retrieve jobs that were created after this date or time. has_labels: retrieve jobs that have labels on them specified by - this dict. If the value is set to `*`, filters having the label - regardless of the label value will be filtered. For example, to + this dict. If the value is set to `*`, jobs having the label + regardless of the label value will be returned. For example, to query programs that have the shape label and have the color label with value red can be queried using @@ -101,18 +108,11 @@ def list_jobs( `quantum.enums.ExecutionStatus.State` enum for accepted values. """ - @abstractmethod + @abc.abstractmethod def list_processors(self) -> Sequence[abstract_processor.AbstractProcessor]: - """Returns a list of Processors that the user has visibility to in the - current Engine project. The names of these processors are used to - identify devices when scheduling jobs and gathering calibration metrics. - - Returns: - A list of EngineProcessors to access status, device and calibration - information. - """ + """Returns all processors in this engine visible to the user.""" - @abstractmethod + @abc.abstractmethod def get_processor(self, processor_id: str) -> abstract_processor.AbstractProcessor: """Returns an EngineProcessor for a Quantum Engine processor. @@ -123,7 +123,7 @@ def get_processor(self, processor_id: str) -> abstract_processor.AbstractProcess A EngineProcessor for the processor. """ - @abstractmethod + @abc.abstractmethod def get_sampler( self, processor_id: Union[str, List[str]], gate_set: Serializer ) -> cirq.Sampler: diff --git a/cirq-google/cirq_google/engine/abstract_job.py b/cirq-google/cirq_google/engine/abstract_job.py index 46204e2f13f..322e2ed2b5f 100644 --- a/cirq-google/cirq_google/engine/abstract_job.py +++ b/cirq-google/cirq_google/engine/abstract_job.py @@ -12,23 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. """A helper for jobs that have been created on the Quantum Engine.""" -from abc import ABC, abstractmethod -from typing import Dict, Iterator, List, Optional, overload, Tuple, TYPE_CHECKING -from cirq_google.engine.client import quantum -from cirq_google.engine.calibration_result import CalibrationResult +import abc +from typing import Dict, Iterator, List, Optional, overload, Tuple, TYPE_CHECKING import cirq +import cirq_google.engine.client.quantum as quantum + if TYPE_CHECKING: import datetime - from cirq_google.engine.calibration import Calibration - from cirq_google.engine.abstract_engine import AbstractEngine - from cirq_google.engine.abstract_processor import AbstractProcessor - from cirq_google.engine.abstract_program import AbstractProgram + import cirq_google.engine.calibration as calibration + import cirq_google.engine.calibration_result as calibration_result + import cirq_google.engine.abstract_engine as abstract_engine + import cirq_google.engine.abstract_processor as abstract_processor + import cirq_google.engine.abstract_program as abstract_program -class AbstractJob(ABC): +class AbstractJob(abc.ABC): """An abstract object representing a quantum job execution. This represents the state of a possibly asynchronous Job being @@ -50,31 +51,31 @@ class AbstractJob(ABC): """ - @abstractmethod - def engine(self) -> 'AbstractEngine': + @abc.abstractmethod + def engine(self) -> 'abstract_engine.AbstractEngine': """Returns the parent `AbstractEngine` object.""" - @abstractmethod + @abc.abstractmethod def id(self) -> str: """Returns the id of this job.""" - @abstractmethod - def program(self) -> 'AbstractProgram': + @abc.abstractmethod + def program(self) -> 'abstract_program.AbstractProgram': """Returns the parent `AbstractProgram`object.""" - @abstractmethod + @abc.abstractmethod def create_time(self) -> 'datetime.datetime': """Returns when the job was created.""" - @abstractmethod + @abc.abstractmethod def update_time(self) -> 'datetime.datetime': """Returns when the job was last updated.""" - @abstractmethod + @abc.abstractmethod def description(self) -> str: """Returns the description of the job.""" - @abstractmethod + @abc.abstractmethod def set_description(self, description: str) -> 'AbstractJob': """Sets the description of the job. @@ -85,11 +86,11 @@ def set_description(self, description: str) -> 'AbstractJob': This `AbstractJob`. """ - @abstractmethod + @abc.abstractmethod def labels(self) -> Dict[str, str]: """Returns the labels of the job.""" - @abstractmethod + @abc.abstractmethod def set_labels(self, labels: Dict[str, str]) -> 'AbstractJob': """Sets (overwriting) the labels for a previously created quantum job. @@ -100,7 +101,7 @@ def set_labels(self, labels: Dict[str, str]) -> 'AbstractJob': This `AbstractJob`. """ - @abstractmethod + @abc.abstractmethod def add_labels(self, labels: Dict[str, str]) -> 'AbstractJob': """Adds new labels to a previously created quantum job. @@ -111,7 +112,7 @@ def add_labels(self, labels: Dict[str, str]) -> 'AbstractJob': This `AbstractJob`. """ - @abstractmethod + @abc.abstractmethod def remove_labels(self, keys: List[str]) -> 'AbstractJob': """Removes labels with given keys. @@ -122,19 +123,19 @@ def remove_labels(self, keys: List[str]) -> 'AbstractJob': This `AbstractJob`. """ - @abstractmethod + @abc.abstractmethod def processor_ids(self) -> List[str]: """Returns the processor ids provided when the job was created.""" - @abstractmethod + @abc.abstractmethod def execution_status(self) -> quantum.enums.ExecutionStatus.State: """Return the execution status of the job.""" - @abstractmethod + @abc.abstractmethod def failure(self) -> Optional[Tuple[str, str]]: """Return failure code and message of the job if present.""" - @abstractmethod + @abc.abstractmethod def get_repetitions_and_sweeps(self) -> Tuple[int, List[cirq.Sweep]]: """Returns the repetitions and sweeps for the job. @@ -142,25 +143,25 @@ def get_repetitions_and_sweeps(self) -> Tuple[int, List[cirq.Sweep]]: A tuple of the repetition count and list of sweeps. """ - @abstractmethod - def get_processor(self) -> 'Optional[AbstractProcessor]': + @abc.abstractmethod + def get_processor(self) -> Optional['abstract_processor.AbstractProcessor']: """Returns the AbstractProcessor for the processor the job is/was run on, if available, else None.""" - @abstractmethod - def get_calibration(self) -> Optional['Calibration']: + @abc.abstractmethod + def get_calibration(self) -> Optional['calibration.Calibration']: """Returns the recorded calibration at the time when the job was run, if one was captured, else None.""" - @abstractmethod - def cancel(self) -> None: + @abc.abstractmethod + def cancel(self) -> Optional[bool]: """Cancel the job.""" - @abstractmethod - def delete(self) -> None: + @abc.abstractmethod + def delete(self) -> Optional[bool]: """Deletes the job and result, if any.""" - @abstractmethod + @abc.abstractmethod def batched_results(self) -> List[List[cirq.Result]]: """Returns the job results, blocking until the job is complete. @@ -169,12 +170,12 @@ def batched_results(self) -> List[List[cirq.Result]]: for each circuit in the batch. """ - @abstractmethod + @abc.abstractmethod def results(self) -> List[cirq.Result]: """Returns the job results, blocking until the job is complete.""" - @abstractmethod - def calibration_results(self) -> List[CalibrationResult]: + @abc.abstractmethod + def calibration_results(self) -> List['calibration_result.CalibrationResult']: """Returns the results of a run_calibration() call. This function will fail if any other type of results were returned. diff --git a/cirq-google/cirq_google/engine/abstract_job_test.py b/cirq-google/cirq_google/engine/abstract_job_test.py new file mode 100644 index 00000000000..fe723d6a655 --- /dev/null +++ b/cirq-google/cirq_google/engine/abstract_job_test.py @@ -0,0 +1,99 @@ +# Copyright 2021 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. +from typing import Dict, List, TYPE_CHECKING +from cirq_google.engine.abstract_job import AbstractJob + +if TYPE_CHECKING: + import datetime + import cirq_google.engine.abstract_engine as abstract_engine + import cirq_google.engine.abstract_processor as abstract_processor + import cirq_google.engine.abstract_program as abstract_program + + +class MockJob(AbstractJob): + def engine(self) -> 'abstract_engine.AbstractEngine': + pass + + def id(self) -> str: + pass + + def program(self) -> 'abstract_program.AbstractProgram': + pass + + def create_time(self) -> 'datetime.datetime': + pass + + def update_time(self) -> 'datetime.datetime': + pass + + def description(self) -> str: + pass + + def set_description(self, description: str) -> 'AbstractJob': + pass + + def labels(self) -> Dict[str, str]: + pass + + def set_labels(self, labels: Dict[str, str]) -> 'AbstractJob': + pass + + def add_labels(self, labels: Dict[str, str]) -> 'AbstractJob': + pass + + def remove_labels(self, keys: List[str]) -> 'AbstractJob': + pass + + def processor_ids(self): + pass + + def execution_status(self): + pass + + def failure(self): + pass + + def get_repetitions_and_sweeps(self): + pass + + def get_processor(self): + pass + + def get_calibration(self): + pass + + def cancel(self) -> None: + pass + + def delete(self) -> None: + pass + + def batched_results(self): + pass + + def results(self): + return list(range(5)) + + def calibration_results(self): + pass + + +def test_instantiation_and_iteration(): + job = MockJob() + assert len(job) == 5 + assert job[3] == 3 + count = 0 + for num in job: + assert num == count + count += 1 diff --git a/cirq-google/cirq_google/engine/abstract_processor.py b/cirq-google/cirq_google/engine/abstract_processor.py index 7e814133498..6ebad0fbba2 100644 --- a/cirq-google/cirq_google/engine/abstract_processor.py +++ b/cirq-google/cirq_google/engine/abstract_processor.py @@ -11,26 +11,32 @@ # 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. -from abc import ABC, abstractmethod +"""Abstract interface for a quantum processor. + +This interface can run circuits, sweeps, batches, or calibration +requests. Inheritors of this interface should implement all +methods. +""" + +import abc import datetime from typing import Dict, Iterable, List, Optional, Sequence, TYPE_CHECKING, Union import cirq -from cirq_google.engine.client.quantum import types as qtypes -from cirq_google.engine.client.quantum import enums as qenums -from cirq_google.api import v2 -from cirq_google.engine import calibration +import cirq_google.api.v2 as v2 +import cirq_google.engine.calibration as calibration +import cirq_google.engine.client.quantum as quantum if TYPE_CHECKING: import cirq_google - from cirq_google.engine.abstract_engine import AbstractEngine - from cirq_google.engine.abstract_job import AbstractJob - from cirq_google.engine.abstract_program import AbstractProgram - from cirq_google.serialization.serializer import Serializer + import cirq_google.engine.abstract_engine as abstract_engine + import cirq_google.engine.abstract_job as abstract_job + import cirq_google.engine.abstract_program as abstract_program + import cirq_google.serialization.serializer as serializer -class AbstractProcessor(ABC): +class AbstractProcessor(abc.ABC): """An abstract interface for a quantum processor. This quantum processor has the ability to execute single circuits @@ -52,9 +58,9 @@ def run( program: cirq.Circuit, program_id: Optional[str] = None, job_id: Optional[str] = None, - param_resolver: cirq.ParamResolver = cirq.ParamResolver({}), + param_resolver: cirq.ParamResolver = None, repetitions: int = 1, - gate_set: Optional['Serializer'] = None, + gate_set: Optional['serializer.Serializer'] = None, program_description: Optional[str] = None, program_labels: Optional[Dict[str, str]] = None, job_description: Optional[str] = None, @@ -86,7 +92,7 @@ def run( A single Result for this run. """ - @abstractmethod + @abc.abstractmethod def run_sweep( self, program: cirq.Circuit, @@ -94,12 +100,12 @@ def run_sweep( job_id: Optional[str] = None, params: cirq.Sweepable = None, repetitions: int = 1, - gate_set: Optional['Serializer'] = None, + gate_set: Optional['serializer.Serializer'] = None, program_description: Optional[str] = None, program_labels: Optional[Dict[str, str]] = None, job_description: Optional[str] = None, job_labels: Optional[Dict[str, str]] = None, - ) -> 'AbstractJob': + ) -> 'abstract_job.AbstractJob': """Runs the supplied Circuit on this processor. In contrast to run, this runs across multiple parameter sweeps, and @@ -129,7 +135,7 @@ def run_sweep( TrialResults, one for each parameter sweep. """ - @abstractmethod + @abc.abstractmethod def run_batch( self, programs: Sequence[cirq.AbstractCircuit], @@ -137,12 +143,12 @@ def run_batch( job_id: Optional[str] = None, params_list: List[cirq.Sweepable] = None, repetitions: int = 1, - gate_set: Optional['Serializer'] = None, + gate_set: Optional['serializer.Serializer'] = None, program_description: Optional[str] = None, program_labels: Optional[Dict[str, str]] = None, job_description: Optional[str] = None, job_labels: Optional[Dict[str, str]] = None, - ) -> 'AbstractJob': + ) -> 'abstract_job.AbstractJob': """Runs the supplied Circuits on this processor. This will combine each Circuit provided in `programs` into @@ -183,18 +189,18 @@ def run_batch( parameter sweep. """ - @abstractmethod + @abc.abstractmethod def run_calibration( self, layers: List['cirq_google.CalibrationLayer'], program_id: Optional[str] = None, job_id: Optional[str] = None, - gate_set: Optional['Serializer'] = None, + gate_set: Optional['serializer.Serializer'] = None, program_description: Optional[str] = None, program_labels: Optional[Dict[str, str]] = None, job_description: Optional[str] = None, job_labels: Optional[Dict[str, str]] = None, - ) -> 'AbstractJob': + ) -> 'abstract_job.AbstractJob': """Runs the specified calibrations on the processor. Each calibration will be specified by a `CalibrationLayer` @@ -229,40 +235,40 @@ def run_calibration( calibration_results(). """ - @abstractmethod - def get_sampler(self, gate_set: Optional['Serializer']) -> cirq.Sampler: + @abc.abstractmethod + def get_sampler(self, gate_set: Optional['serializer.Serializer']) -> cirq.Sampler: """Returns a sampler backed by the processor. Args: gate_set: Determines how to serialize circuits if needed. """ - @abstractmethod - def engine(self) -> Optional['AbstractEngine']: + @abc.abstractmethod + def engine(self) -> Optional['abstract_engine.AbstractEngine']: """Returns the parent Engine object. Returns: The program's parent Engine. """ - @abstractmethod + @abc.abstractmethod def health(self) -> str: """Returns the current health of processor.""" - @abstractmethod + @abc.abstractmethod def expected_down_time(self) -> 'Optional[datetime.datetime]': """Returns the start of the next expected down time of the processor, if set.""" - @abstractmethod + @abc.abstractmethod def expected_recovery_time(self) -> 'Optional[datetime.datetime]': """Returns the expected the processor should be available, if set.""" - @abstractmethod + @abc.abstractmethod def supported_languages(self) -> List[str]: """Returns the list of processor supported program languages.""" - @abstractmethod + @abc.abstractmethod def get_device_specification(self) -> Optional[v2.device_pb2.DeviceSpecification]: """Returns a device specification proto for use in determining information about the device. @@ -271,38 +277,44 @@ def get_device_specification(self) -> Optional[v2.device_pb2.DeviceSpecification Device specification proto if present. """ - @abstractmethod - def get_device(self, gate_sets: Iterable['Serializer']) -> cirq.Device: + @abc.abstractmethod + def get_device(self, gate_sets: Iterable['serializer.Serializer']) -> cirq.Device: """Returns a `Device` created from the processor's device specification. This method queries the processor to retrieve the device specification, - which is then use to create a `SerializableDevice` that will validate + which is then use to create a `Device` that will validate that operations are supported and use the correct qubits. + + Args: + gate_sets: An iterable of serializers that can be used in the device. + + Returns: + A `cirq.Devive` representing the processor. """ - @abstractmethod + @abc.abstractmethod def list_calibrations( self, - earliest_timestamp_seconds: Optional[int] = None, - latest_timestamp_seconds: Optional[int] = None, + earliest_timestamp: Optional[Union[datetime.datetime, datetime.date, int]] = None, + latest_timestamp: Optional[Union[datetime.datetime, datetime.date, int]] = None, ) -> List[calibration.Calibration]: """Retrieve metadata about a specific calibration run. - Params: - earliest_timestamp_seconds: The earliest timestamp of a calibration + Args: + earliest_timestamp: The earliest timestamp of a calibration to return in UTC. - latest_timestamp_seconds: The latest timestamp of a calibration to + latest_timestamp: The latest timestamp of a calibration to return in UTC. Returns: The list of calibration data with the most recent first. """ - @abstractmethod + @abc.abstractmethod def get_calibration(self, calibration_timestamp_seconds: int) -> calibration.Calibration: """Retrieve metadata about a specific calibration run. - Params: + Args: calibration_timestamp_seconds: The timestamp of the calibration in seconds since epoch. @@ -310,7 +322,7 @@ def get_calibration(self, calibration_timestamp_seconds: int) -> calibration.Cal The calibration data. """ - @abstractmethod + @abc.abstractmethod def get_current_calibration( self, ) -> Optional[calibration.Calibration]: @@ -320,13 +332,13 @@ def get_current_calibration( The calibration data or None if there is no current calibration. """ - @abstractmethod + @abc.abstractmethod def create_reservation( self, start_time: datetime.datetime, end_time: datetime.datetime, whitelisted_users: Optional[List[str]] = None, - ) -> qtypes.QuantumReservation: + ) -> quantum.types.QuantumReservation: """Creates a reservation on this processor. Args: @@ -337,15 +349,15 @@ def create_reservation( with permission "quantum.reservations.use" on the project). """ - @abstractmethod - def remove_reservation(self, reservation_id: str): + @abc.abstractmethod + def remove_reservation(self, reservation_id: str) -> None: """Removes a reservation on this processor.""" - @abstractmethod - def get_reservation(self, reservation_id: str) -> qtypes.QuantumReservation: + @abc.abstractmethod + def get_reservation(self, reservation_id: str) -> quantum.types.QuantumReservation: """Retrieve a reservation given its id.""" - @abstractmethod + @abc.abstractmethod def update_reservation( self, reservation_id: str, @@ -360,12 +372,12 @@ def update_reservation( None, it will not be updated. """ - @abstractmethod + @abc.abstractmethod def list_reservations( self, - from_time: Union[None, datetime.datetime, datetime.timedelta] = datetime.timedelta(), - to_time: Union[None, datetime.datetime, datetime.timedelta] = datetime.timedelta(weeks=2), - ) -> List[qtypes.QuantumReservation]: + from_time: Union[None, datetime.datetime, datetime.timedelta], + to_time: Union[None, datetime.datetime, datetime.timedelta], + ) -> List[quantum.types.QuantumReservation]: """Retrieves the reservations from a processor. Only reservations from this processor and project will be @@ -387,13 +399,13 @@ def list_reservations( A list of reservations. """ - @abstractmethod + @abc.abstractmethod def get_schedule( self, from_time: Union[None, datetime.datetime, datetime.timedelta] = datetime.timedelta(), to_time: Union[None, datetime.datetime, datetime.timedelta] = datetime.timedelta(weeks=2), - time_slot_type: Optional[qenums.QuantumTimeSlot.TimeSlotType] = None, - ) -> List[qenums.QuantumTimeSlot]: + time_slot_type: Optional[quantum.enums.QuantumTimeSlot.TimeSlotType] = None, + ) -> List[quantum.enums.QuantumTimeSlot]: """Retrieves the schedule for a processor. The schedule may be filtered by time. diff --git a/cirq-google/cirq_google/engine/abstract_program.py b/cirq-google/cirq_google/engine/abstract_program.py index 50e8e4e99b4..7d5b0241f64 100644 --- a/cirq-google/cirq_google/engine/abstract_program.py +++ b/cirq-google/cirq_google/engine/abstract_program.py @@ -11,18 +11,25 @@ # 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. -from abc import ABC, abstractmethod +"""An interface for quantum programs. + +The quantum program represents a circuit (or other execution) that, +when combined with a run context, will become a quantum job. +""" + + +import abc import datetime from typing import Dict, List, Optional, Sequence, Set, TYPE_CHECKING, Union import cirq -from cirq_google.engine.client import quantum +import cirq_google.engine.client.quantum as quantum if TYPE_CHECKING: - from cirq_google.engine.abstract_job import AbstractJob - from cirq_google.engine.abstract_engine import AbstractEngine + import cirq_google.engine.abstract_job as abstract_job + import cirq_google.engine.abstract_engine as abstract_engine -class AbstractProgram(ABC): +class AbstractProgram(abc.ABC): """An abstract object representing a quantum program. This program generally wraps a `Circuit` with additional metadata. @@ -34,16 +41,16 @@ class AbstractProgram(ABC): This is an abstract class that inheritors should implement. """ - @abstractmethod - def engine(self) -> 'AbstractEngine': + @abc.abstractmethod + def engine(self) -> 'abstract_engine.AbstractEngine': """Returns the parent Engine object. Returns: The program's parent Engine. """ - @abstractmethod - def get_job(self, job_id: str) -> 'AbstractJob': + @abc.abstractmethod + def get_job(self, job_id: str) -> 'abstract_job.AbstractJob': """Returns an AbstractJob for an existing id. Args: @@ -53,14 +60,14 @@ def get_job(self, job_id: str) -> 'AbstractJob': A AbstractJob for this program. """ - @abstractmethod + @abc.abstractmethod def list_jobs( self, created_before: Optional[Union[datetime.datetime, datetime.date]] = None, created_after: Optional[Union[datetime.datetime, datetime.date]] = None, has_labels: Optional[Dict[str, str]] = None, execution_states: Optional[Set[quantum.enums.ExecutionStatus.State]] = None, - ) -> Sequence['AbstractJob']: + ) -> Sequence['abstract_job.AbstractJob']: """Returns the list of jobs for this program. Args: @@ -81,21 +88,24 @@ def list_jobs( execution_states: retrieve jobs that have an execution state that is contained in `execution_states`. See `quantum.enums.ExecutionStatus.State` enum for accepted values. + + Returns: + A sequence of `AbstractJob` objects that satisfy the constraints. """ - @abstractmethod + @abc.abstractmethod def create_time(self) -> 'datetime.datetime': """Returns when the program was created.""" - @abstractmethod + @abc.abstractmethod def update_time(self) -> 'datetime.datetime': """Returns when the program was last updated.""" - @abstractmethod + @abc.abstractmethod def description(self) -> str: """Returns the description of the program.""" - @abstractmethod + @abc.abstractmethod def set_description(self, description: str) -> 'AbstractProgram': """Sets the description of the program. @@ -106,11 +116,11 @@ def set_description(self, description: str) -> 'AbstractProgram': This AbstractProgram. """ - @abstractmethod + @abc.abstractmethod def labels(self) -> Dict[str, str]: """Returns the labels of the program.""" - @abstractmethod + @abc.abstractmethod def set_labels(self, labels: Dict[str, str]) -> 'AbstractProgram': """Sets (overwriting) the labels for a previously created quantum program. @@ -121,7 +131,7 @@ def set_labels(self, labels: Dict[str, str]) -> 'AbstractProgram': This AbstractProgram. """ - @abstractmethod + @abc.abstractmethod def add_labels(self, labels: Dict[str, str]) -> 'AbstractProgram': """Adds new labels to a previously created quantum program. @@ -132,7 +142,7 @@ def add_labels(self, labels: Dict[str, str]) -> 'AbstractProgram': This AbstractProgram. """ - @abstractmethod + @abc.abstractmethod def remove_labels(self, keys: List[str]) -> 'AbstractProgram': """Removes labels with given keys from the labels of a previously created quantum program. @@ -144,7 +154,7 @@ def remove_labels(self, keys: List[str]) -> 'AbstractProgram': This AbstractProgram. """ - @abstractmethod + @abc.abstractmethod def get_circuit(self, program_num: Optional[int] = None) -> cirq.Circuit: """Returns the cirq Circuit for the program. This is only supported if the program was created with the V2 protos. @@ -158,7 +168,7 @@ def get_circuit(self, program_num: Optional[int] = None) -> cirq.Circuit: The program's cirq Circuit. """ - @abstractmethod + @abc.abstractmethod def batch_size(self) -> int: """Returns the number of programs in a batch program. @@ -166,7 +176,7 @@ def batch_size(self) -> int: ValueError: if the program created was not a batch program. """ - @abstractmethod + @abc.abstractmethod def delete(self, delete_jobs: bool = False) -> None: """Deletes a previously created quantum program. @@ -175,6 +185,6 @@ def delete(self, delete_jobs: bool = False) -> None: will fail if the program contains any jobs. """ - @abstractmethod + @abc.abstractmethod def delete_job(self, job_id: str) -> None: """Removes a child job from this program.""" From d7770db77ec38883c54eec424da220e77bda25d6 Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Fri, 12 Nov 2021 11:29:52 -0800 Subject: [PATCH 08/16] Resolve Dave comments --- cirq-google/cirq_google/engine/abstract_engine.py | 4 ++-- cirq-google/cirq_google/engine/abstract_processor.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cirq-google/cirq_google/engine/abstract_engine.py b/cirq-google/cirq_google/engine/abstract_engine.py index 39bc748bfd4..c2a56efb498 100644 --- a/cirq-google/cirq_google/engine/abstract_engine.py +++ b/cirq-google/cirq_google/engine/abstract_engine.py @@ -64,7 +64,7 @@ def list_programs( Args: created_after: retrieve programs that were created after this date or time. - created_before: retrieve programs that were created after this date + created_before: retrieve programs that were created before this date or time. has_labels: retrieve programs that have labels on them specified by this dict. If the value is set to `*`, programs having the label @@ -93,7 +93,7 @@ def list_jobs( Args: created_after: retrieve jobs that were created after this date or time. - created_before: retrieve jobs that were created after this date + created_before: retrieve jobs that were created before this date or time. has_labels: retrieve jobs that have labels on them specified by this dict. If the value is set to `*`, jobs having the label diff --git a/cirq-google/cirq_google/engine/abstract_processor.py b/cirq-google/cirq_google/engine/abstract_processor.py index 6ebad0fbba2..04d0314084c 100644 --- a/cirq-google/cirq_google/engine/abstract_processor.py +++ b/cirq-google/cirq_google/engine/abstract_processor.py @@ -132,7 +132,7 @@ def run_sweep( job_labels: Optional set of labels to set on the job. Returns: An AbstractJob. If this is iterated over it returns a list of - TrialResults, one for each parameter sweep. + `cirq.Result`, one for each parameter sweep. """ @abc.abstractmethod @@ -183,8 +183,8 @@ def run_batch( job_labels: Optional set of labels to set on the job. Returns: An AbstractJob. If this is iterated over it returns a list of - TrialResults. All TrialResults for the first circuit are listed - first, then the TrialResults for the second, etc. The TrialResults + `cirq.Result`. All Results for the first circuit are listed + first, then the Results for the second, etc. The Results for a circuit are listed in the order imposed by the associated parameter sweep. """ From 0ae56bfcbf7863b97e2ae6a5e86dcf6028db9a28 Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Sun, 14 Nov 2021 06:14:35 -0800 Subject: [PATCH 09/16] Filter docstring cleanup. --- cirq-google/cirq_google/engine/abstract_program.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cirq-google/cirq_google/engine/abstract_program.py b/cirq-google/cirq_google/engine/abstract_program.py index 7d5b0241f64..19249ed5d21 100644 --- a/cirq-google/cirq_google/engine/abstract_program.py +++ b/cirq-google/cirq_google/engine/abstract_program.py @@ -78,8 +78,8 @@ def list_jobs( created_before: retrieve jobs that were created after this date or time. has_labels: retrieve jobs that have labels on them specified by - this dict. If the value is set to `*`, filters having the label - regardless of the label value will be filtered. For example, to + this dict. If the value is set to `*`, jobs having the label + regardless of the label value will be returned. For example, to query programs that have the shape label and have the color label with value red can be queried using From 93ce2fbe2c460adb00c624773d568de4093a59b1 Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Mon, 15 Nov 2021 05:22:09 -0800 Subject: [PATCH 10/16] Address review comments and fix coverage --- .../engine/abstract_local_engine.py | 2 +- .../engine/abstract_local_engine_test.py | 23 ++++++++-- .../cirq_google/engine/abstract_local_job.py | 4 ++ .../engine/abstract_local_job_test.py | 15 +------ .../engine/abstract_local_processor.py | 44 ++++++++++++------- .../engine/abstract_local_processor_test.py | 22 +++++----- .../engine/abstract_local_program.py | 4 +- 7 files changed, 67 insertions(+), 47 deletions(-) diff --git a/cirq-google/cirq_google/engine/abstract_local_engine.py b/cirq-google/cirq_google/engine/abstract_local_engine.py index b42b842966c..45c278f90fb 100644 --- a/cirq-google/cirq_google/engine/abstract_local_engine.py +++ b/cirq-google/cirq_google/engine/abstract_local_engine.py @@ -45,7 +45,7 @@ def __init__(self, processors: List[AbstractLocalProcessor]): def get_program(self, program_id: str) -> AbstractProgram: """Returns an exsiting AbstractProgram given an identifier. - Iteratibvely checks each processor for the given id. + Iteratively checks each processor for the given id. Args: program_id: Unique ID of the program within the parent project. diff --git a/cirq-google/cirq_google/engine/abstract_local_engine_test.py b/cirq-google/cirq_google/engine/abstract_local_engine_test.py index 76927107580..6dffdd258d8 100644 --- a/cirq-google/cirq_google/engine/abstract_local_engine_test.py +++ b/cirq-google/cirq_google/engine/abstract_local_engine_test.py @@ -22,6 +22,7 @@ from cirq_google.engine.abstract_local_engine import AbstractLocalEngine from cirq_google.engine.abstract_local_processor import AbstractLocalProcessor from cirq_google.engine.abstract_program import AbstractProgram +import cirq_google.engine.calibration as calibration class ProgramDictProcessor(AbstractLocalProcessor): @@ -34,8 +35,8 @@ def __init__(self, programs: Dict[str, AbstractProgram], **kwargs): def get_calibration(self, *args, **kwargs): pass - def get_latest_calibration(self, *args, **kwargs): - pass + def get_latest_calibration(self, timestamp: int) -> Optional[calibration.Calibration]: + return calibration.Calibration() def get_current_calibration(self, *args, **kwargs): pass @@ -98,6 +99,9 @@ def test_get_processor(): assert engine.get_processor('test') == processor1 assert engine.get_processor('test').engine() == engine + with pytest.raises(KeyError): + _ = engine.get_processor('invalid') + def test_list_processor(): processor1 = ProgramDictProcessor(programs=[], processor_id='proc') @@ -113,14 +117,14 @@ def test_list_processor(): def test_get_programs(): program1 = NothingProgram([cirq.Circuit()], None) job1 = NothingJob( - job_id='test3', processor_id='test1', parent_program=program1, repetitions=100, sweeps=[] + job_id='test3', processor_id='proc', parent_program=program1, repetitions=100, sweeps=[] ) program1.add_job('jerb', job1) job1.add_labels({'color': 'blue'}) program2 = NothingProgram([cirq.Circuit()], None) job2 = NothingJob( - job_id='test4', processor_id='test2', parent_program=program2, repetitions=100, sweeps=[] + job_id='test4', processor_id='crop', parent_program=program2, repetitions=100, sweeps=[] ) program2.add_job('jerb2', job2) job2.add_labels({'color': 'red'}) @@ -139,6 +143,17 @@ def test_get_programs(): assert engine.list_jobs(has_labels={'color': 'blue'}) == [job1] assert engine.list_jobs(has_labels={'color': 'red'}) == [job2] + program3 = NothingProgram([cirq.Circuit()], engine) + assert program3.engine() == engine + + job3 = NothingJob( + job_id='test5', processor_id='crop', parent_program=program3, repetitions=100, sweeps=[] + ) + assert job3.program() == program3 + assert job3.engine() == engine + assert job3.get_processor() == processor2 + assert job3.get_calibration() == calibration.Calibration() + def test_get_sampler(): processor = ProgramDictProcessor(programs={}, processor_id='grocery') diff --git a/cirq-google/cirq_google/engine/abstract_local_job.py b/cirq-google/cirq_google/engine/abstract_local_job.py index 6fc8788e093..ffbf868bdd8 100644 --- a/cirq-google/cirq_google/engine/abstract_local_job.py +++ b/cirq-google/cirq_google/engine/abstract_local_job.py @@ -101,6 +101,7 @@ def set_description(self, description: str) -> 'AbstractJob': This AbstractJob. """ self._description = description + self._update_time = datetime.datetime.now() return self def labels(self) -> Dict[str, str]: @@ -117,6 +118,7 @@ def set_labels(self, labels: Dict[str, str]) -> 'AbstractJob': This AbstractJob. """ self._labels = copy.copy(labels) + self._update_time = datetime.datetime.now() return self def add_labels(self, labels: Dict[str, str]) -> 'AbstractJob': @@ -128,6 +130,7 @@ def add_labels(self, labels: Dict[str, str]) -> 'AbstractJob': Returns: This AbstractJob. """ + self._update_time = datetime.datetime.now() for key in labels: self._labels[key] = labels[key] return self @@ -142,6 +145,7 @@ def remove_labels(self, keys: List[str]) -> 'AbstractJob': Returns: This AbstractJob. """ + self._update_time = datetime.datetime.now() for key in keys: del self._labels[key] return self 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 d0a1d953cc3..7b7877515ad 100644 --- a/cirq-google/cirq_google/engine/abstract_local_job_test.py +++ b/cirq-google/cirq_google/engine/abstract_local_job_test.py @@ -54,6 +54,7 @@ def test_description_and_labels(): job = NothingJob( job_id='test', processor_id='pot_of_gold', parent_program=None, repetitions=100, sweeps=[] ) + assert job.id() == 'test' assert job.processor_ids() == ['pot_of_gold'] assert not job.description() job.set_description('nothing much') @@ -71,7 +72,7 @@ def test_description_and_labels(): assert job.labels() == {'walls': 'gray'} -def test_reps_and_Sweeps(): +def test_reps_and_sweeps(): job = NothingJob( job_id='test', processor_id='grill', @@ -92,15 +93,3 @@ def test_create_update_time(): job._update_time = update_time assert job.create_time() == create_time assert job.update_time() == update_time - - -def test_engineand_processor_calibration(): - pass - - -def test_cwprogramengine(): - pass - - -def test_abstract_functions(): - pass diff --git a/cirq-google/cirq_google/engine/abstract_local_processor.py b/cirq-google/cirq_google/engine/abstract_local_processor.py index 3553a7f3321..c55a744df59 100644 --- a/cirq-google/cirq_google/engine/abstract_local_processor.py +++ b/cirq-google/cirq_google/engine/abstract_local_processor.py @@ -49,7 +49,7 @@ class AbstractLocalProcessor(AbstractProcessor): processor_id: Unique string id of the processor. engine: The parent `AbstractEngine` object, if available. expected_down_time: Optional datetime of the next expected downtime. - For informational purpose only.yy + For informational purpose only. expected_recovery_time: Optional datetime when the processor is expected to be available again. For informational purpose only. schedule: List of time slots that the scheduling/reservation should @@ -71,7 +71,7 @@ def __init__( self._expected_recovery_time = expected_recovery_time self._expected_down_time = expected_down_time self._reservations: Dict[str, qtypes.QuantumReservation] = {} - self._reservation_id_counter = 0 + self._resource_id_counter = 0 self._processor_id = processor_id self._project_name = project_name @@ -121,11 +121,11 @@ def expected_recovery_time(self) -> 'Optional[datetime.datetime]': def _create_id(self, id_type: str = 'reservation') -> str: """Creates a unique resource id for child objects.""" - self._reservation_id_counter += 1 + self._resource_id_counter += 1 return ( f'projects/{self._project_name}/' f'processors/{self._processor_id}/' - f'{id_type}/{self._reservation_id_counter}' + f'{id_type}/{self._resource_id_counter}' ) def _reservation_to_time_slot( @@ -173,8 +173,8 @@ def _insert_reservation_into(self, time_slot: qtypes.QuantumTimeSlot) -> None: # t should be removed else: if not t.end_time.seconds or t.end_time.seconds > time_slot.end_time.seconds: - # [-------------t---------] # [---time_slot---] + # [-------------t---------] # t should be split start = qtypes.QuantumTimeSlot( processor_name=self._processor_id, @@ -196,8 +196,8 @@ def _insert_reservation_into(self, time_slot: qtypes.QuantumTimeSlot) -> None: new_schedule.append(end) else: - # [----t-----] # [---time_slot---] + # [----t-----] t.end_time.seconds = time_slot.start_time.seconds new_schedule.append(t) new_schedule.append(time_slot) @@ -254,12 +254,12 @@ def create_reservation( self._insert_reservation_into(time_slot) return new_reservation - def remove_reservation(self, reservation_id: str): + def remove_reservation(self, reservation_id: str) -> None: """Removes a reservation on this processor.""" if reservation_id in self._reservations: del self._reservations[reservation_id] - def get_reservation(self, reservation_id: str): + def get_reservation(self, reservation_id: str) -> qtypes.QuantumReservation: """Retrieve a reservation given its id.""" if reservation_id in self._reservations: return self._reservations[reservation_id] @@ -269,15 +269,27 @@ def get_reservation(self, reservation_id: str): def update_reservation( self, reservation_id: str, - start_time: datetime.datetime = None, - end_time: datetime.datetime = None, - whitelisted_users: List[str] = None, - ): + start_time: Optional[datetime.datetime] = None, + end_time: Optional[datetime.datetime] = None, + whitelisted_users: Optional[List[str]] = None, + ) -> None: """Updates a reservation with new information. Updates a reservation with a new start date, end date, or list of additional users. For each field, it the argument is left as None, it will not be updated. + + Args: + reservation_id: The string identifier of the reservation to change. + start_time: New starting time of the reservation. If unspecified, + starting time is left unchanged. + end_time: New starting time of the reservation. If unspecified, + ending time is left unchanged. + whitelisted_users: The new list of whitelisted users to allow on + the reservation. If unspecified, the users are left unchanged. + + Raises: + ValueError: if reservation_id does not exist. """ if reservation_id not in self._reservations: raise ValueError(f'Reservation id {reservation_id} does not exist.') @@ -293,7 +305,7 @@ def list_reservations( self, from_time: Union[None, datetime.datetime, datetime.timedelta] = datetime.timedelta(), to_time: Union[None, datetime.datetime, datetime.timedelta] = datetime.timedelta(weeks=2), - ): + ) -> List[qtypes.QuantumReservation]: """Retrieves the reservations from a processor. Only reservations from this processor and project will be @@ -318,9 +330,9 @@ def list_reservations( end_timestamp = _to_timestamp(to_time) reservation_list = [] for reservation in self._reservations.values(): - if start_timestamp and reservation.start_time.seconds < start_timestamp: + if end_timestamp and reservation.start_time.seconds > end_timestamp: continue - if end_timestamp and reservation.end_time.seconds > end_timestamp: + if start_timestamp and reservation.end_time.seconds < start_timestamp: continue reservation_list.append(reservation) return reservation_list @@ -399,7 +411,7 @@ def list_programs( Args: created_after: retrieve programs that were created after this date or time. - created_before: retrieve programs that were created after this date + created_before: retrieve programs that were created before this date or time. has_labels: retrieve programs that have labels on them specified by this dict. If the value is set to `*`, filters having the label diff --git a/cirq-google/cirq_google/engine/abstract_local_processor_test.py b/cirq-google/cirq_google/engine/abstract_local_processor_test.py index 461cc224da9..7a3aad9e2f6 100644 --- a/cirq-google/cirq_google/engine/abstract_local_processor_test.py +++ b/cirq-google/cirq_google/engine/abstract_local_processor_test.py @@ -102,7 +102,7 @@ def test_reservations(): p = NothingProcessor(processor_id='test') start_reservation = datetime.datetime.now() end_reservation = datetime.datetime.now() + datetime.timedelta(hours=2) - users = ['dstrain@google.com'] + users = ['gooduser@test.com'] # Create Reservation reservation = p.create_reservation( @@ -125,7 +125,7 @@ def test_reservations(): p.update_reservation(reservation_id=reservation.name, start_time=start_reservation) reservation = p.get_reservation(reservation.name) assert reservation.start_time.seconds == int(start_reservation.timestamp()) - users = ['dstrain@google.com', 'dabacon@google.com'] + users = ['gooduser@test.com', 'otheruser@prod.com'] p.update_reservation(reservation_id=reservation.name, whitelisted_users=users) reservation = p.get_reservation(reservation.name) assert reservation.whitelisted_users == users @@ -155,15 +155,15 @@ def test_list_reservations(): reservation2, reservation3, ] - assert p.list_reservations(now - 0.5 * hour, now + 3 * hour) == [reservation2, reservation3] - assert p.list_reservations(now + 0.5 * hour, now + 3 * hour) == [reservation3] - assert p.list_reservations(now - 0.5 * hour, now + 1.5 * hour) == [reservation2] - assert p.list_reservations(now - 1.5 * hour, now + 1.5 * hour) == [reservation1, reservation2] - - assert p.list_reservations(-0.5 * hour, 3 * hour) == [reservation2, reservation3] - assert p.list_reservations(0.5 * hour, 3 * hour) == [reservation3] - assert p.list_reservations(-0.5 * hour, 1.5 * hour) == [reservation2] - assert p.list_reservations(-1.5 * hour, 1.5 * hour) == [reservation1, reservation2] + assert p.list_reservations(now + 0.5 * hour, now + 3 * hour) == [reservation2, reservation3] + assert p.list_reservations(now + 1.5 * hour, now + 3 * hour) == [reservation3] + assert p.list_reservations(now + 0.5 * hour, now + 0.75 * hour) == [reservation2] + assert p.list_reservations(now - 1.5 * hour, now + 0.5 * hour) == [reservation1, reservation2] + + assert p.list_reservations(0.5 * hour, 3 * hour) == [reservation2, reservation3] + assert p.list_reservations(1.5 * hour, 3 * hour) == [reservation3] + assert p.list_reservations(0.25 * hour, 0.5 * hour) == [reservation2] + assert p.list_reservations(-1.5 * hour, 0.5 * hour) == [reservation1, reservation2] assert p.list_reservations(now - 2 * hour, None) == [reservation1, reservation2, reservation3] diff --git a/cirq-google/cirq_google/engine/abstract_local_program.py b/cirq-google/cirq_google/engine/abstract_local_program.py index fa9445d85ac..2ed1e907ee4 100644 --- a/cirq-google/cirq_google/engine/abstract_local_program.py +++ b/cirq-google/cirq_google/engine/abstract_local_program.py @@ -27,7 +27,7 @@ class AbstractLocalProgram(AbstractProgram): """A quantum program designed for local in-memory computation. This implements all the methods in `AbstractProgram` using - in-memory structions. Labels, descriptions, and time are all + in-memory objects. Labels, descriptions, and time are all stored using dictionaries. This is a partially implemented instance. Inheritors will still @@ -84,7 +84,7 @@ def list_jobs( Args: created_after: retrieve jobs that were created after this date or time. - created_before: retrieve jobs that were created after this date + created_before: retrieve jobs that were created before this date or time. has_labels: retrieve jobs that have labels on them specified by this dict. If the value is set to `*`, filters having the label From 2b3928a093f4daf3a0dfdd1d5da65983af5c0c38 Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Mon, 15 Nov 2021 06:04:09 -0800 Subject: [PATCH 11/16] Piece of missing documentation --- cirq-google/cirq_google/engine/abstract_local_program.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-google/cirq_google/engine/abstract_local_program.py b/cirq-google/cirq_google/engine/abstract_local_program.py index 2ed1e907ee4..e6dcb8e2a0b 100644 --- a/cirq-google/cirq_google/engine/abstract_local_program.py +++ b/cirq-google/cirq_google/engine/abstract_local_program.py @@ -31,7 +31,7 @@ class AbstractLocalProgram(AbstractProgram): stored using dictionaries. This is a partially implemented instance. Inheritors will still - A program created via the Engine interface. + need to implement abstract methods. """ def __init__(self, circuits: List[cirq.Circuit], engine: 'AbstractLocalEngine'): From 9b32368ff3bcfd351046f6bf17073de97c4225ab Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Mon, 15 Nov 2021 10:30:47 -0800 Subject: [PATCH 12/16] Update simulated local abstract to use new function types. --- .../engine/simulated_local_processor.py | 22 +++++++++++++++---- .../engine/simulated_local_processor_test.py | 16 +++++++------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/cirq-google/cirq_google/engine/simulated_local_processor.py b/cirq-google/cirq_google/engine/simulated_local_processor.py index 31b8c5c2aee..d42823ddea0 100644 --- a/cirq-google/cirq_google/engine/simulated_local_processor.py +++ b/cirq-google/cirq_google/engine/simulated_local_processor.py @@ -38,6 +38,18 @@ ] +def _date_to_timestamp( + union_time: Optional[Union[datetime.datetime, datetime.date, int]] +) -> Optional[int]: + if isinstance(union_time, int): + return union_time + elif isinstance(union_time, datetime.datetime): + return int(union_time.timestamp()) + elif isinstance(union_time, datetime.date): + return int(datetime.datetime.combine(union_time, datetime.datetime.min.time()).timestamp()) + return None + + class SimulatedLocalProcessor(AbstractLocalProcessor): """A processor backed by a sampler and device. @@ -121,10 +133,12 @@ def health(self): def list_calibrations( self, - earliest_timestamp_seconds: Optional[int] = None, - latest_timestamp_seconds: Optional[int] = None, + earliest_timestamp: Optional[Union[datetime.datetime, datetime.date, int]] = None, + latest_timestamp: Optional[Union[datetime.datetime, datetime.date, int]] = None, ) -> List[calibration.Calibration]: calibration_list: List[calibration.Calibration] = [] + earliest_timestamp_seconds = _date_to_timestamp(earliest_timestamp) + latest_timestamp_seconds = _date_to_timestamp(latest_timestamp) for calibration_seconds in self._calibrations: if ( earliest_timestamp_seconds is not None @@ -219,7 +233,7 @@ def run( program: cirq.Circuit, program_id: Optional[str] = None, job_id: Optional[str] = None, - param_resolver: cirq.ParamResolver = cirq.ParamResolver({}), + param_resolver: Optional[cirq.ParamResolver] = None, repetitions: int = 1, gate_set: Optional['Serializer'] = None, program_description: Optional[str] = None, @@ -256,7 +270,7 @@ def run( program=program, program_id=program_id, job_id=job_id, - params=[param_resolver], + params=[param_resolver or cirq.ParamResolver({})], repetitions=repetitions, gate_set=gate_set, program_description=program_description, diff --git a/cirq-google/cirq_google/engine/simulated_local_processor_test.py b/cirq-google/cirq_google/engine/simulated_local_processor_test.py index 6c93bb20f6d..2629312d406 100644 --- a/cirq-google/cirq_google/engine/simulated_local_processor_test.py +++ b/cirq-google/cirq_google/engine/simulated_local_processor_test.py @@ -27,6 +27,7 @@ def test_calibrations(): + now = datetime.datetime.now() future = int((datetime.datetime.now() + datetime.timedelta(hours=2)).timestamp()) cal_proto1 = v2.metrics_pb2.MetricsSnapshot(timestamp_ms=10000) cal_proto2 = v2.metrics_pb2.MetricsSnapshot(timestamp_ms=20000) @@ -41,20 +42,19 @@ def test_calibrations(): assert proc.get_calibration(20000) == cal2 assert proc.get_calibration(future) == cal3 assert proc.get_current_calibration() == cal2 + assert proc.list_calibrations(earliest_timestamp=5000, latest_timestamp=15000) == [cal1] + assert proc.list_calibrations(earliest_timestamp=15000, latest_timestamp=25000) == [cal2] assert proc.list_calibrations( - earliest_timestamp_seconds=5000, latest_timestamp_seconds=15000 - ) == [cal1] - assert proc.list_calibrations( - earliest_timestamp_seconds=15000, latest_timestamp_seconds=25000 - ) == [cal2] + earliest_timestamp=now, latest_timestamp=now + datetime.timedelta(hours=2) + ) == [cal3] assert proc.list_calibrations( - earliest_timestamp_seconds=future - 1, latest_timestamp_seconds=future + 1 + earliest_timestamp=datetime.date.today(), latest_timestamp=now + datetime.timedelta(hours=2) ) == [cal3] - cal_list = proc.list_calibrations(latest_timestamp_seconds=25000) + cal_list = proc.list_calibrations(latest_timestamp=25000) assert len(cal_list) == 2 assert cal1 in cal_list assert cal2 in cal_list - cal_list = proc.list_calibrations(earliest_timestamp_seconds=15000) + cal_list = proc.list_calibrations(earliest_timestamp=15000) assert len(cal_list) == 2 assert cal2 in cal_list assert cal3 in cal_list From e1cecc65db4d2b12833f5a9ae3b871b65d031fdc Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Mon, 15 Nov 2021 10:32:14 -0800 Subject: [PATCH 13/16] Fix typo --- cirq-google/cirq_google/engine/abstract_local_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-google/cirq_google/engine/abstract_local_processor.py b/cirq-google/cirq_google/engine/abstract_local_processor.py index c55a744df59..14c8139e183 100644 --- a/cirq-google/cirq_google/engine/abstract_local_processor.py +++ b/cirq-google/cirq_google/engine/abstract_local_processor.py @@ -283,7 +283,7 @@ def update_reservation( reservation_id: The string identifier of the reservation to change. start_time: New starting time of the reservation. If unspecified, starting time is left unchanged. - end_time: New starting time of the reservation. If unspecified, + end_time: New ending time of the reservation. If unspecified, ending time is left unchanged. whitelisted_users: The new list of whitelisted users to allow on the reservation. If unspecified, the users are left unchanged. From e64d01888fce86657ca8801e352cbe40d00ec319 Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Mon, 29 Nov 2021 10:35:24 -0800 Subject: [PATCH 14/16] Address review comments --- .../cirq_google/engine/simulated_local_job.py | 9 +-- .../engine/simulated_local_job_test.py | 15 +++- .../engine/simulated_local_processor.py | 72 ++++++++----------- .../engine/simulated_local_processor_test.py | 13 +++- .../engine/simulated_local_program.py | 2 - 5 files changed, 59 insertions(+), 52 deletions(-) diff --git a/cirq-google/cirq_google/engine/simulated_local_job.py b/cirq-google/cirq_google/engine/simulated_local_job.py index 207da701156..18c23110f37 100644 --- a/cirq-google/cirq_google/engine/simulated_local_job.py +++ b/cirq-google/cirq_google/engine/simulated_local_job.py @@ -11,7 +11,8 @@ # 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. -"""A helper for jobs that have been created on the Quantum Engine.""" +"""An implementation of AbstractJob that uses in-memory constructs +and a provided sampler to execute circuits.""" from typing import cast, List, Optional, Tuple import cirq @@ -43,12 +44,12 @@ class SimulatedLocalJob(AbstractLocalJob): def __init__( self, *args, - sampler: cirq.Sampler = cirq.Simulator(), + sampler: cirq.Sampler = None, simulation_type: LocalSimulationType = LocalSimulationType.SYNCHRONOUS, **kwargs, ): super().__init__(*args, **kwargs) - self._sampler = sampler + self._sampler = sampler or cirq.Simulator() self._simulation_type = simulation_type self._state = quantum.enums.ExecutionStatus.State.READY self._type = simulation_type @@ -122,4 +123,4 @@ def calibration_results(self) -> List[CalibrationResult]: This function will fail if any other type of results were returned. """ - raise NotImplemented # coverage: ignore + raise NotImplementedError 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 f44bbfc34dd..d63d9b82251 100644 --- a/cirq-google/cirq_google/engine/simulated_local_job_test.py +++ b/cirq-google/cirq_google/engine/simulated_local_job_test.py @@ -11,7 +11,7 @@ # 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. -"""A helper for jobs that have been created on the Quantum Engine.""" +"""Tests for SimulatedLocalJob.""" import pytest import numpy as np import sympy @@ -136,3 +136,16 @@ def test_failure(): code, message = job.failure() assert code == '500' assert 'Circuit contains ops whose symbols were not specified' in message + + +def test_run_calibration_unsupported(): + program = ParentProgram([cirq.Circuit()], None) + job = SimulatedLocalJob( + job_id='test_job', + processor_id='test1', + parent_program=program, + repetitions=100, + sweeps=[{}], + ) + with pytest.raises(NotImplementedError): + _ = job.calibration_results() diff --git a/cirq-google/cirq_google/engine/simulated_local_processor.py b/cirq-google/cirq_google/engine/simulated_local_processor.py index d42823ddea0..66f5123dc0c 100644 --- a/cirq-google/cirq_google/engine/simulated_local_processor.py +++ b/cirq-google/cirq_google/engine/simulated_local_processor.py @@ -33,7 +33,6 @@ VALID_LANGUAGES = [ 'type.googleapis.com/cirq.google.api.v2.Program', - 'type.googleapis.com/cirq.google.api.v2.FocusedCalibration', 'type.googleapis.com/cirq.google.api.v2.BatchProgram', ] @@ -54,7 +53,7 @@ class SimulatedLocalProcessor(AbstractLocalProcessor): """A processor backed by a sampler and device. Intended for local simulation testing, this processor will - create a VAlidationSampler that will validate requests based on + create a `ValidationSampler` that will validate requests based on the provided device and an additional Callable (that can verify serialization constraints, for instance). Jobs will then be executed using the provided sampler. @@ -66,7 +65,7 @@ class SimulatedLocalProcessor(AbstractLocalProcessor): API or for testing or mocking. Attributes: - sammpler: A `cirq.Sampler` that can execute the quantum jobs. + sampler: A `cirq.Sampler` that can execute the quantum jobs. device: An optional device, for validation of qubit connectivity. validator: A Callable that can validate additional characteristics beyond the device, such as serialization, repetition limits, etc. @@ -103,15 +102,9 @@ def get_calibration(self, calibration_timestamp_seconds: int) -> calibration.Cal return self._calibrations[calibration_timestamp_seconds] def get_latest_calibration(self, timestamp: int) -> Optional[calibration.Calibration]: - latest = None - current_calibration = None - for calibration_seconds in self._calibrations: - if calibration_seconds <= timestamp and ( - latest is None or latest < calibration_seconds - ): - latest = calibration_seconds - current_calibration = self._calibrations[latest] - return current_calibration + if not self._calibrations: + return None + return self._calibrations[max(self._calibrations)] def get_current_calibration(self) -> Optional[calibration.Calibration]: return self.get_latest_calibration(int(datetime.datetime.now().timestamp())) @@ -126,7 +119,7 @@ def get_device(self, gate_sets: Optional[Iterable['Serializer']] = None) -> cirq return self._device def get_device_specification(self) -> Optional[v2.device_pb2.DeviceSpecification]: - raise NotImplemented # coverage: ignore + raise NotImplementedError def health(self): return 'OK' @@ -136,22 +129,16 @@ def list_calibrations( earliest_timestamp: Optional[Union[datetime.datetime, datetime.date, int]] = None, latest_timestamp: Optional[Union[datetime.datetime, datetime.date, int]] = None, ) -> List[calibration.Calibration]: - calibration_list: List[calibration.Calibration] = [] - earliest_timestamp_seconds = _date_to_timestamp(earliest_timestamp) - latest_timestamp_seconds = _date_to_timestamp(latest_timestamp) - for calibration_seconds in self._calibrations: - if ( - earliest_timestamp_seconds is not None - and earliest_timestamp_seconds > calibration_seconds - ): - continue - if ( - latest_timestamp_seconds is not None - and latest_timestamp_seconds < calibration_seconds - ): - continue - calibration_list.append(self._calibrations[calibration_seconds]) - return calibration_list + earliest_timestamp_seconds = _date_to_timestamp(earliest_timestamp) or 0 + latest_timestamp_seconds = ( + _date_to_timestamp(latest_timestamp) + or datetime.datetime(datetime.MAXYEAR, 1, 1).timestamp() + ) + return [ + cal[1] + for cal in self._calibrations.items() + if earliest_timestamp_seconds <= cal[0] <= latest_timestamp_seconds + ] def get_sampler(self, gate_set: Optional['Serializer'] = None) -> cirq.Sampler: return self._sampler @@ -165,18 +152,19 @@ def list_programs( created_after: Optional[Union[datetime.datetime, datetime.date]] = None, has_labels: Optional[Dict[str, str]] = None, ) -> List[AbstractLocalProgram]: - programs: List[AbstractLocalProgram] = [] - for program in self._programs.values(): - if created_before is not None and created_before < program.create_time(): - continue - if created_after is not None and created_after > program.create_time(): - continue - if has_labels is not None: - labels = program.labels() - if any(key not in labels or labels[key] != has_labels[key] for key in has_labels): - continue - programs.append(program) - return programs + before_limit = created_before or datetime.datetime(datetime.MAXYEAR, 1, 1) + after_limit = created_after or datetime.datetime(datetime.MINYEAR, 1, 1) + labels = has_labels or {} + return list( + filter( + lambda program: after_limit < program.create_time() < before_limit + and all( + (key in program.labels() and program.labels()[key] == labels[key]) + for key in labels + ), + self._programs.values(), + ) + ) def get_program(self, program_id: str) -> AbstractProgram: """Returns an AbstractProgram for an existing Quantum Engine program. @@ -316,4 +304,4 @@ def run_sweep( return job def run_calibration(self, *args, **kwargs): - raise NotImplemented # coverage: ignore + raise NotImplementedError diff --git a/cirq-google/cirq_google/engine/simulated_local_processor_test.py b/cirq-google/cirq_google/engine/simulated_local_processor_test.py index 2629312d406..e70d9a146b0 100644 --- a/cirq-google/cirq_google/engine/simulated_local_processor_test.py +++ b/cirq-google/cirq_google/engine/simulated_local_processor_test.py @@ -11,7 +11,7 @@ # 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. -"""A helper for jobs that have been created on the Quantum Engine.""" +"""Tests for SimulatedLocalProcessor""" from typing import List import datetime import pytest @@ -144,6 +144,7 @@ def test_run_sweep(): assert len(job) == 2 assert np.all(job[0].measurements['m'] == 1) assert np.all(job[1].measurements['m'] == 0) + # Test iteration for idx, result in enumerate(job): assert np.all(result.measurements['m'] == 1 - idx) @@ -158,8 +159,6 @@ def test_run_sweep(): assert np.all(results[1].measurements['m'] == 0) assert job.execution_status() == quantum.enums.ExecutionStatus.State.SUCCESS - # iteration - def test_run_batch(): q = cirq.GridQubit(5, 4) @@ -217,3 +216,11 @@ def test_additional_validation(): # Test validation through sampler with pytest.raises(ValueError, match='No Y gates allowed!'): _ = proc.get_sampler().run_sweep(circuit, params=sweep, repetitions=100) + + +def test_unsupported(): + proc = SimulatedLocalProcessor(processor_id='test_proc') + with pytest.raises(NotImplementedError): + _ = proc.get_device_specification() + with pytest.raises(NotImplementedError): + _ = proc.run_calibration() diff --git a/cirq-google/cirq_google/engine/simulated_local_program.py b/cirq-google/cirq_google/engine/simulated_local_program.py index 272b1c442ff..f75d6db8620 100644 --- a/cirq-google/cirq_google/engine/simulated_local_program.py +++ b/cirq-google/cirq_google/engine/simulated_local_program.py @@ -27,8 +27,6 @@ class SimulatedLocalProgram(AbstractLocalProgram): This class functions as a parent class for a `SimulatedLocalJob` object. - - This is an abstract class that inheritors should implement. """ def __init__( From c5171772304fe7f2cdea27e02db15d4d94d20309 Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Mon, 29 Nov 2021 11:34:00 -0800 Subject: [PATCH 15/16] Get a more reasonable default end date for Windows --- cirq-google/cirq_google/engine/simulated_local_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-google/cirq_google/engine/simulated_local_processor.py b/cirq-google/cirq_google/engine/simulated_local_processor.py index 66f5123dc0c..a863f7f4e60 100644 --- a/cirq-google/cirq_google/engine/simulated_local_processor.py +++ b/cirq-google/cirq_google/engine/simulated_local_processor.py @@ -132,7 +132,7 @@ def list_calibrations( earliest_timestamp_seconds = _date_to_timestamp(earliest_timestamp) or 0 latest_timestamp_seconds = ( _date_to_timestamp(latest_timestamp) - or datetime.datetime(datetime.MAXYEAR, 1, 1).timestamp() + or (datetime.datetime.now() + datetime.timedelta(days=10000)).timestamp() ) return [ cal[1] From f46627d4ed9c4e1c62866c2857dcbd1536be8c82 Mon Sep 17 00:00:00 2001 From: Doug Strain Date: Mon, 29 Nov 2021 12:32:16 -0800 Subject: [PATCH 16/16] Add tests and address review comments. --- .../cirq_google/engine/simulated_local_job.py | 9 ++-- .../engine/simulated_local_processor.py | 11 +++-- .../engine/simulated_local_program_test.py | 43 +++++++++++++++++++ 3 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 cirq-google/cirq_google/engine/simulated_local_program_test.py diff --git a/cirq-google/cirq_google/engine/simulated_local_job.py b/cirq-google/cirq_google/engine/simulated_local_job.py index 18c23110f37..114cf697ef0 100644 --- a/cirq-google/cirq_google/engine/simulated_local_job.py +++ b/cirq-google/cirq_google/engine/simulated_local_job.py @@ -105,12 +105,9 @@ def results(self) -> List[cirq.Result]: program = self.program().get_circuit() try: self._state = quantum.enums.ExecutionStatus.State.SUCCESS - if sweeps: - return self._sampler.run_sweep( - program=program, params=sweeps[0], repetitions=reps - ) - else: - return [self._sampler.run(program=program, repetitions=reps)] + return self._sampler.run_sweep( + program=program, params=sweeps[0] if sweeps else None, repetitions=reps + ) except Exception as e: self._failure_code = '500' self._failure_message = str(e) diff --git a/cirq-google/cirq_google/engine/simulated_local_processor.py b/cirq-google/cirq_google/engine/simulated_local_processor.py index a863f7f4e60..454e4b94169 100644 --- a/cirq-google/cirq_google/engine/simulated_local_processor.py +++ b/cirq-google/cirq_google/engine/simulated_local_processor.py @@ -155,13 +155,16 @@ def list_programs( before_limit = created_before or datetime.datetime(datetime.MAXYEAR, 1, 1) after_limit = created_after or datetime.datetime(datetime.MINYEAR, 1, 1) labels = has_labels or {} + + def _labels_match(user_labels, program_labels): + return all( + (key in program_labels and program_labels[key] == labels[key]) for key in labels + ) + return list( filter( lambda program: after_limit < program.create_time() < before_limit - and all( - (key in program.labels() and program.labels()[key] == labels[key]) - for key in labels - ), + and _labels_match(labels, program.labels()), self._programs.values(), ) ) diff --git a/cirq-google/cirq_google/engine/simulated_local_program_test.py b/cirq-google/cirq_google/engine/simulated_local_program_test.py new file mode 100644 index 00000000000..fd3d483cce0 --- /dev/null +++ b/cirq-google/cirq_google/engine/simulated_local_program_test.py @@ -0,0 +1,43 @@ +# Copyright 2021 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 cirq + +from cirq_google.engine.simulated_local_program import SimulatedLocalProgram +from cirq_google.engine.simulated_local_job import SimulatedLocalJob + + +def test_id(): + program = SimulatedLocalProgram([cirq.Circuit()], None, program_id='program') + assert program.id() == 'program' + + +def test_delete(): + program = SimulatedLocalProgram([cirq.Circuit()], None, program_id='program') + job1 = SimulatedLocalJob( + job_id='test_job1', processor_id='test1', parent_program=program, repetitions=100, sweeps=[] + ) + job2 = SimulatedLocalJob( + job_id='test_job2', processor_id='test1', parent_program=program, repetitions=100, sweeps=[] + ) + job3 = SimulatedLocalJob( + job_id='test_job3', processor_id='test1', parent_program=program, repetitions=100, sweeps=[] + ) + program.add_job(job1.id(), job1) + program.add_job(job2.id(), job2) + program.add_job(job3.id(), job3) + assert set(program.list_jobs()) == {job1, job2, job3} + program.delete_job(job2.id()) + assert set(program.list_jobs()) == {job1, job3} + program.delete(delete_jobs=True) + assert program.list_jobs() == []