Repository: XanaduAI/strawberryfields

Issue:
Invalid Blackbird circuit is generated from a Program
#### Issue description

Running a `Program` loaded from a Blackbird circuit which was generated by another `Program` does not always work.  For example, consider the "Teleportatation" example from Strawberry Fields Interactive:
```python
import math
import strawberryfields as sf
from strawberryfields.ops import *
​
sf_prog = sf.Program(3)
​
# See the "Teleportation" algorithm in https://strawberryfields.ai/interactive/.
with sf_prog.context as q:
    Coherent(math.sqrt(5/4), math.atan(1/2)) | q[0]
    Sgate(2, 0) | q[2]
    Sgate(-2, 0) | q[1]
    BSgate(0.7854, 0) | (q[1], q[2])
    BSgate(0.7854, 0) | (q[0], q[1])
    MeasureP | q[1]
    MeasureX | q[0]
    Xgate(q[0].par*1.41421356237000) | q[2]
    Zgate(q[1].par*1.41421356237000) | q[2]

# Convert the SF program ​into a Blackbird program.
bb_prog = sf.io.to_blackbird(sf_prog)
bb_prog._name = "Teleportation"
bb_prog._version = 1.0
bb_prog._target["name"] = "gaussian"

# Recover the SF program from the Blackbird program.
bb_circuit = bb_prog.serialize()​
re_prog  = sf.io.loads(bb_circuit)

# Run the recovered SF program with the Gaussian engine.
eng = sf.LocalEngine("gaussian")
results = eng.run(re_prog)
```

* *Expected behavior:* A `Results` object is returned by the final line of the script.

* *Actual behavior:* A `TypeError` exception is raised.

* *Reproduces how often:* Every time.

* *System information:*
```
Strawberry Fields: a Python library for continuous-variable quantum circuits.
Copyright 2018-2020 Xanadu Quantum Technologies Inc.

Python version:            3.9.5
Platform info:             Linux-5.4.72-microsoft-standard-WSL2-x86_64-with-glibc2.31
Installation path:         /home/mikhail/.local/lib/python3.9/site-packages/strawberryfields
Strawberry Fields version: 0.18.0
Numpy version:             1.20.3
Scipy version:             1.6.3
SymPy version:             1.8
NetworkX version:          2.5.1
The Walrus version:        0.15.1
Blackbird version:         0.3.0
TensorFlow version:        None
```

#### Source code and tracebacks

Running the script above yields the following error:

```python
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-16-da60180d5e6d> in <module>
     28 # Run the recovered SF program using the Gaussian backend.
     29 eng = sf.LocalEngine("gaussian")
---> 30 results = eng.run(re_prog)

~/.local/lib/python3.9/site-packages/strawberryfields/engine.py in run(self, program, args, compile_options, **kwargs)
    492                     )
    493 
--> 494         result = super()._run(
    495             program, args=args, compile_options=compile_options, **eng_run_options
    496         )

~/.local/lib/python3.9/site-packages/strawberryfields/engine.py in _run(self, program, args, compile_options, **kwargs)
    272             target = self.backend.compiler
    273             if target is not None:
--> 274                 p = p.compile(compiler=target, **compile_options)
    275             p.lock()
    276 

~/.local/lib/python3.9/site-packages/strawberryfields/program.py in compile(self, device, compiler, **kwargs)
    610             target = compiler.short_name
    611 
--> 612         seq = compiler.decompose(self.circuit)
    613 
    614         if kwargs.get("warn_connected", True):

~/.local/lib/python3.9/site-packages/strawberryfields/compilers/compiler.py in decompose(self, seq)
    222                 try:
    223                     kwargs = self.decompositions[op_name]
--> 224                     temp = cmd.op.decompose(cmd.reg, **kwargs)
    225                     # now compile the decomposition
    226                     temp = self.decompose(temp)

~/.local/lib/python3.9/site-packages/strawberryfields/ops.py in decompose(self, reg, **kwargs)
    450         Like :func:`Operation.decompose`, but applies self.dagger.
    451         """
--> 452         seq = self._decompose(reg, **kwargs)
    453         if self.dagger:
    454             # apply daggers, reverse the Command sequence

~/.local/lib/python3.9/site-packages/strawberryfields/ops.py in _decompose(self, reg, **kwargs)
   1500     def _decompose(self, reg, **kwargs):
   1501         # into a displacement
-> 1502         r = self.p[0] / np.sqrt(2 * sf.hbar)
   1503         return [Command(Dgate(r, 0), reg)]
   1504 

TypeError: ufunc 'true_divide' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''
```

#### Additional information

The issue appears to have something to do with the final two lines of the generated Blackbird circuit:
```python
name Teleportation
version 1.0
target gaussian

Coherent(1.118033988749895, 0.4636476090008061) | 0
Sgate(2, 0) | 2
Sgate(-2, 0) | 1
BSgate(0.7854, 0) | [1, 2]
BSgate(0.7854, 0) | [0, 1]
MeasureHomodyne(phi=1.5707963267948966) | 1
MeasureHomodyne(phi=0) | 0
Xgate("1.41421356237*q0") | 2
Zgate("1.41421356237*q1") | 2
```



Discussion:
Do you mind looking into this, @thisac ?

Resolving patch:
diff --git a/strawberryfields/io.py b/strawberryfields/io.py
--- a/strawberryfields/io.py
+++ b/strawberryfields/io.py
@@ -46,6 +46,8 @@ def to_blackbird(prog, version="1.0"):
     bb = blackbird.BlackbirdProgram(name=prog.name, version=version)
     bb._modes = list(prog.reg_refs.keys())
 
+    isMeasuredParameter = lambda x: isinstance(x, sfpar.MeasuredParameter)
+
     # TODO not sure if this makes sense: the program has *already been* compiled using this target
     if prog.target is not None:
         # set the target
@@ -83,7 +85,11 @@ def to_blackbird(prog, version="1.0"):
             for a in cmd.op.p:
                 if sfpar.par_is_symbolic(a):
                     # SymPy object, convert to string
-                    a = str(a)
+                    if any(map(isMeasuredParameter, a.free_symbols)):
+                        # check if there are any measured parameters in `a`
+                        a = blackbird.RegRefTransform(a)
+                    else:
+                        a = str(a)
                 op["args"].append(a)
 
         # If program type is "tdm" then add the looped-over arrays to the
diff --git a/strawberryfields/parameters.py b/strawberryfields/parameters.py
--- a/strawberryfields/parameters.py
+++ b/strawberryfields/parameters.py
@@ -97,6 +97,7 @@
 import functools
 import types
 import linecache
+import blackbird
 
 import numpy as np
 import sympy
@@ -257,6 +258,8 @@ def par_convert(args, prog):
     """
 
     def do_convert(a):
+        if isinstance(a, blackbird.RegRefTransform):
+            a = a.expr
         if isinstance(a, sympy.Basic):
             # substitute SF symbolic parameter objects for Blackbird ones
             s = {}


Test patch:
diff --git a/tests/frontend/test_io.py b/tests/frontend/test_io.py
--- a/tests/frontend/test_io.py
+++ b/tests/frontend/test_io.py
@@ -322,8 +322,15 @@ def test_measured_par_str(self):
             ops.Zgate(2 * pf.sin(q[0].par)) | q[1]
 
         bb = io.to_blackbird(prog)
-        expected = {"op": "Zgate", "modes": [1], "args": ["2*sin(q0)"], "kwargs": {}}
-        assert bb.operations[-1] == expected
+        assert bb.operations[-1]["op"] == "Zgate"
+        assert bb.operations[-1]["modes"] == [1]
+
+        assert isinstance(bb.operations[-1]["args"][0], blackbird.RegRefTransform)
+        assert bb.operations[-1]["args"][0].func_str == "2*sin(q0)"
+        assert bb.operations[-1]["args"][0].regrefs == [0]
+
+        assert bb.operations[-1]["kwargs"] == {}
+
 
     def test_free_par_str(self):
         """Test a FreeParameter with some transformations converts properly"""
@@ -460,7 +467,6 @@ def test_gate_kwarg(self):
         assert prog.circuit[0].op.p[0] == 0.54
         assert prog.circuit[0].reg[0].ind == 0
 
-    @pytest.mark.skip("FIXME enable when blackbird.program.RegRefTransform is replaced with sympy.Symbol.")
     def test_gate_measured_par(self):
         """Test a gate with a MeasuredParameter argument."""