Repository: quantumlib/Cirq

Issue:
"Number of circuits and sweeps must match" -- run_batch
I have a list of non-parameterized, non-sweepy circuits and thought I could just not pass `params_list` but it insists that it be of equal length to the number of circuits. 

1. the default value shouldn't be `None` if that doesn't actually work (?)
2. can I pass a list of `None`s? can we make this happen automatically?


Discussion:
I agree that users shouldn't be forced to pass parameters. It should just work.

Resolving patch:
diff --git a/cirq/google/engine/engine.py b/cirq/google/engine/engine.py
--- 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 = [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
--- 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 = [None] * 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':


Test patch:
diff --git a/cirq/google/engine/engine_program_test.py b/cirq/google/engine/engine_program_test.py
--- 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,24 @@ 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()
+    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 +257,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 +491,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 +503,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 +514,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
--- 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',