Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cg.ProcessorSampler #5361

Merged
merged 9 commits into from
May 16, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cirq-google/cirq_google/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
EngineResult,
ProtoVersion,
QuantumEngineSampler,
QuantumProcessorSampler,
ValidatingSampler,
get_engine,
get_engine_calibration,
Expand Down
2 changes: 2 additions & 0 deletions cirq-google/cirq_google/engine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,5 @@
)

from cirq_google.engine.engine_result import EngineResult

from cirq_google.engine.processor_sampler import QuantumProcessorSampler
10 changes: 6 additions & 4 deletions cirq-google/cirq_google/engine/abstract_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from cirq_google.engine import calibration, util

if TYPE_CHECKING:
import cirq_google
import cirq_google as cg
import cirq_google.engine.abstract_engine as abstract_engine
import cirq_google.engine.abstract_job as abstract_job
import cirq_google.serialization.serializer as serializer
Expand Down Expand Up @@ -97,7 +97,7 @@ def run(
@util.deprecated_gate_set_parameter
def run_sweep(
self,
program: cirq.Circuit,
program: cirq.AbstractCircuit,
program_id: Optional[str] = None,
job_id: Optional[str] = None,
params: cirq.Sweepable = None,
Expand Down Expand Up @@ -196,7 +196,7 @@ def run_batch(
@util.deprecated_gate_set_parameter
def run_calibration(
self,
layers: List['cirq_google.CalibrationLayer'],
layers: List['cg.CalibrationLayer'],
program_id: Optional[str] = None,
job_id: Optional[str] = None,
gate_set: Optional['serializer.Serializer'] = None,
Expand Down Expand Up @@ -241,7 +241,9 @@ def run_calibration(

@abc.abstractmethod
@util.deprecated_gate_set_parameter
def get_sampler(self, gate_set: Optional['serializer.Serializer'] = None) -> cirq.Sampler:
def get_sampler(
self, gate_set: Optional['serializer.Serializer'] = None
) -> 'cg.QuantumProcessorSampler':
"""Returns a sampler backed by the processor.

Args:
Expand Down
9 changes: 4 additions & 5 deletions cirq-google/cirq_google/engine/engine_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@
abstract_processor,
calibration,
calibration_layer,
engine_sampler,
processor_sampler,
util,
)
from cirq_google.serialization import serializable_gate_set, serializer
from cirq_google.serialization import gate_sets as gs

if TYPE_CHECKING:
import cirq_google as cg
import cirq_google.engine.engine as engine_base
import cirq_google.engine.abstract_job as abstract_job

Expand Down Expand Up @@ -105,7 +106,7 @@ def engine(self) -> 'engine_base.Engine':
@util.deprecated_gate_set_parameter
def get_sampler(
self, gate_set: Optional[serializer.Serializer] = None
) -> engine_sampler.QuantumEngineSampler:
) -> 'cg.engine.QuantumProcessorSampler':
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wcourtney Will changing this cause any backwards incompatibility problems? This affects the QCS engine in addition to the simulated version.

I think it should be fine, but want to double-check.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a backwards incompatible change for anyone using processor.get_sampler() and trying to use multiple processor_ids. However, this workflow doesn't make any sense and I don't think many people are using processor.get_sampler().

Most people using cg.get_engine().get_sampler() or cg.get_engine_sampler() will be unaffected

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the PR description

"""Returns a sampler backed by the engine.

Args:
Expand All @@ -117,9 +118,7 @@ def get_sampler(
that will send circuits to the Quantum Computing Service
when sampled.1
"""
return engine_sampler.QuantumEngineSampler(
engine=self.engine(), processor_id=self.processor_id
)
return processor_sampler.QuantumProcessorSampler(processor=self)

@util.deprecated_gate_set_parameter
def run_batch(
Expand Down
76 changes: 76 additions & 0 deletions cirq-google/cirq_google/engine/processor_sampler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Copyright 2022 The Cirq Developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: should EngineSampler use this class underneath?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it (easily) can.

Engine.run_sweep and Processor.run_sweep have meaningfully different semantics: The first will send out a job to one of potentially many processors. This logic happens server-side. While we could theoretically replicate it using a collection of Processors / ProcessorSamplers underneath EngineSampler, it wouldn't be a small change.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am starting to agree with maffoo. It seems like ProcessorSampler is basically the same as a QuantumEngineSampler with a processor set already. Seems like these two can/should be combined.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After off-line conversations, I am fully on-board with this change.

I think we should deprecate QuantumEngineSampler and switch engine.get_sampler(processor_id) to use engine.get_processor(processor_id).get_sampler() (or, alternatively, make QuantumEngineSampler a thin wrapper around this ProcessorSampler functionality). Since this functionality currently supports multiple processor ids and sending an EngineProgram, it probably needs more thought and should be done in a follow-up PR.

Short story, I approve this PR as is.


from typing import List, Optional, Sequence, TYPE_CHECKING, Union, cast

import cirq

if TYPE_CHECKING:
import cirq_google as cg


class QuantumProcessorSampler(cirq.Sampler):
"""A wrapper around AbstractProcessor to implement the cirq.Sampler interface."""

def __init__(self, *, processor: 'cg.engine.AbstractProcessor'):
"""Inits QuantumProcessorSampler.

Args:
processor: AbstractProcessor instance to use.
"""
self._processor = processor

def run_sweep(
self, program: 'cirq.AbstractCircuit', params: cirq.Sweepable, repetitions: int = 1
) -> Sequence['cg.EngineResult']:
job = self._processor.run_sweep(program=program, params=params, repetitions=repetitions)
return job.results()

def run_batch(
self,
programs: Sequence[cirq.AbstractCircuit],
params_list: Optional[List[cirq.Sweepable]] = None,
repetitions: Union[int, List[int]] = 1,
) -> Sequence[Sequence['cg.EngineResult']]:
"""Runs the supplied circuits.

In order to gain a speedup from using this method instead of other run
methods, the following conditions must be satisfied:
1. All circuits must measure the same set of qubits.
2. The number of circuit repetitions must be the same for all
circuits. That is, the `repetitions` argument must be an integer,
or else a list with identical values.
"""
if isinstance(repetitions, List) and len(programs) != len(repetitions):
raise ValueError(
'len(programs) and len(repetitions) must match. '
f'Got {len(programs)} and {len(repetitions)}.'
)
if isinstance(repetitions, int) or len(set(repetitions)) == 1:
# All repetitions are the same so batching can be done efficiently
if isinstance(repetitions, List):
repetitions = repetitions[0]
job = self._processor.run_batch(
programs=programs, params_list=params_list, repetitions=repetitions
)
return job.batched_results()
# Varying number of repetitions so no speedup
return cast(
Sequence[Sequence['cg.EngineResult']],
super().run_batch(programs, params_list, repetitions),
)

@property
def processor(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add return type.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return self._processor
110 changes: 110 additions & 0 deletions cirq-google/cirq_google/engine/processor_sampler_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Copyright 2022 The Cirq Developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from unittest import mock

import pytest

import cirq
import cirq_google as cg


@pytest.mark.parametrize('circuit', [cirq.Circuit(), cirq.FrozenCircuit()])
def test_run_circuit(circuit):
processor = mock.Mock()
sampler = cg.QuantumProcessorSampler(processor=processor)
params = [cirq.ParamResolver({'a': 1})]
sampler.run_sweep(circuit, params, 5)
processor.run_sweep.assert_called_with(params=params, program=circuit, repetitions=5)


def test_run_batch():
processor = mock.Mock()
sampler = cg.QuantumProcessorSampler(processor=processor)
a = cirq.LineQubit(0)
circuit1 = cirq.Circuit(cirq.X(a))
circuit2 = cirq.Circuit(cirq.Y(a))
params1 = [cirq.ParamResolver({'t': 1})]
params2 = [cirq.ParamResolver({'t': 2})]
circuits = [circuit1, circuit2]
params_list = [params1, params2]
sampler.run_batch(circuits, params_list, 5)
processor.run_batch.assert_called_with(
params_list=params_list, programs=circuits, repetitions=5
)


def test_run_batch_identical_repetitions():
processor = mock.Mock()
sampler = cg.QuantumProcessorSampler(processor=processor)
a = cirq.LineQubit(0)
circuit1 = cirq.Circuit(cirq.X(a))
circuit2 = cirq.Circuit(cirq.Y(a))
params1 = [cirq.ParamResolver({'t': 1})]
params2 = [cirq.ParamResolver({'t': 2})]
circuits = [circuit1, circuit2]
params_list = [params1, params2]
sampler.run_batch(circuits, params_list, [5, 5])
processor.run_batch.assert_called_with(
params_list=params_list, programs=circuits, repetitions=5
)


def test_run_batch_bad_number_of_repetitions():
processor = mock.Mock()
sampler = cg.QuantumProcessorSampler(processor=processor)
a = cirq.LineQubit(0)
circuit1 = cirq.Circuit(cirq.X(a))
circuit2 = cirq.Circuit(cirq.Y(a))
params1 = [cirq.ParamResolver({'t': 1})]
params2 = [cirq.ParamResolver({'t': 2})]
circuits = [circuit1, circuit2]
params_list = [params1, params2]
with pytest.raises(ValueError, match='2 and 3'):
sampler.run_batch(circuits, params_list, [5, 5, 5])


def test_run_batch_differing_repetitions():
processor = mock.Mock()
job = mock.Mock()
job.results.return_value = []
processor.run_sweep.return_value = job
sampler = cg.QuantumProcessorSampler(processor=processor)
a = cirq.LineQubit(0)
circuit1 = cirq.Circuit(cirq.X(a))
circuit2 = cirq.Circuit(cirq.Y(a))
params1 = [cirq.ParamResolver({'t': 1})]
params2 = [cirq.ParamResolver({'t': 2})]
circuits = [circuit1, circuit2]
params_list = [params1, params2]
repetitions = [1, 2]
sampler.run_batch(circuits, params_list, repetitions)
processor.run_sweep.assert_called_with(params=params2, program=circuit2, repetitions=2)
processor.run_batch.assert_not_called()


def test_processor_sampler_processor_property():
processor = mock.Mock()
sampler = cg.QuantumProcessorSampler(processor=processor)
assert sampler.processor is processor


def test_with_local_processor():
sampler = cg.QuantumProcessorSampler(
processor=cg.engine.SimulatedLocalProcessor(processor_id='my-fancy-processor')
)
r = sampler.run(cirq.Circuit(cirq.measure(cirq.LineQubit(0), key='z')))
assert isinstance(r, cg.EngineResult)
assert r.job_id == 'projects/fake_project/processors/my-fancy-processor/job/2'
assert r.measurements['z'] == [[0]]
6 changes: 3 additions & 3 deletions cirq-google/cirq_google/engine/qcs_notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

from cirq_google import (
PhasedFSimEngineSimulator,
QuantumEngineSampler,
QuantumProcessorSampler,
Sycamore,
SQRT_ISWAP_INV_PARAMETERS,
PhasedFSimCharacterization,
Expand All @@ -30,7 +30,7 @@
@dataclasses.dataclass
class QCSObjectsForNotebook:
device: cirq.Device
sampler: Union[PhasedFSimEngineSimulator, QuantumEngineSampler]
sampler: Union[PhasedFSimEngineSimulator, QuantumProcessorSampler]
signed_in: bool

@property
Expand Down Expand Up @@ -80,7 +80,7 @@ def get_qcs_objects_for_notebook(
print(f"Authentication failed: {exc}")

# Attempt to connect to the Quantum Engine API, and use a simulator if unable to connect.
sampler: Union[PhasedFSimEngineSimulator, QuantumEngineSampler]
sampler: Union[PhasedFSimEngineSimulator, QuantumProcessorSampler]
try:
engine = get_engine(project_id)
if processor_id:
Expand Down
6 changes: 4 additions & 2 deletions cirq-google/cirq_google/engine/simulated_local_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
from cirq_google.engine.simulated_local_job import SimulatedLocalJob
from cirq_google.engine.simulated_local_program import SimulatedLocalProgram
from cirq_google.serialization.circuit_serializer import CIRCUIT_SERIALIZER
from cirq_google.engine.processor_sampler import QuantumProcessorSampler

if TYPE_CHECKING:
import cirq_google as cg
from cirq_google.serialization.serializer import Serializer

VALID_LANGUAGES = [
Expand Down Expand Up @@ -161,7 +163,7 @@ def list_calibrations(

@util.deprecated_gate_set_parameter
def get_sampler(self, gate_set: Optional['Serializer'] = None) -> cirq.Sampler:
return self._sampler
return QuantumProcessorSampler(processor=self)

def supported_languages(self) -> List[str]:
return VALID_LANGUAGES
Expand Down Expand Up @@ -256,7 +258,7 @@ def run(
program_labels: Optional[Dict[str, str]] = None,
job_description: Optional[str] = None,
job_labels: Optional[Dict[str, str]] = None,
) -> cirq.Result:
) -> 'cg.EngineResult':
"""Runs the supplied Circuit on this processor.

Args:
Expand Down
1 change: 1 addition & 0 deletions cirq-google/cirq_google/json_test_data/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
'SerializingArg',
'THETA_ZETA_GAMMA_FLOQUET_PHASED_FSIM_CHARACTERIZATION',
'QuantumEngineSampler',
'QuantumProcessorSampler',
'ValidatingSampler',
'CouldNotPlaceError',
# Abstract:
Expand Down
2 changes: 1 addition & 1 deletion cirq-google/cirq_google/workflow/processor_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def get_processor(self) -> 'cg.engine.AbstractProcessor':
This is the primary method that descendants must implement.
"""

def get_sampler(self) -> 'cirq.Sampler':
def get_sampler(self) -> 'cg.QuantumProcessorSampler':
"""Return a `cirq.Sampler` for the processor specified by this class.

The default implementation delegates to `self.get_processor()`.
Expand Down
27 changes: 23 additions & 4 deletions cirq-google/cirq_google/workflow/processor_record_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,18 +118,37 @@ def test_simulated_backend_with_bad_local_device():
def test_simulated_backend_descriptive_name():
p = cg.SimulatedProcessorWithLocalDeviceRecord('rainbow')
assert str(p) == 'rainbow-simulator'
assert isinstance(p.get_sampler(), cg.ValidatingSampler)
assert isinstance(p.get_sampler()._sampler, cirq.Simulator)
assert isinstance(p.get_sampler(), cg.engine.QuantumProcessorSampler)

# The actual simulator hiding behind the indirection is
# p.get_sampler() -> QuantumProcessorSampler
# p.get_sampler().processor._sampler -> Validating Sampler
# p.get_sampler().processor._sampler._sampler -> The actual simulator
assert isinstance(p.get_sampler().processor._sampler._sampler, cirq.Simulator)

p = cg.SimulatedProcessorWithLocalDeviceRecord('rainbow', noise_strength=1e-3)
assert str(p) == 'rainbow-depol(1.000e-03)'
assert isinstance(p.get_sampler()._sampler, cirq.DensityMatrixSimulator)
assert isinstance(p.get_sampler().processor._sampler._sampler, cirq.DensityMatrixSimulator)

p = cg.SimulatedProcessorWithLocalDeviceRecord('rainbow', noise_strength=float('inf'))
assert str(p) == 'rainbow-zeros'
assert isinstance(p.get_sampler()._sampler, cirq.ZerosSampler)
assert isinstance(p.get_sampler().processor._sampler._sampler, cirq.ZerosSampler)


def test_sampler_equality():
p = cg.SimulatedProcessorWithLocalDeviceRecord('rainbow')
assert p.get_sampler().__class__ == p.get_processor().get_sampler().__class__


def test_engine_result():
proc_rec = cg.SimulatedProcessorWithLocalDeviceRecord('rainbow')

proc = proc_rec.get_processor()
samp = proc_rec.get_sampler()

circ = cirq.Circuit(cirq.measure(cirq.GridQubit(5, 4)))

res1 = proc.run(circ)
assert isinstance(res1, cg.EngineResult)
res2 = samp.run(circ)
assert isinstance(res2, cg.EngineResult)