Repository: rigetti/pyquil

Issue:
Adding two `Program`s together unexpectedly mutates first `Program`
Pre-Report Checklist
--------------------

- [x] I am running the latest versions of pyQuil and the Forest SDK
- [x] I checked to make sure that this bug has not already been reported

Issue Description
-----------------

Summary: when adding two Programs together, like p1 + p2, the first program p1 gets mutated — p1.calibrations will as a result contain the combined calibrations. But I would have expected both p1 and p2 to remain unchanged.

I believe the reason for issue is that in the source code, it uses `p.calibrations.extend(other.calibrations)`
which mutates the original’s list
. https://github.com/rigetti/pyquil/blob/master/pyquil/quil.py#L882

How to Reproduce
----------------
### Code Snippet

```python
print("@@@ p32 before adding")
print(p32)

print("@@@ p33 before adding")
print(p33)

p_all = p32 + p33

print("@@@ p32 after adding <-- here is the unexpected behavior")
print(p32)

print("@@@ p33 after adding")
print(p33)

print("@@@ p_all after adding")
print(p_all)
```

### Error Output

```
@@@ p32 before adding
DEFCAL RX(pi/2) 32:
    FENCE 32
    NONBLOCKING PULSE 32 "rf" drag_gaussian(duration: 3.2e-08, fwhm: 8e-09, t0: 1.6e-08, anh: -190000000.0, alpha: -1.8848698349348032, scale: 0.30631340170943533, phase: 0.0, detuning: 1622438.2425563578)
    FENCE 32

RX(pi/2) 32

@@@ p33 before adding
DEFCAL RX(pi/2) 33:
    FENCE 33
    NONBLOCKING PULSE 33 "rf" drag_gaussian(duration: 2e-08, fwhm: 5e-09, t0: 1e-08, anh: -190000000.0, alpha: -0.9473497322033984, scale: 0.25680107985232403, phase: 0.0, detuning: 1322130.5458282642)
    FENCE 33

RX(pi/2) 33

@@@ p32 after adding <-- here is the unexpected behavior
DEFCAL RX(pi/2) 32:
    FENCE 32
    NONBLOCKING PULSE 32 "rf" drag_gaussian(duration: 3.2e-08, fwhm: 8e-09, t0: 1.6e-08, anh: -190000000.0, alpha: -1.8848698349348032, scale: 0.30631340170943533, phase: 0.0, detuning: 1622438.2425563578)
    FENCE 32

DEFCAL RX(pi/2) 33:
    FENCE 33
    NONBLOCKING PULSE 33 "rf" drag_gaussian(duration: 2e-08, fwhm: 5e-09, t0: 1e-08, anh: -190000000.0, alpha: -0.9473497322033984, scale: 0.25680107985232403, phase: 0.0, detuning: 1322130.5458282642)
    FENCE 33

RX(pi/2) 32

@@@ p33 after adding
DEFCAL RX(pi/2) 33:
    FENCE 33
    NONBLOCKING PULSE 33 "rf" drag_gaussian(duration: 2e-08, fwhm: 5e-09, t0: 1e-08, anh: -190000000.0, alpha: -0.9473497322033984, scale: 0.25680107985232403, phase: 0.0, detuning: 1322130.5458282642)
    FENCE 33

RX(pi/2) 33

@@@ p_all after adding
DEFCAL RX(pi/2) 32:
    FENCE 32
    NONBLOCKING PULSE 32 "rf" drag_gaussian(duration: 3.2e-08, fwhm: 8e-09, t0: 1.6e-08, anh: -190000000.0, alpha: -1.8848698349348032, scale: 0.30631340170943533, phase: 0.0, detuning: 1622438.2425563578)
    FENCE 32

DEFCAL RX(pi/2) 33:
    FENCE 33
    NONBLOCKING PULSE 33 "rf" drag_gaussian(duration: 2e-08, fwhm: 5e-09, t0: 1e-08, anh: -190000000.0, alpha: -0.9473497322033984, scale: 0.25680107985232403, phase: 0.0, detuning: 1322130.5458282642)
    FENCE 33

RX(pi/2) 32
RX(pi/2) 33
```

Environment Context
-------------------

Operating System:  macOS Monterey

Python Version (`python -V`):  3.9.13

Quilc Version (`quilc --version`): N/A

QVM Version (`qvm --version`): N/A

Python Environment Details (`pip freeze` or `conda list`): 

```
Package                       Version
----------------------------- -----------
aiohttp                       3.8.1
aiohttp-retry                 2.4.6
aiosignal                     1.2.0
alabaster                     0.7.12
ansiwrap                      0.8.4
anyio                         3.6.1
appdirs                       1.4.4
argon2-cffi                   21.3.0
argon2-cffi-bindings          21.2.0
asteval                       0.9.26
asttokens                     2.0.5
async-timeout                 4.0.2
asyncssh                      2.10.1
atpublic                      3.0.1
attrs                         20.3.0
Babel                         2.10.1
backcall                      0.2.0
beautifulsoup4                4.11.1
benchmark-quantum-gates       0.5.0
bitarray                      2.5.1
black                         22.3.0
bleach                        5.0.0
cachetools                    5.0.0
certifi                       2021.10.8
cffi                          1.15.0
charset-normalizer            2.0.12
click                         8.1.3
colorama                      0.4.4
commonmark                    0.9.1
configobj                     5.0.6
coverage                      6.3.3
cryptography                  37.0.2
cvxopt                        1.3.0
cvxpy                         1.2.0
cycler                        0.11.0
debugpy                       1.6.3
decorator                     5.1.1
defusedxml                    0.7.1
dictdiffer                    0.9.0
diskcache                     5.4.0
distro                        1.7.0
docutils                      0.17.1
dpath                         2.0.6
dulwich                       0.20.35
dvc                           2.10.2
dvc-render                    0.0.5
dvclive                       0.8.0
ecos                          2.0.10
entrypoints                   0.4
executing                     0.8.3
fastjsonschema                2.15.3
flake8                        4.0.1
flake8-black                  0.3.2
flake8-docstrings             1.6.0
flatten-dict                  0.4.2
flufl.lock                    7.0
fonttools                     4.33.3
forest-benchmarking           0.8.0
frozenlist                    1.3.0
fsspec                        2022.3.0
ftfy                          6.1.1
funcy                         1.17
future                        0.18.2
future-fstrings               1.2.0
gitdb                         4.0.9
GitPython                     3.1.27
google-api-core               2.7.3
google-api-python-client      2.47.0
google-auth                   2.6.6
google-auth-httplib2          0.1.0
googleapis-common-protos      1.56.1
gprof2dot                     2021.2.21
grandalf                      0.6
h11                           0.9.0
httpcore                      0.11.1
httplib2                      0.20.4
httpx                         0.15.5
idna                          3.3
imagesize                     1.3.0
importlib-metadata            4.11.3
iniconfig                     1.1.1
ipykernel                     6.13.0
ipympl                        0.9.2
ipython                       8.3.0
ipython-genutils              0.2.0
ipywidgets                    7.7.0
iso8601                       0.1.16
isort                         5.10.1
jedi                          0.18.1
Jinja2                        3.1.2
joblib                        1.1.0
json5                         0.9.8
jsonschema                    4.5.1
jupyter-client                7.3.1
jupyter-core                  4.10.0
jupyter-lsp                   1.5.1
jupyter-server                1.17.0
jupyter-server-mathjax        0.2.5
jupyterlab                    3.4.2
jupyterlab-git                0.34.2
jupyterlab-lsp                3.10.1
jupyterlab-pygments           0.2.2
jupyterlab-server             2.13.0
jupyterlab-widgets            1.1.0
kaleido                       0.2.1
kiwisolver                    1.4.2
lark                          0.11.3
lmfit                         1.0.3
mailchecker                   4.1.16
Mako                          1.2.1
MarkupSafe                    2.1.1
matplotlib                    3.5.2
matplotlib-inline             0.1.3
mccabe                        0.6.1
mistune                       0.8.4
mpmath                        1.2.1
msgpack                       0.6.2
multidict                     6.0.2
mypy-extensions               0.4.3
nanotime                      0.5.2
nbclassic                     0.3.7
nbclient                      0.6.3
nbconvert                     6.5.0
nbdime                        3.1.1
nbformat                      5.4.0
nest-asyncio                  1.5.5
networkx                      2.8
notebook                      6.4.11
notebook-shim                 0.1.0
numexpr                       2.8.1
numpy                         1.21.0
oauth2client                  4.1.3
osqp                          0.6.2.post5
packaging                     21.3
pandas                        1.4.2
pandocfilters                 1.5.0
papermill                     2.3.4
parso                         0.8.3
pathspec                      0.9.0
patsy                         0.5.2
pexpect                       4.8.0
phonenumbers                  8.12.48
pickleshare                   0.7.5
Pillow                        9.1.0
pip                           22.2.2
platformdirs                  2.5.2
plotly                        5.8.0
pluggy                        1.0.0
prometheus-client             0.14.1
prompt-toolkit                3.0.29
protobuf                      3.20.1
psutil                        5.9.0
ptyprocess                    0.7.0
pure-eval                     0.2.2
py                            1.11.0
pyaml                         21.10.1
pyarrow                       5.0.0
pyasn1                        0.4.8
pyasn1-modules                0.2.8
pycodestyle                   2.8.0
pycparser                     2.21
pydantic                      1.9.0
pydocstyle                    6.1.1
pydot                         1.4.2
PyDrive2                      1.10.1
pyflakes                      2.4.0
pygit2                        1.9.1
Pygments                      2.12.0
pygtrie                       2.4.2
PyJWT                         1.7.1
pyOpenSSL                     22.0.0
pyparsing                     3.0.9
pyquil                        3.1.0
pyrsistent                    0.18.1
pytest                        7.1.2
pytest-cov                    3.0.0
pytest-depends                1.0.1
pytest-profiling              1.7.0
python-benedict               0.25.1
python-dateutil               2.8.2
python-fsutil                 0.6.0
python-rapidjson              1.6
python-slugify                6.1.2
pytz                          2022.1
PyYAML                        6.0
pyzmq                         22.3.0
qcs-api-client                0.20.13
qdldl                         0.1.5.post2
qpu-hybrid-benchmark-trueq    0.5.8
qutip                         4.7.0
qutip-qip                     0.2.1
requests                      2.27.1
retry                         0.9.2
retrying                      1.3.3
rfc3339                       6.2
rfc3986                       1.5.0
rich                          12.4.1
rigetti-qpu-hybrid-benchmark  0.5.24
rpcq                          3.10.0
rsa                           4.8
ruamel.yaml                   0.17.21
ruamel.yaml.clib              0.2.6
scikit-learn                  1.1.0
scikit-optimize               0.9.0
scipy                         1.8.0
scmrepo                       0.0.19
scs                           3.2.0
seaborn                       0.11.2
Send2Trash                    1.8.0
setuptools                    63.4.1
setuptools-scm                6.4.2
shortuuid                     1.0.9
shtab                         1.5.4
six                           1.16.0
smmap                         5.0.0
sniffio                       1.2.0
snowballstemmer               2.2.0
soupsieve                     2.3.2.post1
Sphinx                        4.5.0
sphinx-autodoc-typehints      1.18.1
sphinx-rtd-theme              0.4.3
sphinxcontrib-applehelp       1.0.2
sphinxcontrib-devhelp         1.0.2
sphinxcontrib-htmlhelp        2.0.0
sphinxcontrib-jsmath          1.0.1
sphinxcontrib-qthelp          1.0.3
sphinxcontrib-serializinghtml 1.1.5
stack-data                    0.2.0
statsmodels                   0.13.2
sympy                         1.10.1
tables                        3.7.0
tabulate                      0.8.9
tenacity                      8.0.1
terminado                     0.13.3
text-unidecode                1.3
textwrap3                     0.9.2
threadpoolctl                 3.1.0
tinycss2                      1.1.1
tokenize-rt                   4.2.1
toml                          0.10.2
tomli                         2.0.1
tornado                       6.1
tqdm                          4.64.0
traitlets                     5.2.0
trueq                         2.13.1
typing_extensions             4.2.0
uncertainties                 3.1.6
uritemplate                   4.1.1
urllib3                       1.26.9
voluptuous                    0.13.1
wcwidth                       0.2.5
webencodings                  0.5.1
websocket-client              1.3.2
wheel                         0.37.1
widgetsnbextension            3.6.0
xmltodict                     0.13.0
yarl                          1.7.2
zc.lockfile                   2.0
zipp                          3.8.0
```


Discussion:

Patch:
diff --git a/pyquil/quil.py b/pyquil/quil.py
index 2fabf86..15fd7b5 100644
--- a/pyquil/quil.py
+++ b/pyquil/quil.py
@@ -874,9 +874,9 @@ class Program:
         p = Program()
         p.inst(self)
         p.inst(other)
-        p._calibrations = self.calibrations
-        p._waveforms = self.waveforms
-        p._frames = self.frames
+        p._calibrations = self.calibrations.copy()
+        p._waveforms = self.waveforms.copy()
+        p._frames = self.frames.copy()
         p._memory = self._memory.copy()
         if isinstance(other, Program):
             p.calibrations.extend(other.calibrations)


Test patch:
diff --git a/test/unit/test_program.py b/test/unit/test_program.py
index b4a4747..06d2bab 100644
--- a/test/unit/test_program.py
+++ b/test/unit/test_program.py
@@ -56,3 +56,31 @@ def test_parameterized_readout_symmetrization():
     p += RX(symmetrization[0], 0)
     p += RX(symmetrization[1], 1)
     assert parameterized_readout_symmetrization([0, 1]).out() == p.out()
+
+
+def test_adding_does_not_mutate():
+    # https://github.com/rigetti/pyquil/issues/1476
+    p1 = Program(
+        """
+DEFCAL RX(pi/2) 32:
+    FENCE 32
+    NONBLOCKING PULSE 32 "rf" drag_gaussian(duration: 3.2e-08, fwhm: 8e-09, t0: 1.6e-08, anh: -190000000.0, alpha: -1.8848698349348032, scale: 0.30631340170943533, phase: 0.0, detuning: 1622438.2425563578)
+    FENCE 32
+
+RX(pi/2) 32
+"""
+    )
+    original_p1 = p1.copy()
+    p2 = Program(
+        """
+DEFCAL RX(pi/2) 33:
+    FENCE 33
+    NONBLOCKING PULSE 33 "rf" drag_gaussian(duration: 2e-08, fwhm: 5e-09, t0: 1e-08, anh: -190000000.0, alpha: -0.9473497322033984, scale: 0.25680107985232403, phase: 0.0, detuning: 1322130.5458282642)
+    FENCE 33
+
+RX(pi/2) 33
+"""
+    )
+    p_all = p1 + p2
+    assert p1 == original_p1
+    assert p1.calibrations != p_all.calibrations