diff --git a/cirq-google/cirq_google/engine/abstract_engine.py b/cirq-google/cirq_google/engine/abstract_engine.py index 4d6dea9cf20..1df4f9a3783 100644 --- a/cirq-google/cirq_google/engine/abstract_engine.py +++ b/cirq-google/cirq_google/engine/abstract_engine.py @@ -11,7 +11,6 @@ # 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. -# coverage: ignore """Interface for Engine objects. This class is an abstract class which all Engine implementations 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..45c278f90fb --- /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. + + Iteratively 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..6dffdd258d8 --- /dev/null +++ b/cirq-google/cirq_google/engine/abstract_local_engine_test.py @@ -0,0 +1,163 @@ +# 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 +import cirq_google.engine.calibration as calibration + + +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, timestamp: int) -> Optional[calibration.Calibration]: + return calibration.Calibration() + + 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 + + with pytest.raises(KeyError): + _ = engine.get_processor('invalid') + + +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='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='crop', 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] + + 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') + 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..ffbf868bdd8 --- /dev/null +++ b/cirq-google/cirq_google/engine/abstract_local_job.py @@ -0,0 +1,173 @@ +# 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 + self._update_time = datetime.datetime.now() + 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) + self._update_time = datetime.datetime.now() + 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. + """ + self._update_time = datetime.datetime.now() + 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. + """ + self._update_time = datetime.datetime.now() + 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..7b7877515ad --- /dev/null +++ b/cirq-google/cirq_google/engine/abstract_local_job_test.py @@ -0,0 +1,95 @@ +# 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.id() == 'test' + 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 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..14c8139e183 --- /dev/null +++ b/cirq-google/cirq_google/engine/abstract_local_processor.py @@ -0,0 +1,422 @@ +# 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. + 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._resource_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._resource_id_counter += 1 + return ( + f'projects/{self._project_name}/' + f'processors/{self._processor_id}/' + f'{id_type}/{self._resource_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: + # [---time_slot---] + # [-------------t---------] + # 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: + # [---time_slot---] + # [----t-----] + 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) -> 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) -> qtypes.QuantumReservation: + """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: 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 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. + + 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.') + 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), + ) -> 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. + """ + start_timestamp = _to_timestamp(from_time) + end_timestamp = _to_timestamp(to_time) + reservation_list = [] + for reservation in self._reservations.values(): + if end_timestamp and reservation.start_time.seconds > end_timestamp: + continue + if start_timestamp and reservation.end_time.seconds < start_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 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 + 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..7a3aad9e2f6 --- /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(2000000), + end_time=_time(1000000), + ) + + +def test_reservations(): + p = NothingProcessor(processor_id='test') + start_reservation = datetime.datetime.now() + end_reservation = datetime.datetime.now() + datetime.timedelta(hours=2) + users = ['gooduser@test.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 = ['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 + + 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 + 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] + + 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=1000000), + end_time=Timestamp(seconds=3000000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, + ) + time_slot2 = qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=2000000), + end_time=Timestamp(seconds=4000000), + 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=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(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=1000000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, + ) + unbounded_end = qtypes.QuantumTimeSlot( + processor_name='test', + 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(500000), + to_time=_time(2500000), + ) + == [unbounded_start, unbounded_end] + ) + assert ( + p.get_schedule( + from_time=_time(1500000), + to_time=_time(2500000), + ) + == [unbounded_end] + ) + assert ( + p.get_schedule( + from_time=_time(500000), + to_time=_time(1500000), + ) + == [unbounded_start] + ) + assert ( + p.get_schedule( + from_time=_time(1200000), + to_time=_time(1500000), + ) + == [] + ) + + +@pytest.mark.parametrize( + ('time_slot'), + ( + qtypes.QuantumTimeSlot( + processor_name='test', + 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=1000000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.OPEN_SWIM, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + end_time=Timestamp(seconds=2000000), + 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(500000), + end_time=_time(1500000), + ) + + +def test_create_reservation_open_time_slots(): + time_slot = qtypes.QuantumTimeSlot( + processor_name='test', + 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(500000), + end_time=_time(1500000), + ) + assert p.get_schedule(from_time=_time(200000), to_time=_time(2500000)) == [ + qtypes.QuantumTimeSlot( + processor_name='test', + 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=1500000), + end_time=Timestamp(seconds=2000000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ), + ] + + +def test_create_reservation_split_time_slots(): + time_slot = qtypes.QuantumTimeSlot( + processor_name='test', + 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(1200000), + end_time=_time(1500000), + ) + assert p.get_schedule(from_time=_time(200000), to_time=_time(2500000)) == [ + qtypes.QuantumTimeSlot( + processor_name='test', + 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=1200000), + end_time=Timestamp(seconds=1500000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1500000), + end_time=Timestamp(seconds=2000000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ), + ] + + +def test_create_reservation_add_at_end(): + time_slot = qtypes.QuantumTimeSlot( + processor_name='test', + 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(2500000), + end_time=_time(3500000), + ) + assert p.get_schedule(from_time=_time(500000), to_time=_time(2500000)) == [ + qtypes.QuantumTimeSlot( + processor_name='test', + 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=2500000), + end_time=Timestamp(seconds=3500000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, + ), + ] + + +def test_create_reservation_border_conditions(): + time_slot = qtypes.QuantumTimeSlot( + processor_name='test', + 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(1900000), + end_time=_time(2000000), + ) + p.create_reservation( + start_time=_time(1000000), + end_time=_time(1100000), + ) + assert p.get_schedule(from_time=_time(200000), to_time=_time(2500000)) == [ + qtypes.QuantumTimeSlot( + processor_name='test', + 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=1100000), + end_time=Timestamp(seconds=1900000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=1900000), + end_time=Timestamp(seconds=2000000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, + ), + ] + + +def test_create_reservation_unbounded(): + time_slot_begin = qtypes.QuantumTimeSlot( + processor_name='test', + end_time=Timestamp(seconds=2000000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ) + time_slot_end = qtypes.QuantumTimeSlot( + processor_name='test', + 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(1000000), + end_time=_time(3000000), + ) + p.create_reservation( + start_time=_time(4000000), + end_time=_time(6000000), + ) + assert p.get_schedule(from_time=_time(200000), to_time=_time(10000000)) == [ + qtypes.QuantumTimeSlot( + processor_name='test', + end_time=Timestamp(seconds=1000000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + 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=4000000), + end_time=Timestamp(seconds=6000000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=6000000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ), + ] + + +def test_create_reservation_splitunbounded(): + time_slot_begin = qtypes.QuantumTimeSlot( + processor_name='test', + end_time=Timestamp(seconds=3000000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ) + time_slot_end = qtypes.QuantumTimeSlot( + processor_name='test', + 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(1000000), + end_time=_time(2000000), + ) + p.create_reservation( + start_time=_time(6000000), + end_time=_time(7000000), + ) + assert p.get_schedule(from_time=_time(200000), to_time=_time(10000000)) == [ + qtypes.QuantumTimeSlot( + processor_name='test', + end_time=Timestamp(seconds=1000000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + 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=2000000), + end_time=Timestamp(seconds=3000000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.UNALLOCATED, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + 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=6000000), + end_time=Timestamp(seconds=7000000), + slot_type=qenums.QuantumTimeSlot.TimeSlotType.RESERVATION, + ), + qtypes.QuantumTimeSlot( + processor_name='test', + start_time=Timestamp(seconds=7000000), + 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..e6dcb8e2a0b --- /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 objects. Labels, descriptions, and time are all + stored using dictionaries. + + This is a partially implemented instance. Inheritors will still + need to implement abstract methods. + """ + + 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 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 + 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 index 61222c3e734..04d0314084c 100644 --- a/cirq-google/cirq_google/engine/abstract_processor.py +++ b/cirq-google/cirq_google/engine/abstract_processor.py @@ -11,7 +11,6 @@ # 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. -# coverage: ignore """Abstract interface for a quantum processor. This interface can run circuits, sweeps, batches, or calibration diff --git a/cirq-google/cirq_google/engine/abstract_program.py b/cirq-google/cirq_google/engine/abstract_program.py index 4a9c80b8936..19249ed5d21 100644 --- a/cirq-google/cirq_google/engine/abstract_program.py +++ b/cirq-google/cirq_google/engine/abstract_program.py @@ -11,7 +11,6 @@ # 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. -# coverage: ignore """An interface for quantum programs. The quantum program represents a circuit (or other execution) that,