From 37716d3ccfc5d2298834e24d08a878bcedcc4dde Mon Sep 17 00:00:00 2001 From: Dave Bacon Date: Mon, 16 Nov 2020 22:12:03 -0800 Subject: [PATCH 1/3] Fixes run_batch requiring param list --- cirq/google/engine/engine.py | 10 +- cirq/google/engine/engine_program.py | 35 ++- cirq/google/engine/engine_program_test.py | 320 +++++++++++++--------- cirq/google/engine/engine_test.py | 25 ++ 4 files changed, 248 insertions(+), 142 deletions(-) diff --git a/cirq/google/engine/engine.py b/cirq/google/engine/engine.py index 9622ed1850b..d548a8bd9c5 100644 --- a/cirq/google/engine/engine.py +++ b/cirq/google/engine/engine.py @@ -325,9 +325,11 @@ def run_batch( 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 + 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. + 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. processor_ids: The engine processors that should be candidates @@ -347,7 +349,9 @@ def run_batch( for a circuit are listed in the order imposed by the associated parameter sweep. """ - if not params_list or len(programs) != len(params_list): + if params_list is None: + params_list = [dict()] * len(programs) + if len(programs) != len(params_list): raise ValueError('Number of circuits and sweeps must match') if not processor_ids: raise ValueError('Processor id must be specified.') diff --git a/cirq/google/engine/engine_program.py b/cirq/google/engine/engine_program.py index 17f9b2c20fb..d5a816bb22d 100644 --- a/cirq/google/engine/engine_program.py +++ b/cirq/google/engine/engine_program.py @@ -53,7 +53,7 @@ def __init__(self, program_id: Unique ID of the program within the parent project. context: Engine configuration and context to use. _program: The optional current program state. - result_type: Whether the program was created using a BatchProgram. + result_type: The type of program that was created. """ self.project_id = project_id self.program_id = program_id @@ -135,7 +135,9 @@ def run_batch( where # is alphanumeric and YYMMDD is the current year, month, and day. params_list: Parameter sweeps to run with the program. There must - be one Sweepable object for each circuit in the batch. + be one Sweepable object for each circuit in the batch. If this + is None, it is assumed that the circuits are not parameterized + and do not require sweeps. repetitions: The number of circuit repetitions to run. processor_ids: The engine processors that should be candidates to run the program. Only one of these will be scheduled for @@ -149,16 +151,20 @@ def run_batch( first, then the TrialResults for the second, etc. The TrialResults for a circuit are listed in the order imposed by the associated parameter sweep. + + Raises: + ValueError: if the program was not a batch program or no processors + were supplied. """ import cirq.google.engine.engine as engine_base if self.result_type != ResultType.Batch: raise ValueError('Can only use run_batch() in batch mode.') + if params_list is None: + params_list = [dict()] * self.batch_size() if not job_id: job_id = engine_base._make_random_id('job-') if not processor_ids: raise ValueError('No processors specified') - if not params_list: - raise ValueError('No parameter list specified') # Pack the run contexts into batches batch = v2.batch_pb2.BatchRunContext() @@ -464,6 +470,27 @@ def get_circuit(self, program_num: Optional[int] = None) -> 'Circuit': self.project_id, self.program_id, True) return self._deserialize_program(self._program.code, program_num) + 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. + """ + if self.result_type != ResultType.Batch: + raise ValueError('Program was not a batch program but instead ' + f'was of type {self.result_type}.') + import cirq.google.engine.engine as engine_base + if not self._program or not self._program.HasField('code'): + self._program = self.context.client.get_program( + self.project_id, self.program_id, True) + code = self._program.code + code_type = code.type_url[len(engine_base.TYPE_PREFIX):] + if code_type == 'cirq.google.api.v2.BatchProgram': + batch = v2.batch_pb2.BatchProgram.FromString(code.value) + return len(batch.programs) + raise ValueError('Program was not a batch program but instead was of ' + f'type {code_type}.') + @staticmethod def _deserialize_program(code: qtypes.any_pb2.Any, program_num: Optional[int] = None) -> 'Circuit': diff --git a/cirq/google/engine/engine_program_test.py b/cirq/google/engine/engine_program_test.py index 67dce18b6ea..3f34c4a0279 100644 --- a/cirq/google/engine/engine_program_test.py +++ b/cirq/google/engine/engine_program_test.py @@ -32,6 +32,134 @@ def _to_any(proto): return any_proto +_BATCH_PROGRAM_V2 = _to_any( + Merge( + """programs { language { + gate_set: "xmon" +} +circuit { + scheduling_strategy: MOMENT_BY_MOMENT + moments { + operations { + gate { + id: "xy" + } + args { + key: "axis_half_turns" + value { + arg_value { + float_value: 0.0 + } + } + } + args { + key: "half_turns" + value { + arg_value { + float_value: 0.5 + } + } + } + qubits { + id: "5_2" + } + } + } + moments { + operations { + gate { + id: "meas" + } + args { + key: "invert_mask" + value { + arg_value { + bool_values { + } + } + } + } + args { + key: "key" + value { + arg_value { + string_value: "result" + } + } + } + qubits { + id: "5_2" + } + } + } +} +} +""", v2.batch_pb2.BatchProgram())) + +_PROGRAM_V2 = _to_any( + Merge( + """language { + gate_set: "xmon" +} +circuit { + scheduling_strategy: MOMENT_BY_MOMENT + moments { + operations { + gate { + id: "xy" + } + args { + key: "axis_half_turns" + value { + arg_value { + float_value: 0.0 + } + } + } + args { + key: "half_turns" + value { + arg_value { + float_value: 0.5 + } + } + } + qubits { + id: "5_2" + } + } + } + moments { + operations { + gate { + id: "meas" + } + args { + key: "invert_mask" + value { + arg_value { + bool_values { + } + } + } + } + args { + key: "key" + value { + arg_value { + string_value: "result" + } + } + } + qubits { + id: "5_2" + } + } + } +} +""", v2.program_pb2.Program())) + + @mock.patch('cirq.google.engine.engine_client.EngineClient.create_job') def test_run_sweeps_delegation(create_job): create_job.return_value = ('steve', qtypes.QuantumJob()) @@ -84,13 +212,25 @@ def test_run_calibration_no_processors(create_job): _ = program.run_calibration(job_id='spot') -def test_run_batch_no_sweeps(): - program = cg.EngineProgram('no-meow', - 'no-meow', - EngineContext(), - result_type=ResultType.Batch) - with pytest.raises(ValueError, match='No parameter list specified'): - _ = program.run_batch(repetitions=1, processor_ids=['lazykitty']) +@mock.patch('cirq.google.engine.engine_client.EngineClient.create_job') +def test_run_batch_no_sweeps(create_job): + # Running with no sweeps is fine. Uses program's batch size to create + # proper empty sweeps. + create_job.return_value = ('kittens', qtypes.QuantumJob()) + program = cg.EngineProgram( + 'my-meow', + 'my-meow', + _program=qtypes.QuantumProgram(code=_BATCH_PROGRAM_V2), + context=EngineContext(), + result_type=ResultType.Batch) + job = program.run_batch(job_id='steve', + repetitions=10, + processor_ids=['lazykitty']) + assert job._job == qtypes.QuantumJob() + print(create_job.call_args[1]['run_context']) + batch_run_context = v2.batch_pb2.BatchRunContext() + create_job.call_args[1]['run_context'].Unpack(batch_run_context) + assert len(batch_run_context.run_contexts) == 1 def test_run_batch_no_processors(): @@ -118,7 +258,7 @@ def test_run_batch_not_in_batch_mode(): params_list=resolver_list) -def test_run__in_batch_mode(): +def test_run_in_batch_mode(): program = cg.EngineProgram('no-meow', 'no-meow', EngineContext(), @@ -352,70 +492,8 @@ def test_get_circuit_v2(get_program): cirq.X(cirq.GridQubit(5, 2))**0.5, cirq.measure(cirq.GridQubit(5, 2), key='result')) - program_proto = Merge( - """language { - gate_set: "xmon" -} -circuit { - scheduling_strategy: MOMENT_BY_MOMENT - moments { - operations { - gate { - id: "xy" - } - args { - key: "axis_half_turns" - value { - arg_value { - float_value: 0.0 - } - } - } - args { - key: "half_turns" - value { - arg_value { - float_value: 0.5 - } - } - } - qubits { - id: "5_2" - } - } - } - moments { - operations { - gate { - id: "meas" - } - args { - key: "invert_mask" - value { - arg_value { - bool_values { - } - } - } - } - args { - key: "key" - value { - arg_value { - string_value: "result" - } - } - } - qubits { - id: "5_2" - } - } - } -} -""", v2.program_pb2.Program()) program = cg.EngineProgram('a', 'b', EngineContext()) - get_program.return_value = qtypes.QuantumProgram( - code=_to_any(program_proto)) + get_program.return_value = qtypes.QuantumProgram(code=_PROGRAM_V2) assert program.get_circuit() == circuit get_program.assert_called_once_with('a', 'b', True) @@ -426,71 +504,8 @@ def test_get_circuit_batch(get_program): cirq.X(cirq.GridQubit(5, 2))**0.5, cirq.measure(cirq.GridQubit(5, 2), key='result')) - program_proto = Merge( - """programs { language { - gate_set: "xmon" -} -circuit { - scheduling_strategy: MOMENT_BY_MOMENT - moments { - operations { - gate { - id: "xy" - } - args { - key: "axis_half_turns" - value { - arg_value { - float_value: 0.0 - } - } - } - args { - key: "half_turns" - value { - arg_value { - float_value: 0.5 - } - } - } - qubits { - id: "5_2" - } - } - } - moments { - operations { - gate { - id: "meas" - } - args { - key: "invert_mask" - value { - arg_value { - bool_values { - } - } - } - } - args { - key: "key" - value { - arg_value { - string_value: "result" - } - } - } - qubits { - id: "5_2" - } - } - } -} -} -""", v2.batch_pb2.BatchProgram()) program = cg.EngineProgram('a', 'b', EngineContext()) - get_program.return_value = qtypes.QuantumProgram( - code=_to_any(program_proto)) + get_program.return_value = qtypes.QuantumProgram(code=_BATCH_PROGRAM_V2) with pytest.raises(ValueError, match='A program number must be specified'): program.get_circuit() with pytest.raises(ValueError, @@ -500,6 +515,41 @@ def test_get_circuit_batch(get_program): get_program.assert_called_once_with('a', 'b', True) +@mock.patch('cirq.google.engine.engine_client.EngineClient.get_program') +def test_get_batch_size(get_program): + # Has to fetch from engine if not _program specified. + program = cg.EngineProgram('a', + 'b', + EngineContext(), + result_type=ResultType.Batch) + get_program.return_value = qtypes.QuantumProgram(code=_BATCH_PROGRAM_V2) + assert program.batch_size() == 1 + + # If _program specified, uses that value. + program = cg.EngineProgram( + 'a', + 'b', + EngineContext(), + _program=qtypes.QuantumProgram(code=_BATCH_PROGRAM_V2), + result_type=ResultType.Batch) + assert program.batch_size() == 1 + + with pytest.raises(ValueError, match='ResultType.Program'): + program = cg.EngineProgram('a', + 'b', + EngineContext(), + result_type=ResultType.Program) + _ = program.batch_size() + + with pytest.raises(ValueError, match='cirq.google.api.v2.Program'): + get_program.return_value = qtypes.QuantumProgram(code=_PROGRAM_V2) + program = cg.EngineProgram('a', + 'b', + EngineContext(), + result_type=ResultType.Batch) + _ = program.batch_size() + + @pytest.fixture(scope='session', autouse=True) def mock_grpc_client(): with mock.patch('cirq.google.engine.engine_client' diff --git a/cirq/google/engine/engine_test.py b/cirq/google/engine/engine_test.py index d2dc8dc1ce4..65ca1f6c68d 100644 --- a/cirq/google/engine/engine_test.py +++ b/cirq/google/engine/engine_test.py @@ -610,6 +610,31 @@ def test_run_batch(client): client().get_job_results.assert_called_once() +@mock.patch('cirq.google.engine.engine_client.EngineClient') +def test_run_batch_no_params(client): + # OK to run with no params, it should use empty sweeps for each + # circuit. + setup_run_circuit_with_result_(client, _BATCH_RESULTS_V2) + engine = cg.Engine( + project_id='proj', + proto_version=cg.engine.engine.ProtoVersion.V2, + ) + engine.run_batch(programs=[_CIRCUIT, _CIRCUIT2], + gate_set=cg.XMON, + job_id='job-id', + processor_ids=['mysim']) + # Validate correct number of params have been created and that they + # are empty sweeps. + run_context = v2.batch_pb2.BatchRunContext() + client().create_job.call_args[1]['run_context'].Unpack(run_context) + assert len(run_context.run_contexts) == 2 + for rc in run_context.run_contexts: + sweeps = rc.parameter_sweeps + assert len(sweeps) == 1 + assert sweeps[0].repetitions == 1 + assert sweeps[0].sweep == v2.run_context_pb2.Sweep() + + def test_batch_size_validation_fails(): engine = cg.Engine( project_id='proj', From 5522950df003fc0accfc2aca53d3187a49cc4973 Mon Sep 17 00:00:00 2001 From: Dave Bacon Date: Sat, 21 Nov 2020 22:35:15 -0800 Subject: [PATCH 2/3] review comments --- cirq/google/engine/engine.py | 4 ++-- cirq/google/engine/engine_program.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cirq/google/engine/engine.py b/cirq/google/engine/engine.py index d548a8bd9c5..8374e0776bb 100644 --- a/cirq/google/engine/engine.py +++ b/cirq/google/engine/engine.py @@ -350,8 +350,8 @@ def run_batch( parameter sweep. """ if params_list is None: - params_list = [dict()] * len(programs) - if len(programs) != len(params_list): + params_list = [None] * len(programs) + elif len(programs) != len(params_list): raise ValueError('Number of circuits and sweeps must match') if not processor_ids: raise ValueError('Processor id must be specified.') diff --git a/cirq/google/engine/engine_program.py b/cirq/google/engine/engine_program.py index d5a816bb22d..fae73affc43 100644 --- a/cirq/google/engine/engine_program.py +++ b/cirq/google/engine/engine_program.py @@ -160,7 +160,7 @@ def run_batch( if self.result_type != ResultType.Batch: raise ValueError('Can only use run_batch() in batch mode.') if params_list is None: - params_list = [dict()] * self.batch_size() + params_list = [None] * self.batch_size() if not job_id: job_id = engine_base._make_random_id('job-') if not processor_ids: From f3adc72fbb5654ac5eedb24efc49f39804c8a8fa Mon Sep 17 00:00:00 2001 From: Dave Bacon Date: Mon, 23 Nov 2020 07:47:02 -0800 Subject: [PATCH 3/3] Remove print statement --- cirq/google/engine/engine_program_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cirq/google/engine/engine_program_test.py b/cirq/google/engine/engine_program_test.py index 3f34c4a0279..c047b2ce70b 100644 --- a/cirq/google/engine/engine_program_test.py +++ b/cirq/google/engine/engine_program_test.py @@ -227,7 +227,6 @@ def test_run_batch_no_sweeps(create_job): repetitions=10, processor_ids=['lazykitty']) assert job._job == qtypes.QuantumJob() - print(create_job.call_args[1]['run_context']) batch_run_context = v2.batch_pb2.BatchRunContext() create_job.call_args[1]['run_context'].Unpack(batch_run_context) assert len(batch_run_context.run_contexts) == 1