Source code for strawberryfields.compilers.xunitary

# Copyright 2019-2020 Xanadu Quantum Technologies Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at


# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# See the License for the specific language governing permissions and
# limitations under the License.
# pylint: disable=too-many-branches,too-many-statements
"""General interferometer compiler for the X class of circuits."""

from collections import defaultdict
import copy

import numpy as np
from thewalrus.symplectic import expand

from strawberryfields.program_utils import CircuitError, Command, group_operations

import strawberryfields.ops as ops

from .compiler import Compiler
from .gbs import GBS
from .gaussian_unitary import GaussianUnitary

def list_duplicates(seq):
    """Returns a generator representing the duplicated values in the sequence
    mapped to the indices they appear at."""
    tally = defaultdict(list)
    for i, item in enumerate(seq):
    return ((key, locs) for key, locs in tally.items() if len(locs) > 1)

[docs]class Xunitary(Compiler): r"""General interferometer or unitary compiler for the X class of circuits. This compilation routine is performed at the interferometer/unitary matrix level. This compiler accepts the following gates, decompositions, and measurements: * :class:`~.ops.S2gate` * :class:`~.ops.Rgate` * :class:`~.ops.BSgate` * :class:`~.ops.MZgate` * :class:`~.ops.Interferometer` * :class:`~.ops.BipartiteGraphEmbed` * :class:`~.ops.MeasureFock` All ``S2gate`` operations, if present, **must** be placed at the start of the program, and match the X-series topology. That is, for a device with :math:`2N` modes, the ``S2gate`` operations can only be applied to modes :math:`(m, m+N)`. Subsequent operations represent the interferometer, and may consist of any combination of :class:`~.ops.BSgate`, :class:`~.ops.MZgate`, :class:`~.ops.Interferometer`, :class:`~.ops.BipartiteGraphEmbed`, as long as the unitary on modes :math:`(0, 1,\dots, N-1)` is repeated on modes :math:`(N, N+1, \dots, 2N-1)`. The unitary will automatically be compiled to match the topology of the X-series. Finally, the circuit must complete with Fock measurements. **Example** The compiler may be used on its own: >>> prog.compile(compiler="Xunitary") Alternatively, it can be combined with an X series device specification to include additional information, such as allowed parameter ranges. >>> eng = sf.RemoteEngine("X8") >>> spec = eng.device_spec >>> prog.compile(device=spec, compiler="Xunitary") """ short_name = "Xunitary" interactive = False primitives = { "S2gate", "MeasureFock", "Rgate", "BSgate", "MZgate", "Interferometer", } decompositions = { "BipartiteGraphEmbed": { "mesh": "rectangular_symmetric", "drop_identity": False, }, }
[docs] 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