def binary_arithmetic(pp, qq, p, q): """Test the correctness of basic binary arithmetic expressions.""" assert par_evaluate(pp + qq) == pytest.approx(p + q) assert par_evaluate(pp - qq) == pytest.approx(p - q) assert par_evaluate(pp * qq) == pytest.approx(p * q) assert par_evaluate(pp / qq) == pytest.approx(p / q) assert par_evaluate(pp**qq) == pytest.approx(p**q)
def test_par_evaluate_dtype_TF(self, p, dtype): """Test the TF parameter evaluation works when a dtype is provided""" pytest.importorskip("tensorflow", minversion="2.0") import tensorflow as tf x = FreeParameter("x") x.val = tf.Variable(p) res = par_evaluate(x, dtype=dtype) assert res.dtype is tf.as_dtype(dtype)
def test_par_evaluate(self, p): x = FreeParameter("x") with pytest.raises(ParameterError, match="unbound parameter with no default value"): par_evaluate(x) # val only x.val = p assert np.all(par_evaluate(x) == p) # default only x.val = None x.default = p assert np.all(par_evaluate(x) == p) # both val and default x.val = p x.default = 0.0 assert np.all(par_evaluate(x) == p)
def test_zgate_decompose(self, backend, hbar, applied_cmds): """Test parameter processing occuring within the Zgate._decompose method.""" import tensorflow as tf mapping = {"p": tf.Variable(0.1)} prog = self.create_program(sf.ops.Zgate, mapping) # verify bound parameters are correct assert prog.free_params["p"].val is mapping["p"] # assert executed program is constructed correctly eng = sf.LocalEngine(backend) result = eng.run(prog, args=mapping) assert len(applied_cmds) == 1 assert isinstance(applied_cmds[0].op, sf.ops.Dgate) assert par_evaluate(applied_cmds[0].op.p[0]) == mapping["p"] / np.sqrt(2 * hbar) assert applied_cmds[0].op.p[1] == np.pi / 2
def test_gate_dagger(self, G, monkeypatch): """Test the dagger functionality of the gates""" G2 = G.H assert not G.dagger assert G2.dagger def dummy_apply(self, reg, backend, **kwargs): """Dummy apply function, used to store the evaluated params""" self.res = par_evaluate(self.p) with monkeypatch.context() as m: # patch the standard Operation class apply method # with our dummy method, that stores the applied parameter # in the attribute res. This allows us to extract # and verify the parameter was properly negated. m.setattr(G2.__class__, "_apply", dummy_apply) G2.apply([], None) orig_params = par_evaluate(G2.p) applied_params = G2.res # dagger should negate the first param assert applied_params == [-orig_params[0]] + orig_params[1:]
def compile(self, seq, registers): """Try to arrange a quantum circuit into the canonical Symplectic form. This method checks whether the circuit can be implemented as a sequence of Gaussian operations. If the answer is yes it arranges them in the canonical order with displacement at the end. 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 a Gaussian unitary """ # Check which modes are actually being used used_modes = [] for operations in seq: modes = [modes_label.ind for modes_label in operations.reg] used_modes.append(modes) # pylint: disable=consider-using-set-comprehension used_modes = list( set([item for sublist in used_modes for item in sublist])) # dictionary mapping the used modes to consecutive non-negative integers dict_indices = {used_modes[i]: i for i in range(len(used_modes))} nmodes = len(used_modes) # This is the identity transformation in phase-space, multiply by the identity and add zero Snet = np.identity(2 * nmodes) rnet = np.zeros(2 * nmodes) # Now we will go through each operation in the sequence `seq` and apply it in quadrature space # We will keep track of the net transforation in the Symplectic matrix `Snet` and the quadrature # vector `rnet`. for operations in seq: name = operations.op.__class__.__name__ params = par_evaluate(operations.op.p) modes = [modes_label.ind for modes_label in operations.reg] if name == "Dgate": rnet = rnet + expand_vector( params[0] * (np.exp(1j * params[1])), dict_indices[modes[0]], nmodes) else: if name == "Rgate": S = expand(rotation(params[0]), dict_indices[modes[0]], nmodes) elif name == "Sgate": S = expand(squeezing(params[0], params[1]), dict_indices[modes[0]], nmodes) elif name == "S2gate": S = expand( two_mode_squeezing(params[0], params[1]), [dict_indices[modes[0]], dict_indices[modes[1]]], nmodes, ) elif name == "Interferometer": S = expand(interferometer(params[0]), [dict_indices[mode] for mode in modes], nmodes) elif name == "GaussianTransform": S = expand(params[0], [dict_indices[mode] for mode in modes], nmodes) elif name == "BSgate": S = expand( beam_splitter(params[0], params[1]), [dict_indices[modes[0]], dict_indices[modes[1]]], nmodes, ) elif name == "MZgate": v = np.exp(1j * params[0]) u = np.exp(1j * params[1]) U = 0.5 * np.array([[u * (v - 1), 1j * (1 + v)], [1j * u * (1 + v), 1 - v]]) S = expand( interferometer(U), [dict_indices[modes[0]], dict_indices[modes[1]]], nmodes, ) Snet = S @ Snet rnet = S @ rnet # Having obtained the net displacement we simply convert it into complex notation alphas = 0.5 * (rnet[0:nmodes] + 1j * rnet[nmodes:2 * nmodes]) # And now we just pass the net transformation as a big Symplectic operation plus displacements ord_reg = [r for r in list(registers) if r.ind in used_modes] ord_reg = sorted(list(ord_reg), key=lambda x: x.ind) if np.allclose(Snet, np.identity(2 * nmodes)): A = [] else: A = [Command(ops.GaussianTransform(Snet), ord_reg)] B = [ Command(ops.Dgate(np.abs(alphas[i]), np.angle(alphas[i])), ord_reg[i]) for i in range(len(ord_reg)) if not np.allclose(alphas[i], 0.0) ] return A + B
def dummy_apply(self, reg, backend, **kwargs): """Dummy apply function, used to store the evaluated params""" self.res = par_evaluate(self.p)
def test_parameter_unary_negation(self, p): """Test unary negation works as expected.""" pp = FreeParameter("x") pp.val = p assert par_evaluate(-p) == pytest.approx(-p) assert par_evaluate(-pp) == pytest.approx(-p)
def test_par_evaluate_dtype_numpy(self, p, dtype): """Test the numpy parameter evaluation works when a dtype is provided""" x = FreeParameter("x") x.val = p res = par_evaluate(x, dtype=dtype) assert res.dtype.type is dtype
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
def program_equivalence(prog1, prog2, compare_params=True, atol=1e-6, rtol=0): r"""Checks if two programs are equivalent. This function converts the program lists into directed acyclic graphs, and runs the NetworkX `is_isomorphic` graph function in order to determine if the two programs are equivalent. Note: when checking for parameter equality between two parameters :math:`a` and :math:`b`, we use the following formula: .. math:: |a - b| \leq (\texttt{atol} + \texttt{rtol}\times|b|) Args: prog1 (strawberryfields.program.Program): quantum program prog2 (strawberryfields.program.Program): quantum program compare_params (bool): Set to ``False`` to turn of comparing program parameters; equivalency will only take into account the operation order. atol (float): the absolute tolerance parameter for checking quantum operation parameter equality rtol (float): the relative tolerance parameter for checking quantum operation parameter equality Returns: bool: returns ``True`` if two quantum programs are equivalent """ DAG1 = list_to_DAG(prog1.circuit) DAG2 = list_to_DAG(prog2.circuit) circuit = [] for G in [DAG1, DAG2]: # relabel the DAG nodes to integers circuit.append(nx.convert_node_labels_to_integers(G)) # add node attributes to store the operation name and parameters name_mapping = {i: n.op.__class__.__name__ for i, n in enumerate(G.nodes())} parameter_mapping = {i: par_evaluate(n.op.p) for i, n in enumerate(G.nodes())} # CXgate and BSgate are not symmetric wrt permuting the order of the two # modes it acts on; i.e., the order of the wires matter wire_mapping = {} for i, n in enumerate(G.nodes()): if n.op.__class__.__name__ == "CXgate": if np.allclose(n.op.p[0], 0): # if the CXgate parameter is 0, wire order doesn't matter wire_mapping[i] = 0 else: # if the CXgate parameter is not 0, order matters wire_mapping[i] = [j.ind for j in n.reg] elif n.op.__class__.__name__ == "BSgate": if np.allclose([j % np.pi for j in par_evaluate(n.op.p)], [np.pi / 4, np.pi / 2]): # if the beamsplitter is *symmetric*, then the order of the # wires does not matter. wire_mapping[i] = 0 else: # beamsplitter is not symmetric, order matters wire_mapping[i] = [j.ind for j in n.reg] else: # not a CXgate or a BSgate, order of wires doesn't matter wire_mapping[i] = 0 # TODO: at the moment, we do not check for whether an empty # wire will match an operation with trivial parameters. # Maybe we can do this in future, but this is a subgraph # isomorphism problem and much harder. nx.set_node_attributes(circuit[-1], name_mapping, name="name") nx.set_node_attributes(circuit[-1], parameter_mapping, name="p") nx.set_node_attributes(circuit[-1], wire_mapping, name="w") def node_match(n1, n2): """Returns True if both nodes have the same name and same parameters, within a certain tolerance""" name_match = n1["name"] == n2["name"] p_match = np.allclose(n1["p"], n2["p"], atol=atol, rtol=rtol) wire_match = n1["w"] == n2["w"] if compare_params: return name_match and p_match and wire_match return name_match and wire_match # check if circuits are equivalent return nx.is_isomorphic(circuit[0], circuit[1], node_match)
def to_xir(prog: Program, **kwargs) -> xir.Program: """Convert a Strawberry Fields Program to an XIR Program. Args: prog (Program): the Strawberry Fields program Keyword Args: add_decl (bool): Whether gate and output declarations should be added to the XIR program. Default is ``False``. Returns: xir.Program """ xir_prog = xir.Program() add_decl = kwargs.get("add_decl", False) if isinstance(prog, TDMProgram): xir_prog.add_option("_type_", "tdm") xir_prog.add_option("N", prog.N) for i, p in enumerate(prog.tdm_params): xir_prog.add_constant(f"p{i}", _listr(p)) if prog.name: xir_prog.add_option("_name_", prog.name) if prog.target: xir_prog.add_option("target", prog.target) # pylint: disable=protected-access if "cutoff_dim" in prog.backend_options: xir_prog.add_option("cutoff_dim", prog.backend_options["cutoff_dim"]) if "shots" in prog.run_options: xir_prog.add_option("shots", prog.run_options["shots"]) # fill in the quantum circuit for cmd in prog.circuit or []: name = cmd.op.__class__.__name__ wires = tuple(i.ind for i in cmd.reg) if "Measure" in name: if add_decl: output_decl = xir.Declaration(name, type_="out", wires=wires) xir_prog.add_declaration(output_decl) params = {} if cmd.op.p: # argument is quadrature phase a = cmd.op.p[0] if a in getattr(prog, "loop_vars", ()): params["phi"] = a.name else: params["phi"] = a # special case to take into account 'select' keyword argument if cmd.op.select is not None: params["select"] = cmd.op.select if name == "MeasureFock": # special case to take into account 'dark_counts' keyword argument if cmd.op.dark_counts is not None: params["dark_counts"] = cmd.op.dark_counts else: if add_decl: if name not in [ gdecl.name for gdecl in xir_prog.declarations["gate"] ]: params = [f"p{i}" for i, _ in enumerate(cmd.op.p)] gate_decl = xir.Declaration(name, type_="gate", params=params, wires=tuple(range(len(wires)))) xir_prog.add_declaration(gate_decl) params = [] for i, a in enumerate(cmd.op.p): if sfpar.par_is_symbolic(a): # try to evaluate symbolic parameter try: a = sfpar.par_evaluate(a) except sfpar.ParameterError: # if a tdm param if a in getattr(prog, "loop_vars", ()): a = a.name # if a pure symbol (free parameter), convert to string elif a.is_symbol: a = a.name # else, assume it's a symbolic function and replace all free parameters # with string representations else: symbolic_func = a.copy() for s in symbolic_func.free_symbols: symbolic_func = symbolic_func.subs(s, s.name) a = str(symbolic_func) elif isinstance(a, str): pass elif isinstance(a, Iterable): # if an iterable, make sure it only consists of lists and Python types a = _listr(a) params.append(a) op = xir.Statement(name, params, wires) xir_prog.add_statement(op) return xir_prog
def test_decomposition(self, tol): """Test that a graph is correctly decomposed""" n = 3 prog = sf.Program(2*n) A = np.zeros([2*n, 2*n]) B = np.random.random([n, n]) A[:n, n:] = B A += A.T sq, U, V = dec.bipartite_graph_embed(B) G = ops.BipartiteGraphEmbed(A) cmds = G.decompose(prog.register) S = np.identity(4 * n) # calculating the resulting decomposed symplectic for cmd in cmds: # all operations should be BSgates, Rgates, or S2gates assert isinstance( cmd.op, (ops.Interferometer, ops.S2gate) ) # build up the symplectic transform modes = [i.ind for i in cmd.reg] if isinstance(cmd.op, ops.S2gate): # check that the registers are i, i+n assert len(modes) == 2 assert modes[1] == modes[0] + n r, phi = par_evaluate(cmd.op.p) assert -r in sq assert phi == 0 S = _two_mode_squeezing(r, phi, modes, 2*n) @ S if isinstance(cmd.op, ops.Interferometer): # check that each unitary only applies to half the modes assert len(modes) == n assert modes in ([0, 1, 2], [3, 4, 5]) # check matrix is unitary U1 = par_evaluate(cmd.op.p[0]) assert np.allclose(U1 @ U1.conj().T, np.identity(n), atol=tol, rtol=0) if modes[0] == 0: assert np.allclose(U1, U, atol=tol, rtol=0) else: assert modes[0] == 3 assert np.allclose(U1, V, atol=tol, rtol=0) S_U = np.vstack( [np.hstack([U1.real, -U1.imag]), np.hstack([U1.imag, U1.real])] ) S = expand(S_U, modes, 2*n) @ S # the resulting covariance state cov = S @ S.T A_res = Amat(cov)[:2*n, :2*n] # The bottom right corner of A_res should be identical to A, # up to some constant scaling factor. Check if the ratio # of all elements is one ratio = np.real_if_close(A_res[n:, :n] / B.T) ratio /= ratio[0, 0] assert np.allclose(ratio, np.ones([n, n]), atol=tol, rtol=0)
def compile(self, seq, registers): """Try to arrange a passive circuit into a single multimode passive operation This method checks whether the circuit can be implemented as a sequence of passive gates. If the answer is yes it arranges them into a single operation. Args: seq (Sequence[Command]): passive quantum circuit to modify registers (Sequence[RegRefs]): quantum registers Returns: List[Command]: compiled circuit Raises: CircuitError: the circuit does not correspond to a passive unitary """ # Check which modes are actually being used used_modes = [] for operations in seq: modes = [modes_label.ind for modes_label in operations.reg] used_modes.append(modes) used_modes = list( set(item for sublist in used_modes for item in sublist)) # dictionary mapping the used modes to consecutive non-negative integers dict_indices = {used_modes[i]: i for i in range(len(used_modes))} nmodes = len(used_modes) # We start with an identity then sequentially update with the gate transformations T = np.identity(nmodes, dtype=np.complex128) # Now we will go through each operation in the sequence `seq` and apply it to T for operations in seq: name = operations.op.__class__.__name__ params = par_evaluate(operations.op.p) modes = [modes_label.ind for modes_label in operations.reg] if name == "Rgate": G = np.exp(1j * params[0]) T = _apply_one_mode_gate(G, T, dict_indices[modes[0]]) elif name == "LossChannel": G = np.sqrt(params[0]) T = _apply_one_mode_gate(G, T, dict_indices[modes[0]]) elif name == "Interferometer": U = params[0] if U.shape == (1, 1): T = _apply_one_mode_gate(U[0, 0], T, dict_indices[modes[0]]) elif U.shape == (2, 2): T = _apply_two_mode_gate(U, T, dict_indices[modes[0]], dict_indices[modes[1]]) else: modes = [dict_indices[mode] for mode in modes] U_expand = np.eye(nmodes, dtype=np.complex128) U_expand[np.ix_(modes, modes)] = U T = U_expand @ T elif name == "PassiveChannel": T0 = params[0] if T0.shape == (1, 1): T = _apply_one_mode_gate(T0[0, 0], T, dict_indices[modes[0]]) elif T0.shape == (2, 2): T = _apply_two_mode_gate(T0, T, dict_indices[modes[0]], dict_indices[modes[1]]) else: modes = [dict_indices[mode] for mode in modes] T0_expand = np.eye(nmodes, dtype=np.complex128) T0_expand[np.ix_(modes, modes)] = T0 T = T0_expand @ T elif name == "BSgate": G = _beam_splitter_passive(params[0], params[1]) T = _apply_two_mode_gate(G, T, dict_indices[modes[0]], dict_indices[modes[1]]) elif name == "MZgate": v = np.exp(1j * params[0]) u = np.exp(1j * params[1]) U = 0.5 * np.array([[u * (v - 1), 1j * (1 + v)], [1j * u * (1 + v), 1 - v]]) T = _apply_two_mode_gate(U, T, dict_indices[modes[0]], dict_indices[modes[1]]) elif name == "sMZgate": exp_sigma = np.exp(1j * (params[0] + params[1]) / 2) delta = (params[0] - params[1]) / 2 U = exp_sigma * np.array([[np.sin(delta), np.cos(delta)], [np.cos(delta), -np.sin(delta)]]) T = _apply_two_mode_gate(U, T, dict_indices[modes[0]], dict_indices[modes[1]]) ord_reg = [r for r in list(registers) if r.ind in used_modes] ord_reg = sorted(list(ord_reg), key=lambda x: x.ind) return [Command(ops.PassiveChannel(T), ord_reg)]
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 = 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 % 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