Repository: quantumlib/Cirq

Issue:
Circuits should support concatenation with +
I have several circuits and I want to concatenate them. Should this have a nice syntax? I came up with
```
E = cirq.Circuit.from_ops([op for circuit in [C, D] for moment in circuit for op in moment.operations])
```
where C and D are my old circuits and E is the new one. I would prefer `cirq.Circuit.concatenate(C, D)` or something like that.


Discussion:
You can just concatenate the moments, e.g. 
```python
E = cirq.Circuit(C.moments + D.moments)
```
Then I should be able to do
```
E = C + D
```
We can already slice Circuits, anyway.

Resolving patch:
diff --git a/cirq/circuits/circuit.py b/cirq/circuits/circuit.py
--- a/cirq/circuits/circuit.py
+++ b/cirq/circuits/circuit.py
@@ -55,7 +55,13 @@ class Circuit(object):
             ...
     and sliced,
         circuit[1:3] is a new Circuit made up of two moments, the first being
-            circuit[1] and the second being circuit[2].
+            circuit[1] and the second being circuit[2];
+    and concatenated,
+        circuit1 + circuit2 is a new Circuit made up of the moments in circuit1
+            followed by the moments in circuit2;
+    and multiplied by an integer,
+        circuit * k is a new Circuit made up of the moments in circuit repeated
+            k times.
 
     Attributes:
         moments: A list of the Moments of the circuit.
@@ -103,6 +109,33 @@ def __getitem__(self, key: Union[int, slice]) -> Union['Circuit', Moment]:
             raise TypeError(
                 '__getitem__ called with key not of type slice or int.')
 
+    def __iadd__(self, other):
+        if not isinstance(other, type(self)):
+            return NotImplemented
+        self.moments += other.moments
+        return self
+
+    def __add__(self, other):
+        if not isinstance(other, type(self)):
+            return NotImplemented
+        return Circuit(self.moments + other.moments)
+
+    def __imul__(self, repetitions: int):
+        if not isinstance(repetitions, int):
+            return NotImplemented
+        self.moments *= repetitions
+        return self
+
+    def __mul__(self, repetitions: int):
+        if not isinstance(repetitions, int):
+            return NotImplemented
+        return Circuit(self.moments * repetitions)
+
+    def __rmul__(self, repetitions: int):
+        if not isinstance(repetitions, int):
+            return NotImplemented
+        return self * repetitions
+
     def __len__(self):
         return len(self.moments)
 


Test patch:
diff --git a/cirq/circuits/circuit_test.py b/cirq/circuits/circuit_test.py
--- a/cirq/circuits/circuit_test.py
+++ b/cirq/circuits/circuit_test.py
@@ -119,6 +119,68 @@ def test_slice():
     assert c[0:2:-1] == Circuit()
 
 
+def test_concatenate():
+    a = ops.QubitId()
+    b = ops.QubitId()
+
+    c = Circuit()
+    d = Circuit([Moment([ops.X(b)])])
+    e = Circuit([Moment([ops.X(a), ops.X(b)])])
+
+    assert c + d == Circuit([Moment([ops.X(b)])])
+    assert d + c == Circuit([Moment([ops.X(b)])])
+    assert e + d == Circuit([
+        Moment([ops.X(a), ops.X(b)]),
+        Moment([ops.X(b)])
+    ])
+
+    d += c
+    assert d == Circuit([Moment([ops.X(b)])])
+
+    c += d
+    assert c == Circuit([Moment([ops.X(b)])])
+
+    f = e + d
+    f += e
+    assert f == Circuit([
+        Moment([ops.X(a), ops.X(b)]),
+        Moment([ops.X(b)]),
+        Moment([ops.X(a), ops.X(b)])
+    ])
+
+    with pytest.raises(TypeError):
+        _ = c + 'a'
+    with pytest.raises(TypeError):
+        c += 'a'
+
+
+def test_multiply():
+    a = ops.QubitId()
+
+    c = Circuit()
+    d = Circuit([Moment([ops.X(a)])])
+
+    assert c * 0 == Circuit()
+    assert d * 0 == Circuit()
+    assert d * 2 == Circuit([Moment([ops.X(a)]),
+                             Moment([ops.X(a)])])
+    assert 1 * c == Circuit()
+    assert -1 * d == Circuit()
+    assert 1 * d == Circuit([Moment([ops.X(a)])])
+
+    d *= 3
+    assert d == Circuit([Moment([ops.X(a)]),
+                         Moment([ops.X(a)]),
+                         Moment([ops.X(a)])])
+
+    with pytest.raises(TypeError):
+        _ = c * 'a'
+    with pytest.raises(TypeError):
+        _ = 'a' * c
+    with pytest.raises(TypeError):
+        c *= 'a'
+
+
 def test_container_methods():
     a = ops.QubitId()
     b = ops.QubitId()