def test_heterodyne(self, setup_eng, tol):
        """Test that heterodyne detection on a TMS state
        returns post-selected value."""
        alpha = 0.43 - 0.12j
        r = 5

        eng, prog = setup_eng(2)

        with prog.context as q:
            ops.S2gate(r) | q
            ops.MeasureHeterodyne(select=alpha) | q[0]

        eng.run(prog)
        assert np.allclose(q[0].val, alpha, atol=tol, rtol=0)
예제 #2
0
    def test_has_feed_forward(self):
        """Check that the ``has_feed_forward`` property behaves as expected when it uses
        feed-forwarding or not.
        """
        # instantiate two programs for testing
        prog_1, prog_2 = sf.Program(2), sf.Program(2)

        # program with feed-forwarding
        with prog_1.context as q:
            ops.Sgate(0.54) | q[1]
            ops.BSgate(0.42, 0.1) | (q[0], q[1])
            ops.Sgate(q[0].par) | q[1]
            ops.MeasureHeterodyne() | q[1]

        # program without feed-forwarding
        with prog_2.context as q:
            ops.Sgate(0.54) | q[1]
            ops.BSgate(0.42, 0.1) | (q[0], q[1])
            ops.MeasureHomodyne(phi=np.pi) | q[1]
        assert prog_1.has_feed_forward
        assert prog_1.has_post_selection is False
        assert prog_2.has_feed_forward is False
        assert prog_2.has_post_selection is False
예제 #3
0
class TestProgram:
    """Tests the Program class."""

    def test_with_block(self, prog):
        """Gate application using a with block."""
        # command queue is empty
        assert len(prog) == 0

        with prog.context as q:
            ops.Dgate(0.5, 0.0) | q[0]
        # now there is one gate in the queue
        assert len(prog) == 1

        with prog.context as q:
            ops.BSgate(0.5, 0.3) | (q[1], q[0])
        assert len(prog) == 2

    def test_parent_program(self):
        """Continuing one program with another."""
        D = ops.Dgate(0.5, 0.0)
        prog = sf.Program(3)
        with prog.context as q:
            D | q[1]
            ops.Del | q[0]
        cont = sf.Program(prog)
        with cont.context as q:
            D | q[0]
            r = ops.New(1)
            D | r
        assert cont.can_follow(prog)
        assert prog.reg_refs == cont.init_reg_refs
        assert prog.unused_indices == cont.init_unused_indices

    def test_print_commands(self, eng, prog):
        """Program.print and Engine.print_applied return correct strings."""
        prog = sf.Program(2)

        # store the result of the print command in list res
        res = []
        # use a print function that simply appends the operation
        # name to the results list
        print_fn = lambda x: res.append(x.__str__())

        # prog should now be empty
        prog.print(print_fn)
        assert res == []

        # define some gates
        D = ops.Dgate(0.5, 0.0)
        BS = ops.BSgate(2 * np.pi, np.pi / 2)
        R = ops.Rgate(np.pi)

        with prog.context as q:
            alice, bob = q
            D | alice
            BS | (alice, bob)
            ops.Del | alice
            R | bob
            charlie, = ops.New(1)
            BS | (bob, charlie)
            ops.MeasureX | bob
            ops.Dgate(bob.par).H | charlie
            ops.Del | bob
            ops.MeasureX | charlie

        res = []
        prog.print(print_fn)

        expected = [
            "Dgate(0.5, 0) | (q[0])",
            "BSgate(6.283, 1.571) | (q[0], q[1])",
            "Del | (q[0])",
            "Rgate(3.142) | (q[1])",
            "New(1)",
            "BSgate(6.283, 1.571) | (q[1], q[2])",
            "MeasureX | (q[1])",
            "Dgate(q1, 0).H | (q[2])",
            "Del | (q[1])",
            "MeasureX | (q[2])",
        ]

        assert res == expected

        # NOTE optimization can change gate order
        result = eng.run(prog, compile_options={'optimize': False})
        res = []
        eng.print_applied(print_fn)
        assert res == ["Run 0:"] + expected

    def test_params(self, prog):
        """Creating and retrieving free parameters."""
        assert not prog.free_params  # no free params to start with

        with pytest.raises(TypeError, match='Parameter names must be strings.'):
            prog.params(1)

        # creating
        x = prog.params('a')
        assert isinstance(x, FreeParameter)
        assert x.name == 'a'
        assert len(prog.free_params) == 1

        # retrieving
        y = prog.params('a')
        assert y is x
        assert len(prog.free_params) == 1

        with pytest.raises(TypeError, match='Parameter names must be strings.'):
            prog.params(x)

        # creating/retrieving multiple
        names = ('foo', 'bar', 'a')
        pars = prog.params(*names)
        assert isinstance(pars, list)
        for n, p in zip(names, pars):
            assert isinstance(p, FreeParameter)
            assert p.name == n
        assert pars[2] is x  # still the same parameter
        assert len(prog.free_params) == 3

        # once the program is locked only retrieval is possible
        prog.lock()
        with pytest.raises(program.CircuitError, match='The Program is locked, no more free parameters can be created.'):
            w = prog.params('www')
        w = prog.params('a')
        assert w is x

    def test_bind_params(self, prog):
        """Binding free parameters."""

        with pytest.raises(ParameterError, match='Unknown free parameter'):
            prog.bind_params({'x': 0})

        x, y = prog.params('x', 'y')
        # bind some params using parameter name
        prog.bind_params({'x': 1.0})
        assert x.val == 1.0
        assert y.val is None
        # bind using the parameter itself
        prog.bind_params({x: 2.0})
        assert x.val == 2.0
        assert y.val is None

    def test_assert_number_of_modes(self):
        """Check that the correct error is raised when calling `prog.assert_number_of_modes`
        with the incorrect number of modes."""
        device_dict = {"modes": 2, "layout": None, "gate_parameters": None, "compiler": [None]}
        spec = sf.api.DeviceSpec(target=None, connection=None, spec=device_dict)

        prog = sf.Program(3)
        with prog.context as q:
            ops.S2gate(0.6) | [q[0], q[1]]
            ops.S2gate(0.6) | [q[1], q[2]]

        with pytest.raises(program.CircuitError, match="program contains 3 modes, but the device 'None' only supports a 2-mode program"):
            prog.assert_number_of_modes(spec)

    @pytest.mark.parametrize(
        "measure_op, measure_name", [
            (ops.MeasureFock(), "fock"),  # MeasureFock
            (ops.MeasureHomodyne(phi=0), "homodyne"),  # MeasureX
            (ops.MeasureHomodyne(phi=42), "homodyne"),  # MeasureHomodyne
            (ops.MeasureHomodyne(phi=np.pi/2), "homodyne"),  # MeasureP
            (ops.MeasureHeterodyne(), "heterodyne"),  # MeasureHD
            (ops.MeasureHeterodyne(select=0), "heterodyne"),  # MeasureHeterodyne
        ],
    )
    def test_assert_max_number_of_measurements(self, measure_op, measure_name):
        """Check that the correct error is raised when calling `prog.assert_number_of_measurements`
        with the incorrect number of measurements in the circuit."""
        # set maximum number of measurements to 2, and measure 3 in prog below
        device_dict = {
            "modes": {
                "max": {
                    "pnr": 2,
                    "homodyne": 2,
                    "heterodyne": 2
                }
            },
            "layout": None, "gate_parameters": {}, "compiler": [None]
        }
        spec = sf.api.DeviceSpec(target="simulon", connection=None, spec=device_dict)

        prog = sf.Program(3)
        with prog.context as q:
            for reg in q:
                measure_op | reg

        with pytest.raises(
            program.CircuitError, match=f"contains 3 {measure_name} measurements"
        ):
            prog.assert_max_number_of_measurements(spec)

    def test_assert_max_number_of_measurements_wrong_entry(self):
        """Check that the correct error is raised when calling `prog.assert_number_of_measurements`
        with the incorrect type of device spec mode entry."""
        device_dict = {"modes": 2, "layout": None, "gate_parameters": None, "compiler": [None]}
        spec = sf.api.DeviceSpec(target="simulon", connection=None, spec=device_dict)

        prog = sf.Program(3)
        with prog.context as q:
            ops.S2gate(0.6) | [q[0], q[1]]
            ops.S2gate(0.6) | [q[1], q[2]]

        with pytest.raises(KeyError, match="Have you specified the correct target?"):
            prog.assert_max_number_of_measurements(spec)
예제 #4
0
class TestValidation:
    """Test for Program circuit validation within
    the compile() method."""

    def test_unknown_circuit_spec(self):
        """Test an unknown compile target."""
        prog = sf.Program(3)
        with prog.context as q:
            ops.MeasureFock() | q

        with pytest.raises(ValueError, match="Unknown compiler 'foo'"):
            new_prog = prog.compile(compiler='foo')

    def test_disconnected_circuit(self):
        """Test the detection of a disconnected circuit."""
        prog = sf.Program(3)
        with prog.context as q:
            ops.S2gate(0.6) | q[0:2]
            ops.Dgate(1.0, 0.0)  | q[2]
            ops.MeasureFock() | q[0:2]
            ops.MeasureX | q[2]

        with pytest.warns(UserWarning, match='The circuit consists of 2 disconnected components.'):
            new_prog = prog.compile(compiler='fock')

    # TODO: move this test into an integration tests folder (a similar test for the
    # `prog.assert_number_of_modes` method can be found above), under `test_assert_number_of_modes`.
    def test_incorrect_modes(self):
        """Test that an exception is raised if the compiler
        is called with a device spec with an incorrect number of modes"""

        class DummyCompiler(Compiler):
            """A circuit with 2 modes"""
            interactive = True
            primitives = {'S2gate', 'Interferometer'}
            decompositions = set()

        device_dict = {"modes": 2, "layout": None, "gate_parameters": None, "compiler": [None]}
        spec = sf.api.DeviceSpec(target=None, connection=None, spec=device_dict)

        prog = sf.Program(3)
        with prog.context as q:
            ops.S2gate(0.6) | [q[0], q[1]]
            ops.S2gate(0.6) | [q[1], q[2]]

        with pytest.raises(program.CircuitError, match="program contains 3 modes, but the device 'None' only supports a 2-mode program"):
            new_prog = prog.compile(device=spec, compiler=DummyCompiler())

    # TODO: move this test into an integration tests folder (a similar test for the
    # `prog.assert_number_of_measurements` method can be found above), named `test_assert_number_of_measurements`.
    @pytest.mark.parametrize(
        "measure_op, measure_name", [
            (ops.MeasureFock(), "fock"),  # MeasureFock
            (ops.MeasureHomodyne(phi=0), "homodyne"),  # MeasureX
            (ops.MeasureHomodyne(phi=42), "homodyne"),  # MeasureHomodyne
            (ops.MeasureHomodyne(phi=np.pi/2), "homodyne"),  # MeasureP
            (ops.MeasureHeterodyne(), "heterodyne"),  # MeasureHD
            (ops.MeasureHeterodyne(select=0), "heterodyne"),  # MeasureHeterodyne
        ],
    )
    def test_incorrect_number_of_measurements(self, measure_op, measure_name):
        """Test that an exception is raised if the compiler is called with a
        device spec with an incorrect number of measurements"""

        class DummyCompiler(Compiler):
            """A circuit with 2 modes"""
            interactive = True
            primitives = {'MeasureHomodyne', 'MeasureHeterodyne', 'MeasureFock'}
            decompositions = set()

        # set maximum number of measurements to 2, and measure 3 in prog below
        device_dict = {
            "modes": {
                "max": {
                    "pnr": 2,
                    "homodyne": 2,
                    "heterodyne": 2
                }
            },
            "layout": None, "gate_parameters": {}, "compiler": [None]
        }
        spec = sf.api.DeviceSpec(target="simulon", connection=None, spec=device_dict)

        prog = sf.Program(3)
        with prog.context as q:
            for reg in q:
                measure_op | reg

        with pytest.raises(
            program.CircuitError, match=f"contains 3 {measure_name} measurements"
        ):
            prog.compile(device=spec, compiler=DummyCompiler())

    def test_no_default_compiler(self):
        """Test that an exception is raised if the DeviceSpec has no compilers
        specified (and thus no default compiler)"""

        device_dict = {"modes": 3, "layout": None, "gate_parameters": None, "compiler": [None]}
        spec = sf.api.DeviceSpec(target="dummy_target", connection=None, spec=device_dict)

        prog = sf.Program(3)
        with prog.context as q:
            ops.S2gate(0.6) | [q[0], q[1]]
            ops.S2gate(0.6) | [q[1], q[2]]

        with pytest.raises(program.CircuitError, match="does not specify a compiler."):
            new_prog = prog.compile(device=spec)

    def test_run_optimizations(self):
        """Test that circuit is optimized when optimize is True"""

        class DummyCircuit(Compiler):
            """A circuit with 2 modes"""
            interactive = True
            primitives = {'Rgate'}
            decompositions = set()

        device_dict = {"modes": 3, "layout": None, "gate_parameters": None, "compiler": [None]}
        spec = sf.api.DeviceSpec(target="dummy_target", connection=None, spec=device_dict)

        prog = sf.Program(3)
        with prog.context as q:
            ops.Rgate(0.3) | q[0]
            ops.Rgate(0.4) | q[0]

        new_prog = prog.compile(
            compiler=DummyCircuit(),
            optimize=True,
        )
        assert new_prog.circuit[0].__str__() == "Rgate(0.7) | (q[0])"

    def test_validate_parameters(self):
        """Test that the parameters are validated in the compile method"""
        mock_layout = textwrap.dedent(
            """\
            name mock
            version 1.0

            S2gate({squeezing_amplitude_0}, 0.0) | [0, 1]
            """
        )

        device_dict = {
            "layout": mock_layout,
            "modes": 2,
            "compiler": [],
            "gate_parameters": {
                "squeezing_amplitude_0": [0, 1],
            },
        }

        class DummyCircuit(Compiler):
            """A circuit with 2 modes"""
            interactive = True
            primitives = {'S2gate'}
            decompositions = set()

        spec = sf.api.DeviceSpec(target=None, spec=device_dict, connection=None)

        prog = sf.Program(2)
        with prog.context as q:
            ops.S2gate(1.5) | q  # invalid value 1.5

        with pytest.raises(ValueError, match="has invalid value"):
            new_prog = prog.compile(
                device=spec,
                compiler=DummyCircuit(),
            )


    def test_no_decompositions(self):
        """Test that no decompositions take
        place if the circuit spec doesn't support it."""

        class DummyCircuit(Compiler):
            """A circuit spec with no decompositions"""
            modes = None
            remote = False
            local = True
            interactive = True
            primitives = {'S2gate', 'Interferometer'}
            decompositions = set()

        prog = sf.Program(3)
        U = np.array([[0, 1], [1, 0]])
        with prog.context as q:
            ops.S2gate(0.6) | [q[0], q[1]]
            ops.Interferometer(U) | [q[0], q[1]]

        new_prog = prog.compile(compiler=DummyCircuit())

        # check compiled program only has two gates
        assert len(new_prog) == 2

        # test gates are correct
        circuit = new_prog.circuit
        assert circuit[0].op.__class__.__name__ == "S2gate"
        assert circuit[1].op.__class__.__name__ == "Interferometer"

    def test_decompositions(self):
        """Test that decompositions take
        place if the circuit spec requests it."""

        class DummyCircuit(Compiler):
            modes = None
            remote = False
            local = True
            interactive = True
            primitives = {'S2gate', 'Interferometer', 'BSgate', 'Sgate'}
            decompositions = {'S2gate': {}}

        prog = sf.Program(3)
        U = np.array([[0, 1], [1, 0]])
        with prog.context as q:
            ops.S2gate(0.6) | [q[0], q[1]]
            ops.Interferometer(U) | [q[0], q[1]]

        new_prog = prog.compile(compiler=DummyCircuit())

        # check compiled program now has 5 gates
        # the S2gate should decompose into two BS and two Sgates
        assert len(new_prog) == 5

        # test gates are correct
        circuit = new_prog.circuit
        assert circuit[0].op.__class__.__name__ == "BSgate"
        assert circuit[1].op.__class__.__name__ == "Sgate"
        assert circuit[2].op.__class__.__name__ == "Sgate"
        assert circuit[3].op.__class__.__name__ == "BSgate"
        assert circuit[4].op.__class__.__name__ == "Interferometer"

    def test_invalid_decompositions(self):
        """Test that an exception is raised if the circuit spec
        requests a decomposition that doesn't exist"""

        class DummyCircuit(Compiler):
            modes = None
            remote = False
            local = True
            interactive = True
            primitives = {'Rgate', 'Interferometer'}
            decompositions = {'Rgate': {}}

        prog = sf.Program(3)
        U = np.array([[0, 1], [1, 0]])
        with prog.context as q:
            ops.Rgate(0.6) | q[0]
            ops.Interferometer(U) | [q[0], q[1]]

        with pytest.raises(NotImplementedError, match="No decomposition available: Rgate"):
            new_prog = prog.compile(compiler=DummyCircuit())

    def test_invalid_primitive(self):
        """Test that an exception is raised if the program
        contains a primitive not allowed on the circuit spec.

        Here, we can simply use the guassian circuit spec and
        the Kerr gate as an existing example.
        """
        prog = sf.Program(3)
        with prog.context as q:
            ops.Kgate(0.6) | q[0]

        with pytest.raises(program.CircuitError, match="Kgate cannot be used with the compiler"):
            new_prog = prog.compile(compiler='gaussian')

    def test_user_defined_decomposition_false(self):
        """Test that an operation that is both a primitive AND
        a decomposition (for instance, ops.Gaussian in the gaussian
        backend) can have it's decomposition behaviour user defined.

        In this case, the Gaussian operation should remain after compilation.
        """
        prog = sf.Program(2)
        cov = np.ones((4, 4)) + np.eye(4)
        r = np.array([0, 1, 1, 2])
        with prog.context as q:
            ops.Gaussian(cov, r, decomp=False) | q

        prog = prog.compile(compiler='gaussian')
        assert len(prog) == 1
        circuit = prog.circuit
        assert circuit[0].op.__class__.__name__ == "Gaussian"

        # test compilation against multiple targets in sequence
        with pytest.raises(program.CircuitError, match="The operation Gaussian is not a primitive for the compiler 'fock'"):
            prog = prog.compile(compiler='fock')

    def test_user_defined_decomposition_true(self):
        """Test that an operation that is both a primitive AND
        a decomposition (for instance, ops.Gaussian in the gaussian
        backend) can have it's decomposition behaviour user defined.

        In this case, the Gaussian operation should compile
        to a Squeezed preparation.
        """
        prog = sf.Program(3)
        r = 0.453
        cov = np.array([[np.exp(-2*r), 0], [0, np.exp(2*r)]])*sf.hbar/2
        with prog.context:
            ops.Gaussian(cov, decomp=True) | 0

        new_prog = prog.compile(compiler='gaussian')

        assert len(new_prog) == 1

        circuit = new_prog.circuit
        assert circuit[0].op.__class__.__name__ == "Squeezed"
        assert circuit[0].op.p[0] == r

    def test_topology_validation(self):
        """Test compilation properly matches the circuit spec topology"""

        class DummyCircuit(Compiler):
            modes = None
            remote = False
            local = True
            interactive = True
            primitives = {'Sgate', 'BSgate', 'Dgate', 'MeasureFock'}
            decompositions = set()

            circuit = textwrap.dedent(
                """\
                name test
                version 0.0

                Sgate({sq}, 0) | 0
                Dgate(-7.123) | 1
                BSgate({theta}) | 0, 1
                MeasureFock() | 0
                MeasureFock() | 2
                """
            )

        prog = sf.Program(3)
        with prog.context as q:
            # the circuit given below is an
            # isomorphism of the one provided above
            # in circuit, so should validate.
            ops.MeasureFock() | q[2]
            ops.Dgate(-7.123, 0.0) | q[1]
            ops.Sgate(0.543) | q[0]
            ops.BSgate(-0.32) | (q[0], q[1])
            ops.MeasureFock() | q[0]

        new_prog = prog.compile(compiler=DummyCircuit())

        # no exception should be raised; topology correctly validated
        assert len(new_prog) == 5

    def test_invalid_topology(self):
        """Test compilation raises exception if toplogy not matched"""

        class DummyCircuit(Compiler):
            modes = None
            remote = False
            local = True
            interactive = True
            primitives = {'Sgate', 'BSgate', 'Dgate', 'MeasureFock'}
            decompositions = set()

            circuit = textwrap.dedent(
                """\
                name test
                version 0.0

                Sgate({sq}, 0) | 0
                Dgate(-7.123, 0) | 1
                BSgate({theta}) | 0, 1
                MeasureFock() | 0
                MeasureFock() | 2
                """
            )

        prog = sf.Program(3)
        with prog.context as q:
            # the circuit given below is NOT an
            # isomorphism of the one provided above
            # in circuit, as the Sgate
            # comes AFTER the beamsplitter.
            ops.MeasureFock() | q[2]
            ops.Dgate(-7.123, 0.0) | q[1]
            ops.BSgate(-0.32) | (q[0], q[1])
            ops.Sgate(0.543) | q[0]
            ops.MeasureFock() | q[0]

        with pytest.raises(program.CircuitError, match="incompatible topology"):
            new_prog = prog.compile(compiler=DummyCircuit())
예제 #5
0
class TestProgram:
    """Tests the Program class."""

    def test_with_block(self, prog):
        """Gate application using a with block."""
        # command queue is empty
        assert len(prog) == 0

        with prog.context as q:
            ops.Dgate(0.5, 0.0) | q[0]
        # now there is one gate in the queue
        assert len(prog) == 1

        with prog.context as q:
            ops.BSgate(0.5, 0.3) | (q[1], q[0])
        assert len(prog) == 2

    def test_parent_program(self):
        """Continuing one program with another."""
        D = ops.Dgate(0.5, 0.0)
        prog = sf.Program(3)
        with prog.context as q:
            D | q[1]
            ops.Del | q[0]
        cont = sf.Program(prog)
        with cont.context as q:
            D | q[0]
            r = ops.New(1)
            D | r
        assert cont.can_follow(prog)
        assert prog.reg_refs == cont.init_reg_refs
        assert prog.unused_indices == cont.init_unused_indices

    def test_print_commands(self, eng, prog):
        """Program.print and Engine.print_applied return correct strings."""
        prog = sf.Program(2)

        # store the result of the print command in list res
        res = []
        # use a print function that simply appends the operation
        # name to the results list
        print_fn = lambda x: res.append(x.__str__())

        # prog should now be empty
        prog.print(print_fn)
        assert res == []

        # define some gates
        D = ops.Dgate(0.5, 0.0)
        BS = ops.BSgate(2 * np.pi, np.pi / 2)
        R = ops.Rgate(np.pi)

        with prog.context as q:
            alice, bob = q
            D | alice
            BS | (alice, bob)
            ops.Del | alice
            R | bob
            (charlie,) = ops.New(1)
            BS | (bob, charlie)
            ops.MeasureX | bob
            ops.Dgate(bob.par).H | charlie
            ops.Del | bob
            ops.MeasureX | charlie

        res = []
        prog.print(print_fn)

        expected = [
            "Dgate(0.5, 0) | (q[0])",
            "BSgate(6.283, 1.571) | (q[0], q[1])",
            "Del | (q[0])",
            "Rgate(3.142) | (q[1])",
            "New(1)",
            "BSgate(6.283, 1.571) | (q[1], q[2])",
            "MeasureX | (q[1])",
            "Dgate(q1, 0).H | (q[2])",
            "Del | (q[1])",
            "MeasureX | (q[2])",
        ]

        assert res == expected

        # NOTE optimization can change gate order
        result = eng.run(prog, compile_options={"optimize": False})
        res = []
        eng.print_applied(print_fn)
        assert res == ["Run 0:"] + expected

    def test_params(self, prog):
        """Creating and retrieving free parameters."""
        assert not prog.free_params  # no free params to start with

        with pytest.raises(TypeError, match="Parameter names must be strings."):
            prog.params(1)

        # creating
        x = prog.params("a")
        assert isinstance(x, FreeParameter)
        assert x.name == "a"
        assert len(prog.free_params) == 1

        # retrieving
        y = prog.params("a")
        assert y is x
        assert len(prog.free_params) == 1

        with pytest.raises(TypeError, match="Parameter names must be strings."):
            prog.params(x)

        # creating/retrieving multiple
        names = ("foo", "bar", "a")
        pars = prog.params(*names)
        assert isinstance(pars, list)
        for n, p in zip(names, pars):
            assert isinstance(p, FreeParameter)
            assert p.name == n
        assert pars[2] is x  # still the same parameter
        assert len(prog.free_params) == 3

        # once the program is locked only retrieval is possible
        prog.lock()
        with pytest.raises(
            program.CircuitError,
            match="The Program is locked, no more free parameters can be created.",
        ):
            w = prog.params("www")
        w = prog.params("a")
        assert w is x

    def test_bind_params(self, prog):
        """Binding free parameters."""

        with pytest.raises(ParameterError, match="Unknown free parameter"):
            prog.bind_params({"x": 0})

        x, y = prog.params("x", "y")
        # bind some params using parameter name
        prog.bind_params({"x": 1.0})
        assert x.val == 1.0
        assert y.val is None
        # bind using the parameter itself
        prog.bind_params({x: 2.0})
        assert x.val == 2.0
        assert y.val is None

    def test_assert_number_of_modes(self):
        """Check that the correct error is raised when calling `prog.assert_number_of_modes`
        with the incorrect number of modes."""
        device_dict = {
            "target": "abc",
            "modes": 2,
            "layout": "",
            "gate_parameters": {},
            "compiler": ["DummyCompiler"],
        }
        spec = sf.DeviceSpec(spec=device_dict)

        prog = sf.Program(3)
        with prog.context as q:
            ops.S2gate(0.6) | [q[0], q[1]]
            ops.S2gate(0.6) | [q[1], q[2]]

        with pytest.raises(
            program.CircuitError,
            match="program contains 3 modes, but the device 'abc' only supports a 2-mode program",
        ):
            prog.assert_number_of_modes(spec)

    @pytest.mark.parametrize(
        "measure_op, measure_name",
        [
            (ops.MeasureFock(), "fock"),  # MeasureFock
            (ops.MeasureHomodyne(phi=0), "homodyne"),  # MeasureX
            (ops.MeasureHomodyne(phi=42), "homodyne"),  # MeasureHomodyne
            (ops.MeasureHomodyne(phi=np.pi / 2), "homodyne"),  # MeasureP
            (ops.MeasureHeterodyne(), "heterodyne"),  # MeasureHD
            (ops.MeasureHeterodyne(select=0), "heterodyne"),  # MeasureHeterodyne
        ],
    )
    def test_assert_max_number_of_measurements(self, measure_op, measure_name):
        """Check that the correct error is raised when calling `prog.assert_number_of_measurements`
        with the incorrect number of measurements in the circuit."""
        # set maximum number of measurements to 2, and measure 3 in prog below
        device_dict = {
            "target": "simulon_gaussian",
            "modes": {"pnr_max": 2, "homodyne_max": 2, "heterodyne_max": 2},
            "layout": "",
            "gate_parameters": {},
            "compiler": ["gaussian"],
        }
        spec = sf.DeviceSpec(spec=device_dict)

        prog = sf.Program(3)
        with prog.context as q:
            for reg in q:
                measure_op | reg

        with pytest.raises(program.CircuitError, match=f"contains 3 {measure_name} measurements"):
            prog.assert_max_number_of_measurements(spec)

    def test_keyerror_assert_max_number_of_measurements(self):
        """Check that the correct error is raised when calling `prog.assert_number_of_measurements`
        with an incorrect device spec modes entry."""
        # set maximum number of measurements to 2, and measure 3 in prog below
        device_dict = {
            "target": "simulon_gaussian",
            "modes": {"max": {"pnr": 2, "homodyne": 2, "heterodyne": 2}},
            "layout": "",
            "gate_parameters": {},
            "compiler": ["gaussian"],
        }
        spec = sf.DeviceSpec(spec=device_dict)

        prog = sf.Program(3)
        with prog.context as q:
            for reg in q:
                ops.MeasureFock() | reg

        match = "Expected keys for the maximum allowed number of PNR"
        with pytest.raises(KeyError, match=match):
            prog.assert_max_number_of_measurements(spec)

    def test_assert_max_number_of_measurements_wrong_entry(self):
        """Check that the correct error is raised when calling `prog.assert_number_of_measurements`
        with the incorrect type of device spec mode entry."""
        device_dict = {
            "target": "simulon_gaussian",
            "modes": 2,
            "layout": "",
            "gate_parameters": {},
            "compiler": ["gaussian"],
        }
        spec = sf.DeviceSpec(spec=device_dict)

        prog = sf.Program(3)
        with prog.context as q:
            ops.S2gate(0.6) | [q[0], q[1]]
            ops.S2gate(0.6) | [q[1], q[2]]

        with pytest.raises(KeyError, match="Expected keys for the maximum allowed number of PNR"):
            prog.assert_max_number_of_measurements(spec)

    def test_has_post_selection(self):
        """Check that the ``has_post_selection`` property behaves as expected when it uses
        post-selection or not.
        """
        # instantiate two programs for testing
        prog_1, prog_2 = sf.Program(2), sf.Program(2)

        # program with post-selection
        with prog_1.context as q:
            ops.Fock(2) | q[0]
            ops.BSgate() | (q[0], q[1])
            ops.MeasureHomodyne(select=0, phi=np.pi) | q[0]
            ops.MeasureFock() | q[1]

        # program without post-selection
        with prog_2.context as q:
            ops.Fock(2) | q[0]
            ops.BSgate() | (q[0], q[1])
            ops.MeasureFock() | q[1]
        assert prog_1.has_post_selection
        assert prog_2.has_post_selection is False

    def test_has_feed_forward(self):
        """Check that the ``has_feed_forward`` property behaves as expected when it uses
        feed-forwarding or not.
        """
        # instantiate two programs for testing
        prog_1, prog_2 = sf.Program(2), sf.Program(2)

        # program with feed-forwarding
        with prog_1.context as q:
            ops.Sgate(0.54) | q[1]
            ops.BSgate(0.42, 0.1) | (q[0], q[1])
            ops.Sgate(q[0].par) | q[1]
            ops.MeasureHeterodyne() | q[1]

        # program without feed-forwarding
        with prog_2.context as q:
            ops.Sgate(0.54) | q[1]
            ops.BSgate(0.42, 0.1) | (q[0], q[1])
            ops.MeasureHomodyne(phi=np.pi) | q[1]
        assert prog_1.has_feed_forward
        assert prog_1.has_post_selection is False
        assert prog_2.has_feed_forward is False
        assert prog_2.has_post_selection is False