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.")
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)
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.")
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)
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
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)
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." )
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 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
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
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
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