示例#1
0
 def assert_number_of_modes(self, device):
     if self.timebins > device.modes["temporal_max"]:
         raise CircuitError(
             f"This program contains {self.timebins} temporal modes, but the device '{device.target}' "
             f"only supports up to {device.modes['temporal_max']} modes.")
     if self.concurr_modes != device.modes["concurrent"]:
         raise CircuitError(
             f"This program contains {self.concurr_modes} concurrent modes, but the device '{device.target}' "
             f"only supports {device.modes['concurrent']} modes.")
     if self.spatial_modes != device.modes["spatial"]:
         raise CircuitError(
             f"This program contains {self.spatial_modes} spatial modes, but the device '{device.target}' "
             f"only supports {device.modes['spatial']} modes.")
示例#2
0
    def compile(self, seq, registers):
        """Try to arrange a quantum circuit into a form suitable for Gaussian boson sampling.

        This method checks whether the circuit can be implemented as a Gaussian boson sampling
        problem, i.e., if it is equivalent to a circuit A+B, where the sequence A only contains
        Gaussian operations, and B only contains Fock measurements.

        If the answer is yes, the circuit is arranged into the A+B order, and all the Fock
        measurements are combined into a single :class:`MeasureFock` operation.

        Args:
            seq (Sequence[Command]): quantum circuit to modify
            registers (Sequence[RegRefs]): quantum registers
        Returns:
            List[Command]: modified circuit
        Raises:
            CircuitError: the circuit does not correspond to GBS
        """
        A, B, C = group_operations(seq,
                                   lambda x: isinstance(x, ops.MeasureFock))

        # C should be empty
        if C:
            raise CircuitError("Operations following the Fock measurements.")

        # A should only contain Gaussian operations
        # (but this is already guaranteed by group_operations() and our primitive set)

        # without Fock measurements GBS is pointless
        if not B:
            raise CircuitError("GBS circuits must contain Fock measurements.")

        # there should be only Fock measurements in B
        measured = set()
        for cmd in B:
            if not isinstance(cmd.op, ops.MeasureFock):
                raise CircuitError(
                    "The Fock measurements are not consecutive.")

            # combine the Fock measurements
            temp = set(cmd.reg)
            if measured & temp:
                raise CircuitError("Measuring the same mode more than once.")
            measured |= temp

        # replace B with a single Fock measurement
        B = [
            Command(ops.MeasureFock(),
                    sorted(list(measured), key=lambda x: x.ind))
        ]
        return super().compile(A + B, registers)
示例#3
0
    def assert_modes(self, device):
        """Check that the number of modes in the program is valid.

        .. note::

            ``device.modes`` must be a dictionary containing the maximum number of allowed
            measurements for the specified target.

        Args:
            device (.strawberryfields.DeviceSpec): device specification object to use
        """
        if self.timebins > device.modes["temporal_max"]:
            raise CircuitError(
                f"This program contains {self.timebins} temporal modes, but the device '{device.target}' "
                f"only supports up to {device.modes['temporal_max']} modes.")
        if self.concurr_modes != device.modes["concurrent"]:
            raise CircuitError(
                f"This program contains {self.concurr_modes} concurrent modes, but the device '{device.target}' "
                f"only supports {device.modes['concurrent']} modes.")
        if self.spatial_modes != device.modes["spatial"]:
            raise CircuitError(
                f"This program contains {self.spatial_modes} spatial modes, but the device '{device.target}' "
                f"only supports {device.modes['spatial']} modes.")
示例#4
0
    def compile(self, seq, registers):
        """Compiles the Borealis circuit by inserting missing phase gates due to loop offsets.

        Args:
            seq (Sequence[Command]): quantum circuit to modify
            registers (Sequence[RegRefs]): quantum registers

        Returns:
            List[Command]: modified circuit

        Raises:
            CircuitError: the given circuit cannot be validated to belong to this circuit class
        """
        # keep track of whether a loop offset has been set by the user (True)
        # or by the compiler (False), for
        self._user_offsets: List[bool] = []

        if self.circuit:
            bb = blackbird.loads(self.circuit)
            program = sio.to_program(bb)
            circuit = program.circuit or []

            for i, cmds in enumerate(zip(circuit, seq)):
                wires_0 = {m.ind for m in cmds[0].reg}
                wires_1 = {m.ind for m in cmds[1].reg}

                ops_not_equal = type(cmds[0].op) != type(cmds[1].op) or wires_0 != wires_1

                # if the operation in the device spec is _not_ a loop offset and differs from the
                # user set value, the topology cannot be made to match the device layout by
                # just inserting loop offsets.
                if self._is_loop_offset(cmds[0].op):
                    if ops_not_equal:
                        seq.insert(i, cmds[0])
                        self._user_offsets.append(False)
                    else:
                        self._user_offsets.append(True)
                elif ops_not_equal:
                    raise CircuitError(
                        "Compilation not possible due to incompatible topologies. Expected loop "
                        f"offset gate or '{type(cmds[0].op).__name__}' on mode(s) {wires_0}, got "
                        f"'{type(cmds[1].op).__name__}' on mode(s) {wires_1}."
                    )

            seq.extend(circuit[len(seq) :])

        # pass the circuit sequence to the general TMD compiler to make sure that
        # it corresponds to the correct device layout in the specification
        return super().compile(seq, registers)
示例#5
0
    def init_circuit(cls, layout: str) -> None:
        """Sets the circuit in the compiler class.

        Args:
            layout (str): the circuit layout for the target device
        """
        if cls._layout:
            # if the exact same circuit is set (apart from newlines) then return
            if cls._layout.replace("\n", "") != layout.replace("\n", ""):
                raise CircuitError(
                    f"Circuit already set in compiler {cls.short_name}. Device layout incompatible "
                    "with compiler layout. Call the compiler's 'reset_circuit' method, or use a "
                    "different device layout.")
            return

        if not isinstance(layout, str):
            raise TypeError(
                "Layout must be a string representing the Blackbird circuit.")

        cls._layout = layout
示例#6
0
    def compile(self, seq, registers):
        """TDM-specific circuit compilation method.

        Checks that the compilers has access to the program's circuit layout and, if so,
        compiles the program using the base compiler.

        Args:
            seq (Sequence[Command]): quantum circuit to modify
            registers (Sequence[RegRefs]): quantum registers

        Returns:
            List[Command]: modified circuit

        Raises:
            CircuitError: if the given circuit hasn't been initialized for this compiler
        """
        if not self.circuit:
            raise CircuitError("TDM programs cannot be compiled without a valid circuit layout.")

        return super().compile(seq, registers)
示例#7
0
    def compile(self, *, device=None, compiler=None):
        """Compile the time-domain program given a Strawberry Fields photonic hardware device specification.

        Currently, the compilation is simply a check that the program matches the device.

        Args:
            device (~strawberryfields.api.DeviceSpec): device specification object to use for
                program compilation
            compiler (str, ~strawberryfields.compilers.Compiler): Compiler name or compile strategy
                to use. If a device is specified, this overrides the compile strategy specified by
                the hardware :class:`~.DeviceSpec`. If no compiler is passed, the default "TD2"
                compiler is used. Currently, the only other allowed compiler is "gaussian".

        Returns:
            Program: compiled program
        """
        if compiler == "gaussian":
            return super().compile(device=device, compiler=compiler)

        if device is not None:
            device_layout = bb.loads(device.layout)

            if device_layout.programtype["name"] != "tdm":
                raise TypeError(
                    'TDM compiler only supports "tdm" type device specification layouts. '
                    "Received {} type.".format(
                        device_layout.programtype["name"]))

            if device.modes is not None:
                self.assert_number_of_modes(device)

            # First check: the gates are in the correct order
            program_gates = [
                cmd.op.__class__.__name__ for cmd in self.rolled_circuit
            ]
            device_gates = [op["op"] for op in device_layout.operations]
            if device_gates != program_gates:
                raise CircuitError(
                    "The gates or the order of gates used in the Program is incompatible with the device '{}' "
                    .format(device.target))

            # Second check: the gates act on the correct modes
            program_modes = [[r.ind for r in cmd.reg]
                             for cmd in self.rolled_circuit]
            device_modes = [op["modes"] for op in device_layout.operations]
            if program_modes != device_modes:
                raise CircuitError(
                    "Program cannot be used with the device '{}' "
                    "due to incompatible mode ordering.".format(device.target))

            # Third check: the parameters of the gates are valid
            # We will loop over the different operations in the device specification

            for i, operation in enumerate(device_layout.operations):
                # We obtain the name of the parameter(s)
                param_names = operation["args"]

                program_params_len = len(self.rolled_circuit[i].op.p)
                device_params_len = len(param_names)
                # The next if is to make sure we do not flag incorrectly things like Sgate(r,0) being different Sgate(r)
                # This assumes that parameters other than the first one are zero if not explicitly stated.
                if device_params_len < program_params_len:
                    for j in range(1, program_params_len):
                        if self.rolled_circuit[i].op.p[j] != 0:
                            raise CircuitError(
                                "Program cannot be used with the device '{}' "
                                "due to incompatible parameter.".format(
                                    device.target))
                # Now we will check explicitly if the parameters in the program match
                num_symbolic_param = 0  # counts the number of symbolic variables, which are labelled consecutively by the context method

                for k, param_name in enumerate(param_names):
                    # Obtain the value of the corresponding parameter in the program
                    program_param = self.rolled_circuit[i].op.p[k]

                    # make sure that hardcoded parameters in the device layout are correct
                    if not isinstance(param_name, str):
                        if not program_param == param_name:
                            raise CircuitError(
                                "Program cannot be used with the device '{}' "
                                "due to incompatible parameter. Parameter has value '{}' "
                                "while its valid value is '{}'".format(
                                    device.target, program_param, param_name))
                        continue

                    # Obtain the relevant parameter range from the device
                    param_range = device.gate_parameters[param_name]
                    if sf.parameters.par_is_symbolic(program_param):
                        # If it is a symbolic value go and lookup its corresponding list in self.tdm_params
                        local_p_vals = self.tdm_params[num_symbolic_param]

                        for x in local_p_vals:
                            if not x in param_range:
                                raise CircuitError(
                                    "Program cannot be used with the device '{}' "
                                    "due to incompatible parameter. Parameter has value '{}' "
                                    "while its valid range is '{}'".format(
                                        device.target, x, param_range))
                        num_symbolic_param += 1

                    else:
                        # If it is a numerical value check directly
                        if not program_param in param_range:
                            raise CircuitError(
                                "Program cannot be used with the device '{}' "
                                "due to incompatible parameter. Parameter has value '{}' "
                                "while its valid range is '{}'".format(
                                    device.target, program_param, param_range))
            return self

        raise CircuitError(
            "TDM programs cannot be compiled without a valid device specification."
        )
示例#8
0
    def compile(self, seq, registers):
        """Try to arrange a quantum circuit into a form suitable for X8.

        Args:
            seq (Sequence[Command]): quantum circuit to modify
            registers (Sequence[RegRefs]): quantum registers
        Returns:
            List[Command]: modified circuit
        Raises:
            CircuitError: the circuit does not correspond to X8
        """
        # pylint: disable=too-many-statements,too-many-branches

        # first do general GBS compilation to make sure
        # Fock measurements are correct
        # ---------------------------------------------
        seq = GBSSpecs().compile(seq, registers)
        A, B, C = group_operations(seq,
                                   lambda x: isinstance(x, ops.MeasureFock))

        if len(B[0].reg) != self.modes:
            raise CircuitError("All modes must be measured.")

        # Check circuit begins with two mode squeezers
        # --------------------------------------------
        A, B, C = group_operations(seq, lambda x: isinstance(x, ops.S2gate))

        regrefs = set()

        if B:
            # get set of circuit registers as a tuple for each S2gate
            regrefs = {(cmd.reg[0].ind, cmd.reg[1].ind) for cmd in B}

        # the set of allowed mode-tuples the S2gates must have
        allowed_modes = set(zip(range(0, 4), range(4, 8)))

        if not regrefs.issubset(allowed_modes):
            raise CircuitError("S2gates do not appear on the correct modes.")

        # ensure provided S2gates all have the allowed squeezing values
        allowed_sq_value = {(0.0, 0.0), (self.sq_amplitude, 0.0)}
        sq_params = {(float(np.round(cmd.op.p[0], 3)), float(cmd.op.p[1]))
                     for cmd in B}

        if not sq_params.issubset(allowed_sq_value):
            wrong_params = sq_params - allowed_sq_value
            raise CircuitError(
                "Incorrect squeezing value(s) (r, phi)={}. Allowed squeezing "
                "value(s) are (r, phi)={}.".format(wrong_params,
                                                   allowed_sq_value))

        # determine which modes do not have input S2gates specified
        missing = allowed_modes - regrefs

        for i, j in missing:
            # insert S2gates with 0 squeezing
            seq.insert(0,
                       Command(ops.S2gate(0, 0), [registers[i], registers[j]]))

        # Check if matches the circuit template
        # --------------------------------------------
        # This will avoid superfluous unitary compilation.
        try:
            seq = super().compile(seq, registers)
        except CircuitError:
            # failed topology check. Continue to more general
            # compilation below.
            pass
        else:
            return seq

        # Compile the unitary: combine and then decompose all unitaries
        # -------------------------------------------------------------
        A, B, C = group_operations(
            seq, lambda x: isinstance(x, (ops.Rgate, ops.BSgate, ops.MZgate)))

        # begin unitary lists for mode [0, 1, 2, 3] and modes [4, 5, 6, 7] with
        # two identity matrices. This is because multi_dot requires
        # at least two matrices in the list.
        U_list0 = [np.identity(self.modes // 2, dtype=np.complex128)] * 2
        U_list4 = [np.identity(self.modes // 2, dtype=np.complex128)] * 2

        if not B:
            # no interferometer was applied
            A, B, C = group_operations(seq,
                                       lambda x: isinstance(x, ops.S2gate))
            A = B  # move the S2gates to A
        else:
            for cmd in B:
                # calculate the unitary matrix representing each
                # rotation gate and each beamsplitter
                modes = [i.ind for i in cmd.reg]
                params = par_evaluate(cmd.op.p)
                U = np.identity(self.modes // 2, dtype=np.complex128)

                if isinstance(cmd.op, ops.Rgate):
                    m = modes[0]
                    U[m % 4, m % 4] = np.exp(1j * params[0])

                elif isinstance(cmd.op, ops.MZgate):
                    m, n = modes
                    U = mach_zehnder(m % 4, n % 4, params[0], params[1],
                                     self.modes // 2)

                elif isinstance(cmd.op, ops.BSgate):
                    m, n = modes

                    t = np.cos(params[0])
                    r = np.exp(1j * params[1]) * np.sin(params[0])

                    U[m % 4, m % 4] = t
                    U[m % 4, n % 4] = -np.conj(r)
                    U[n % 4, m % 4] = r
                    U[n % 4, n % 4] = t

                if set(modes).issubset({0, 1, 2, 3}):
                    U_list0.insert(0, U)
                elif set(modes).issubset({4, 5, 6, 7}):
                    U_list4.insert(0, U)
                else:
                    raise CircuitError(
                        "Unitary must be applied separately to modes [0, 1, 2, 3] and modes [4, 5, 6, 7]."
                    )

        # multiply all unitaries together, to get the final
        # unitary representation on modes [0, 1] and [2, 3].
        U0 = multi_dot(U_list0)
        U4 = multi_dot(U_list4)

        # check unitaries are equal
        if not np.allclose(U0, U4):
            raise CircuitError(
                "Interferometer on modes [0, 1, 2, 3] must be identical to interferometer on modes [4, 5, 6, 7]."
            )

        U = block_diag(U0, U4)

        # replace B with an interferometer
        B = [
            Command(ops.Interferometer(U0), registers[:4]),
            Command(ops.Interferometer(U4), registers[4:]),
        ]

        # decompose the interferometer, using Mach-Zehnder interferometers
        B = self.decompose(B)

        # Do a final circuit topology check
        # ---------------------------------
        seq = super().compile(A + B + C, registers)
        return seq
示例#9
0
    def compile(self, seq, registers):
        # the number of modes in the provided program
        n_modes = len(registers)

        # Number of modes must be even
        if n_modes % 2 != 0:
            raise CircuitError("The X series only supports programs with an even number of modes.")

        # Call the GBS compiler to do basic measurement validation.
        # The GBS compiler also merges multiple measurement commands
        # into a single MeasureFock command at the end of the circuit.
        seq = GBSSpecs().compile(seq, registers)

        # ensure that all modes are measured
        if len(seq[-1].reg) != n_modes:
            raise CircuitError("All modes must be measured.")

        # Use the GaussianUnitary compiler to compute the symplectic
        # matrix representing the Gaussian operations.
        # Note that the Gaussian unitary compiler does not accept measurements,
        # so we append the measurement separately.
        meas_seq = [seq[-1]]
        seq = GaussianUnitary().compile(seq[:-1], registers) + meas_seq

        # determine the modes that are acted on by the symplectic transformation
        used_modes = [x.ind for x in seq[0].reg]

        # extract the compiled symplectic matrix
        S = seq[0].op.p[0]

        if len(used_modes) != n_modes:
            # The symplectic transformation acts on a subset of
            # the programs registers. We must expand the symplectic
            # matrix to one that acts on all registers.
            # simply extract the computed symplectic matrix
            S = expand(seq[0].op.p[0], used_modes, n_modes)

        half_n_modes = n_modes // 2

        # Construct the covariance matrix of the state.
        # Note that hbar is a global variable that is set by the user
        cov = (sf.hbar / 2) * S @ S.T

        # Construct the A matrix
        A = Amat(cov, hbar=sf.hbar)

        # Construct the adjacency matrix represented by the A matrix.
        # This must be an weighted, undirected bipartite graph. That is,
        # B00 = B11 = 0 (no edges between the two vertex sets 0 and 1),
        # and B01 == B10.T (undirected edges between the two vertex sets).
        B = A[:n_modes, :n_modes]
        B00 = B[:half_n_modes, :half_n_modes]
        B01 = B[:half_n_modes, half_n_modes:]
        B10 = B[half_n_modes:, :half_n_modes]
        B11 = B[half_n_modes:, half_n_modes:]

        # Perform unitary validation to ensure that the
        # applied unitary is valid.

        if not np.allclose(B00, 0) or not np.allclose(B11, 0):
            # Not a bipartite graph
            raise CircuitError(
                "The applied unitary cannot mix between the modes {}-{} and modes {}-{}.".format(
                    0, half_n_modes - 1, half_n_modes, n_modes - 1
                )
            )

        if not np.allclose(B01, B10):
            # Not a symmetric bipartite graph
            raise CircuitError(
                "The applied unitary on modes {}-{} must be identical to the applied unitary on modes {}-{}.".format(
                    0, half_n_modes - 1, half_n_modes, n_modes - 1
                )
            )

        # Now that the unitary has been validated, perform the Takagi decomposition
        # to determine the constituent two-mode squeezing and interferometer
        # parameters.
        sqs, U = takagi(B01)
        sqs = np.arctanh(sqs)

        # ensure provided S2gates all have the allowed squeezing values
        if not all(s in self.allowed_sq_ranges for s in sqs):
            wrong_sq_values = [np.round(s, 4) for s in sqs if s not in self.allowed_sq_ranges]
            raise CircuitError(
                "Incorrect squeezing value(s) r={}. Allowed squeezing "
                "value(s) are {}.".format(wrong_sq_values, self.allowed_sq_ranges)
            )

        # Convert the squeezing values into a sequence of S2gate commands
        sq_seq = [
            Command(ops.S2gate(sqs[i]), [registers[i], registers[i + half_n_modes]])
            for i in range(half_n_modes)
        ]

        # NOTE: at some point, it might make sense to add a keyword argument to this method,
        # to allow the user to specify if they want the interferometers decomposed or not.

        # Convert the unitary into a sequence of MZgate and Rgate commands on the signal modes
        U1 = ops.Interferometer(U, mesh="rectangular_symmetric", drop_identity=False)._decompose(
            registers[:half_n_modes]
        )
        U2 = copy.deepcopy(U1)

        for Ui in U2:
            Ui.reg = [registers[r.ind + half_n_modes] for r in Ui.reg]

        return sq_seq + U1 + U2 + meas_seq
示例#10
0
    def compile(self, seq, registers):
        """Try to arrange a quantum circuit into a form suitable for Chip0.

        Args:
            seq (Sequence[Command]): quantum circuit to modify
            registers (Sequence[RegRefs]): quantum registers
        Returns:
            List[Command]: modified circuit
        Raises:
            CircuitError: the circuit does not correspond to Chip0
        """
        # pylint: disable=too-many-statements,too-many-branches
        # First, check if provided sequence matches the circuit template.
        # This will avoid superfluous compilation if the user is using the
        # template directly.
        try:
            seq = super().compile(seq, registers)
        except CircuitError:
            # failed topology check. Continue to more general
            # compilation below.
            pass
        else:
            return seq

        # first do general GBS compilation to make sure
        # Fock measurements are correct
        # ---------------------------------------------
        seq = GBSSpecs().compile(seq, registers)
        A, B, C = group_operations(seq,
                                   lambda x: isinstance(x, ops.MeasureFock))

        if len(B[0].reg) != self.modes:
            raise CircuitError("All modes must be measured.")

        # Check circuit begins with two mode squeezers
        # --------------------------------------------
        A, B, C = group_operations(seq, lambda x: isinstance(x, ops.S2gate))

        if A:
            raise CircuitError("Circuits must start with two S2gates.")

        # get circuit registers
        regrefs = {q for cmd in B for q in cmd.reg}

        if len(regrefs) != self.modes:
            raise CircuitError("S2gates do not appear on the correct modes.")

        # Compile the unitary: combine and then decompose all unitaries
        # -------------------------------------------------------------
        A, B, C = group_operations(
            seq, lambda x: isinstance(x, (ops.Rgate, ops.BSgate)))

        # begin unitary lists for mode [0, 1] and modes [2, 3] with
        # two identity matrices. This is because multi_dot requires
        # at least two matrices in the list.
        U_list01 = [np.identity(self.modes // 2, dtype=np.complex128)] * 2
        U_list23 = [np.identity(self.modes // 2, dtype=np.complex128)] * 2

        if not B:
            # no interferometer was applied
            A, B, C = group_operations(seq,
                                       lambda x: isinstance(x, ops.S2gate))
            A = B  # move the S2gates to A
        else:
            for cmd in B:
                # calculate the unitary matrix representing each
                # rotation gate and each beamsplitter
                # Note: this is done separately on modes [0, 1]
                # and modes [2, 3]
                modes = [i.ind for i in cmd.reg]
                params = [i.x for i in cmd.op.p]
                U = np.identity(self.modes // 2, dtype=np.complex128)

                if isinstance(cmd.op, ops.Rgate):
                    m = modes[0]
                    U[m % 2, m % 2] = np.exp(1j * params[0])

                elif isinstance(cmd.op, ops.BSgate):
                    m, n = modes

                    t = np.cos(params[0])
                    r = np.exp(1j * params[1]) * np.sin(params[0])

                    U[m % 2, m % 2] = t
                    U[m % 2, n % 2] = -np.conj(r)
                    U[n % 2, m % 2] = r
                    U[n % 2, n % 2] = t

                if set(modes).issubset({0, 1}):
                    U_list01.insert(0, U)
                elif set(modes).issubset({2, 3}):
                    U_list23.insert(0, U)
                else:
                    raise CircuitError(
                        "Unitary must be applied separately to modes [0, 1] and modes [2, 3]."
                    )

        # multiply all unitaries together, to get the final
        # unitary representation on modes [0, 1] and [2, 3].
        U01 = multi_dot(U_list01)
        U23 = multi_dot(U_list23)

        # check unitaries are equal
        if not np.allclose(U01, U23):
            raise CircuitError(
                "Interferometer on modes [0, 1] must be identical to interferometer on modes [2, 3]."
            )

        U = block_diag(U01, U23)

        # replace B with an interferometer
        B = [
            Command(ops.Interferometer(U01), registers[:2]),
            Command(ops.Interferometer(U23), registers[2:]),
        ]

        # decompose the interferometer, using Mach-Zehnder interferometers
        B = self.decompose(B)

        # Do a final circuit topology check
        # ---------------------------------
        seq = super().compile(A + B + C, registers)
        return seq
示例#11
0
    def init_circuit(self, prog):
        """Instantiate the circuit and initialize weights, means, and covs
        depending on the ``Preparation`` classes.

        Args:
            prog (object): :class:`~.Program` instance

        Raises:
            NotImplementedError: if ``Ket`` or ``DensityMatrix`` preparation used
            CircuitError: if any of the parameters for non-Gaussian state preparation
                are symbolic
        """
        from strawberryfields.ops import (
            Bosonic,
            Catstate,
            DensityMatrix,
            Fock,
            GKP,
            Ket,
            _New_modes,
        )

        # _New_modes is what gets checked when New() is called in a program circuit.
        # It is included here since it could be used to instantiate a mode for non-Gaussian
        # state preparation, and it's best to initialize any new modes from the outset.
        non_gauss_preps = (Bosonic, Catstate, DensityMatrix, Fock, GKP, Ket,
                           _New_modes)
        nmodes = prog.init_num_subsystems
        self.begin_circuit(nmodes)
        # Dummy initial weights, means and covs
        init_weights, init_means, init_covs = [[0] * nmodes for _ in range(3)]

        vac_means = np.zeros((1, 2), dtype=complex)
        vac_covs = np.expand_dims(0.5 * self.circuit.hbar * np.identity(2),
                                  axis=0)

        # List of modes that have been traversed through
        reg_list = []

        # Go through the operations in the circuit
        for cmd in prog.circuit:
            # Check if an operation other than New() has already acted on these modes.
            labels = [label.ind for label in cmd.reg]
            isitnew = 1 - np.isin(labels, reg_list)
            if np.any(isitnew):
                # Operation parameters
                pars = cmd.op.p
                # Check if any of the preparations rely on symbolic quantities
                if isinstance(cmd.op,
                              non_gauss_preps) and parameter_checker(pars):
                    raise CircuitError(
                        "Symbolic non-Gaussian preparations have not been implemented "
                        "in the bosonic backend.")
                for reg in labels:
                    # All the possible preparations should go in this loop
                    if isinstance(cmd.op, Bosonic):
                        weights, means, covs = [pars[i] for i in range(3)]

                    elif isinstance(cmd.op, Catstate):
                        weights, means, covs = self.prepare_cat(*pars)

                    elif isinstance(cmd.op, GKP):
                        weights, means, covs = self.prepare_gkp(*pars)

                    elif isinstance(cmd.op, Fock):
                        weights, means, covs = self.prepare_fock(*pars)

                    elif isinstance(cmd.op, (Ket, DensityMatrix)):
                        raise NotImplementedError(
                            "Ket and DensityMatrix preparation not implemented in the bosonic backend."
                        )

                    # If a new mode is added in the program context, then add it here
                    elif isinstance(cmd.op, _New_modes):
                        cmd.op.apply(cmd.reg, self)
                        init_weights.append([0])
                        init_means.append([0])
                        init_covs.append([0])

                    # The rest of the preparations are gaussian.
                    # TODO: initialize with Gaussian |vacuum> state
                    # directly by asking preparation methods below for
                    # the right weights, means, covs.
                    else:
                        weights, means, covs = np.array(
                            [1], dtype=complex), vac_means, vac_covs

                    init_weights[reg] = weights
                    init_means[reg] = means
                    init_covs[reg] = covs

                # Add the mode to the list of already prepared modes, unless the command was
                # just to create the new mode, in which case it checks again to see if there is
                # a subsequent non-Gaussian state creation
                if not isinstance(cmd.op, _New_modes):
                    reg_list += labels

            else:
                if type(cmd.op) in non_gauss_preps:
                    raise NotImplementedError(
                        "Non-gaussian state preparations must be the first operation for each register."
                    )

        # Assume unused modes in the circuit are vacuum states.
        # If there are any Gaussian state preparations, these will be handled
        # by the run_prog method
        for i in set(range(nmodes)).difference(reg_list):
            init_weights[i], init_means[i], init_covs[i] = np.array(
                [1]), vac_means, vac_covs

        # Find all possible combinations of means and combs of the
        # Gaussians between the modes.
        mean_combos = it.product(*init_means)
        cov_combos = it.product(*init_covs)

        # Tensor product of the weights.
        tensored_weights = kron_list(init_weights)
        # De-nest the means iterator.
        tensored_means = np.array([np.concatenate(tup) for tup in mean_combos],
                                  dtype=complex)
        # Stack covs appropriately.
        tensored_covs = np.array([block_diag(*tup) for tup in cov_combos])

        # Declare circuit attributes.
        self.circuit.weights = tensored_weights
        self.circuit.means = tensored_means
        self.circuit.covs = tensored_covs
示例#12
0
    def compile(self, seq: Sequence[Command],
                registers: Sequence[RegRef]) -> Sequence[Command]:
        """Class-specific circuit compilation method.

        If additional compilation logic is required, child classes can redefine this method.

        Args:
            seq (Sequence[Command]): quantum circuit to modify
            registers (Sequence[RegRef]): quantum registers

        Returns:
            Sequence[Command]: modified circuit

        Raises:
            CircuitError: the given circuit cannot be validated to belong to this circuit class
        """
        # registers is not used here, but may be used if the method is overwritten pylint: disable=unused-argument
        if self.graph is not None:
            # check topology
            DAG = pu.list_to_DAG(seq)

            # relabel the DAG nodes to integers, with attributes
            # specifying the operation name. This allows them to be
            # compared, rather than using Command objects.
            mapping_name, mapping_args, mapping_modes = {}, {}, {}
            for i, n in enumerate(DAG.nodes()):
                mapping_name[i] = n.op.__class__.__name__
                mapping_args[i] = n.op.p
                mapping_modes[i] = tuple(m.ind for m in n.reg)

            circuit = nx.convert_node_labels_to_integers(DAG)
            nx.set_node_attributes(circuit, mapping_name, name="name")
            nx.set_node_attributes(circuit, mapping_args, name="args")
            nx.set_node_attributes(circuit, mapping_modes, name="modes")

            def node_match(n1, n2):
                """Returns True if both nodes have the same name and modes"""
                return n1["name"] == n2["name"] and n1["modes"] == n2["modes"]

            GM = nx.algorithms.isomorphism.DiGraphMatcher(
                self.graph, circuit, node_match)

            # check if topology matches
            if not GM.is_isomorphic():
                raise pu.CircuitError(
                    "Program cannot be used with the compiler '{}' "
                    "due to incompatible topology.".format(self.short_name))

            # check if hard-coded parameters match
            G1nodes = self.graph.nodes().data()
            G2nodes = circuit.nodes().data()

            for n1, n2 in GM.mapping.items():
                for x, y in zip(G1nodes[n1]["args"], G2nodes[n2]["args"]):
                    if x != y and not (isinstance(x, sym.Symbol)
                                       or isinstance(y, sym.Expr)):
                        raise CircuitError(
                            "Program cannot be used with the compiler '{}' "
                            "due to incompatible parameter values.".format(
                                self.short_name))

        return seq
    def compile(self, seq, registers):
        # the number of modes in the provided program
        n_modes = len(registers)

        # Number of modes must be even
        if n_modes % 2 != 0:
            raise CircuitError(
                "The X series only supports programs with an even number of modes."
            )
        half_n_modes = n_modes // 2
        # Call the GBS compiler to do basic measurement validation.
        # The GBS compiler also merges multiple measurement commands
        # into a single MeasureFock command at the end of the circuit.
        seq = GBS().compile(seq, registers)

        # ensure that all modes are measured
        if len(seq[-1].reg) != n_modes:
            raise CircuitError("All modes must be measured.")

        # Check circuit begins with two-mode squeezers
        # --------------------------------------------
        A, B, C = group_operations(seq, lambda x: isinstance(x, ops.S2gate))
        # If there are no two-mode squeezers add squeezers at the beginning with squeezing param equal to zero.
        if B == []:
            initS2 = [
                Command(ops.S2gate(0, 0),
                        [registers[i], registers[i + half_n_modes]])
                for i in range(half_n_modes)
            ]
            seq = initS2 + seq
            A, B, C = group_operations(seq,
                                       lambda x: isinstance(x, ops.S2gate))

        if A != []:
            raise CircuitError(
                "There can be no operations before the S2gates.")

        regrefs = set()

        if B:
            # get set of circuit registers as a tuple for each S2gate
            regrefs = {(cmd.reg[0].ind, cmd.reg[1].ind) for cmd in B}

        # the set of allowed mode-tuples the S2gates must have
        allowed_modes = set(
            zip(range(0, half_n_modes), range(half_n_modes, n_modes)))

        if not regrefs.issubset(allowed_modes):
            raise CircuitError("S2gates do not appear on the correct modes.")

        # determine which modes do not have input S2gates specified
        missing = allowed_modes - regrefs

        for i, j in missing:
            # insert S2gates with 0 squeezing
            B.insert(0, Command(ops.S2gate(0, 0),
                                [registers[i], registers[j]]))

        # get list of circuit registers as a tuple for each S2gate
        regrefs = [(cmd.reg[0].ind, cmd.reg[1].ind) for cmd in B]

        # merge S2gates
        if len(regrefs) > half_n_modes:
            for mode, indices in list_duplicates(regrefs):
                r = 0
                phi = 0

                for k, i in enumerate(sorted(indices, reverse=True)):
                    removed_cmd = B.pop(i)
                    r += removed_cmd.op.p[0]
                    phi_new = removed_cmd.op.p[1]

                    if k > 0 and phi_new != phi:
                        raise CircuitError(
                            "Cannot merge S2gates with different phase values."
                        )

                    phi = phi_new

                i, j = mode
                B.insert(
                    indices[0],
                    Command(ops.S2gate(r, phi), [registers[i], registers[j]]))

        meas_seq = [C[-1]]
        seq = GaussianUnitary().compile(C[:-1], registers)

        # extract the compiled symplectic matrix
        if seq == []:
            S = np.identity(2 * n_modes)
            used_modes = list(range(n_modes))
        else:
            S = seq[0].op.p[0]
            # determine the modes that are acted on by the symplectic transformation
            used_modes = [x.ind for x in seq[0].reg]

        if not np.allclose(S @ S.T, np.identity(len(S))):
            raise CircuitError(
                "The operations after squeezing do not correspond to an interferometer."
            )

        if len(used_modes) != n_modes:
            # The symplectic transformation acts on a subset of
            # the programs registers. We must expand the symplectic
            # matrix to one that acts on all registers.
            # simply extract the computed symplectic matrix
            S = expand(seq[0].op.p[0], used_modes, n_modes)

        U = S[:n_modes, :n_modes] - 1j * S[:n_modes, n_modes:]
        U11 = U[:half_n_modes, :half_n_modes]
        U12 = U[:half_n_modes, half_n_modes:]
        U21 = U[half_n_modes:, :half_n_modes]
        U22 = U[half_n_modes:, half_n_modes:]
        if not np.allclose(U12, 0) or not np.allclose(U21, 0):
            # Not a bipartite graph
            raise CircuitError(
                "The applied unitary cannot mix between the modes {}-{} and modes {}-{}."
                .format(0, half_n_modes - 1, half_n_modes, n_modes - 1))

        if not np.allclose(U11, U22):
            # Not a symmetric bipartite graph
            raise CircuitError(
                "The applied unitary on modes {}-{} must be identical to the applied unitary on modes {}-{}."
                .format(0, half_n_modes - 1, half_n_modes, n_modes - 1))
        U1 = ops.Interferometer(U11,
                                mesh="rectangular_symmetric",
                                drop_identity=False)._decompose(
                                    registers[:half_n_modes])
        U2 = copy.deepcopy(U1)

        for Ui in U2:
            Ui.reg = [registers[r.ind + half_n_modes] for r in Ui.reg]

        return B + U1 + U2 + meas_seq