Ejemplo n.º 1
0
class PseudoAnsatz:
    n_layers = ansatz_property(name="n_layers")

    def __init__(self, n_layers):
        self.n_layers = n_layers
        self._parametrized_circuit = None

    @property
    def parametrized_circuit(self):
        if self._parametrized_circuit is None:
            self._parametrized_circuit = f"Circuit with {self.n_layers} layers"
        return self._parametrized_circuit

    @invalidates_parametrized_circuit
    def rotate(self):
        """Mock method that "alters" some characteristics of ansatz.
Ejemplo n.º 2
0
class QCBMAnsatz(Ansatz):

    supports_parametrized_circuits = True
    number_of_qubits = ansatz_property("number_of_qubits")
    topology = ansatz_property("topology")

    def __init__(
        self,
        number_of_layers: int,
        number_of_qubits: int,
        topology: str = "all",
    ):
        """
        An ansatz implementation used for running the Quantum Circuit Born Machine.

        Args:
            number_of_layers (int): number of entangling layers in the circuit.
            number_of_qubits (int): number of qubits in the circuit.
            topology (str): the topology representing the connectivity of the qubits.

        Attributes:
            number_of_qubits (int): See Args
            number_of_layers (int): See Args
            topology (str): See Args
            number_of_params: number of the parameters that need to be set for the ansatz circuit.
        """
        super().__init__(number_of_layers)
        self._number_of_qubits = number_of_qubits
        self._topology = topology
        if number_of_layers == 0:
            raise ValueError(
                "QCBMAnsatz is only defined for number_of_layers > 0.")

    @property
    def number_of_params(self) -> int:
        """
        Returns number of parameters in the ansatz.
        """
        return np.sum(self.get_number_of_parameters_by_layer())

    @property
    def n_params_per_ent_layer(self) -> int:
        if self.topology == "all":
            return int(
                (self.number_of_qubits * (self.number_of_qubits - 1)) / 2)
        elif self.topology == "line":
            return self.number_of_qubits - 1
        else:
            raise RuntimeError("Topology {} is not supported".format(
                self.topology))

    @overrides
    def _generate_circuit(self,
                          params: Optional[np.ndarray] = None) -> Circuit:
        """Builds a qcbm ansatz circuit, using the ansatz in https://advances.sciencemag.org/content/5/10/eaaw9918/tab-pdf (Fig.2 - top).

        Args:
            params (numpy.array): input parameters of the circuit (1d array).

        Returns:
            Circuit
        """
        if params is None:
            params = np.asarray([
                sympy.Symbol("theta_{}".format(i))
                for i in range(self.number_of_params)
            ])

        assert len(params) == self.number_of_params

        if self.number_of_layers == 1:
            # Only one layer, should be a single layer of rotations with Rx
            return create_layer_of_gates(self.number_of_qubits, "Rx", params)

        circuit = Circuit()
        parameter_index = 0
        for layer_index in range(self.number_of_layers):
            if layer_index == 0:
                # First layer is always 2 single qubit rotations on Rx Rz
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    "Rx",
                    params[parameter_index:parameter_index +
                           self.number_of_qubits],
                )
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    "Rz",
                    params[parameter_index +
                           self.number_of_qubits:parameter_index +
                           2 * self.number_of_qubits],
                )
                parameter_index += 2 * self.number_of_qubits
            elif (self.number_of_layers % 2 == 1
                  and layer_index == self.number_of_layers - 1):
                # Last layer for odd number of layers is rotations on Rx Rz
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    "Rz",
                    params[parameter_index:parameter_index +
                           self.number_of_qubits],
                )
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    "Rx",
                    params[parameter_index +
                           self.number_of_qubits:parameter_index +
                           2 * self.number_of_qubits],
                )
                parameter_index += 2 * self.number_of_qubits
            elif (self.number_of_layers % 2 == 0
                  and layer_index == self.number_of_layers - 2):
                # Even number of layers, second to last layer is 3 rotation layer with Rx Rz Rx
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    "Rx",
                    params[parameter_index:parameter_index +
                           self.number_of_qubits],
                )
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    "Rz",
                    params[parameter_index +
                           self.number_of_qubits:parameter_index +
                           2 * self.number_of_qubits],
                )
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    "Rx",
                    params[parameter_index +
                           2 * self.number_of_qubits:parameter_index +
                           3 * self.number_of_qubits],
                )
                parameter_index += 3 * self.number_of_qubits
            elif (self.number_of_layers % 2 == 1
                  and layer_index == self.number_of_layers - 3):
                # Odd number of layers, third to last layer is 3 rotation layer with Rx Rz Rx
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    "Rx",
                    params[parameter_index:parameter_index +
                           self.number_of_qubits],
                )
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    "Rz",
                    params[parameter_index +
                           self.number_of_qubits:parameter_index +
                           2 * self.number_of_qubits],
                )
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    "Rx",
                    params[parameter_index +
                           2 * self.number_of_qubits:parameter_index +
                           3 * self.number_of_qubits],
                )
                parameter_index += 3 * self.number_of_qubits
            elif layer_index % 2 == 1:
                # Currently on an entangling layer
                circuit += get_entangling_layer(
                    params[parameter_index:parameter_index +
                           self.n_params_per_ent_layer],
                    self.number_of_qubits,
                    "XX",
                    self.topology,
                )
                parameter_index += self.n_params_per_ent_layer
            else:
                # A normal single qubit rotation layer of Rx Rz
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    "Rx",
                    params[parameter_index:parameter_index +
                           self.number_of_qubits],
                )
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    "Rz",
                    params[parameter_index +
                           self.number_of_qubits:parameter_index +
                           2 * self.number_of_qubits],
                )
                parameter_index += 2 * self.number_of_qubits

        return circuit

    def get_number_of_parameters_by_layer(self) -> np.ndarray:
        """Determine the number of parameters needed for each layer in the ansatz

        Returns:
            A 1D array of integers 
        """
        if self.number_of_layers == 1:
            # If only one layer, then only need parameters for a single layer of Rx gates
            return np.asarray([self.number_of_qubits])

        num_params_by_layer = []
        for layer_index in range(self.number_of_layers):
            if layer_index == 0:
                # First layer is always 2 parameters per qubit for 2 single qubit rotations
                num_params_by_layer.append(self.number_of_qubits * 2)
            elif (self.number_of_layers % 2 == 1
                  and layer_index == self.number_of_layers - 1):
                # Last layer for odd number of layers is 2 layer rotations
                num_params_by_layer.append(self.number_of_qubits * 2)
            elif (self.number_of_layers % 2 == 0
                  and layer_index == self.number_of_layers - 2):
                # Even number of layers, second to last layer is 3 rotation layer
                num_params_by_layer.append(self.number_of_qubits * 3)
            elif (self.number_of_layers % 2 == 1
                  and layer_index == self.number_of_layers - 3):
                # Odd number of layers, third to last layer is 3 rotation layer
                num_params_by_layer.append(self.number_of_qubits * 3)
            elif layer_index % 2 == 1:
                # Currently on an entangling layer
                num_params_by_layer.append(self.n_params_per_ent_layer)
            else:
                # A normal single qubit rotation layer
                num_params_by_layer.append(self.number_of_qubits * 2)

        return np.asarray(num_params_by_layer)
class HEAQuantumCompilingAnsatz(Ansatz):

    supports_parametrized_circuits = True
    number_of_qubits = ansatz_property("number_of_qubits")

    def __init__(self, number_of_layers: int, number_of_qubits: int):
        """An ansatz implementation for the Hardware Efficient Quantum Compiling Ansatz
            used in https://arxiv.org/pdf/2011.12245.pdf

        Args:
            number_of_layers: number of layers in the circuit.
            number_of_qubits: number of qubits in the circuit.

        Attributes:
            number_of_qubits: See Args
            number_of_layers: See Args
        """
        if number_of_layers <= 0:
            raise ValueError("number_of_layers must be a positive integer")
        super().__init__(number_of_layers)
        assert number_of_qubits % 2 == 0
        self._number_of_qubits = number_of_qubits

    def _build_rotational_subcircuit(self, circuit: Circuit,
                                     parameters: np.ndarray) -> Circuit:
        """Add the subcircuit which includes several rotation gates acting on each qubit

        Args:
            circuit: The circuit to append to
            parameters: The variational parameters (or symbolic parameters)

        Returns:
            circuit with added rotational sub-layer
        """
        # Add RZ(theta) RX(pi/2) RZ(theta') RX(pi/2) RZ(theta'')
        for qubit_index in range(self.number_of_qubits):

            qubit_parameters = parameters[qubit_index * 3:(qubit_index + 1) *
                                          3]

            circuit += RZ(qubit_parameters[0])(qubit_index)
            circuit += RX(np.pi / 2)(qubit_index)
            circuit += RZ(qubit_parameters[1])(qubit_index)
            circuit += RX(np.pi / 2)(qubit_index)
            circuit += RZ(qubit_parameters[2])(qubit_index)

        return circuit

    def _build_circuit_layer(self, parameters: np.ndarray) -> Circuit:
        """Build circuit layer for the hardware efficient quantum compiling ansatz

        Args:
            parameters: The variational parameters (or symbolic parameters)

        Returns:
            Circuit containing a single layer of the Hardware Efficient Quantum Compiling Ansatz
        """
        circuit_layer = Circuit()

        # Add RZ(theta) RX(pi/2) RZ(theta') RX(pi/2) RZ(theta'')
        circuit_layer = self._build_rotational_subcircuit(
            circuit_layer, parameters[:3 * self.number_of_qubits])

        qubit_ids = list(range(self.number_of_qubits))
        # Add CNOT(x, x+1) for x in even(qubits)
        for control, target in zip(
                qubit_ids[::2],
                qubit_ids[1::2]):  # loop over qubits 0, 2, 4...
            circuit_layer += CNOT(control, target)

        # Add RZ(theta) RX(pi/2) RZ(theta') RX(pi/2) RZ(theta'')
        circuit_layer = self._build_rotational_subcircuit(
            circuit_layer,
            parameters[3 * self.number_of_qubits:6 * self.number_of_qubits],
        )

        # Add CNOT layer working "inside -> out", skipping every other qubit

        for qubit_index in qubit_ids[:int(self.number_of_qubits /
                                          2)][::-1][::2]:
            control = qubit_index
            target = self.number_of_qubits - qubit_index - 1
            circuit_layer += CNOT(control, target)

            if qubit_index != 0 or self.number_of_qubits % 4 == 0:
                control = self.number_of_qubits - qubit_index
                target = qubit_index - 1
                circuit_layer += CNOT(control, target)

        return circuit_layer

    @overrides
    def _generate_circuit(self,
                          parameters: Optional[np.ndarray] = None) -> Circuit:
        """Builds the ansatz circuit (based on: 2011.12245, Fig. 1)

        Args:
            params (numpy.array): input parameters of the circuit (1d array).

        Returns:
            Circuit
        """
        if parameters is None:
            parameters = self.symbols

        assert len(parameters) == self.number_of_params

        circuit = Circuit()
        for layer_index in range(self.number_of_layers):
            circuit += self._build_circuit_layer(
                parameters[layer_index *
                           self.number_of_params_per_layer:(layer_index + 1) *
                           self.number_of_params_per_layer])
        return circuit

    @property
    def number_of_params(self) -> int:
        """
        Returns number of parameters in the ansatz.
        """
        return self.number_of_params_per_layer * self.number_of_layers

    @property
    def number_of_params_per_layer(self) -> int:
        """
        Returns number of parameters in the ansatz.
        """
        return 3 * self.number_of_qubits * 2

    @property
    def symbols(self) -> List[sympy.Symbol]:
        """
        Returns a list of symbolic parameters used for creating the ansatz.
        The order of the symbols should match the order in which parameters should be passed for creating executable circuit.
        """
        return np.asarray([
            sympy.Symbol("theta_{}".format(i))
            for i in range(self.number_of_params)
        ])
Ejemplo n.º 4
0
class SingletUCCSDAnsatz(Ansatz):

    supports_parametrized_circuits = True
    transformation = ansatz_property("transformation")

    def __init__(
        self,
        number_of_spatial_orbitals: int,
        number_of_alpha_electrons: int,
        number_of_layers: int = 1,
        transformation: str = "Jordan-Wigner",
    ):
        """
        Ansatz class representing Singlet UCCSD Ansatz.

        Args:
            number_of_layers: number of layers of the ansatz. Since it's a UCCSD Ansatz, it can only be equal to 1.
            number_of_spatial_orbitals: number of spatial orbitals.
            number_of_alpha_electrons: number of alpha electrons.
            transformation: transformation used for translation between fermions and qubits.

        Attributes:
            number_of_beta_electrons: number of beta electrons (equal to number_of_alpha_electrons).
            number_of_electrons: total number of electrons (number_of_alpha_electrons + number_of_beta_electrons).
            number_of_qubits: number of qubits required for the ansatz circuit.
            number_of_params: number of the parameters that need to be set for the ansatz circuit.
        """
        super().__init__(number_of_layers=number_of_layers)
        self._number_of_layers = number_of_layers
        self._assert_number_of_layers()
        self._number_of_spatial_orbitals = number_of_spatial_orbitals
        self._number_of_alpha_electrons = number_of_alpha_electrons
        self._transformation = transformation
        self._assert_number_of_spatial_orbitals()

    @property
    def number_of_layers(self):
        return self._number_of_layers

    @invalidates_parametrized_circuit
    @number_of_layers.setter
    def number_of_layers(self, new_number_of_layers):
        self._number_of_layers = new_number_of_layers
        self._assert_number_of_layers()

    @property
    def number_of_spatial_orbitals(self):
        return self._number_of_spatial_orbitals

    @invalidates_parametrized_circuit
    @number_of_spatial_orbitals.setter
    def number_of_spatial_orbitals(self, new_number_of_spatial_orbitals):
        self._number_of_spatial_orbitals = new_number_of_spatial_orbitals
        self._assert_number_of_spatial_orbitals()

    @property
    def number_of_qubits(self):
        return self._number_of_spatial_orbitals * 2

    @property
    def number_of_alpha_electrons(self):
        return self._number_of_alpha_electrons

    @invalidates_parametrized_circuit
    @number_of_alpha_electrons.setter
    def number_of_alpha_electrons(self, new_number_of_alpha_electrons):
        self._number_of_alpha_electrons = new_number_of_alpha_electrons
        self._assert_number_of_spatial_orbitals()

    @property
    def _number_of_beta_electrons(self):
        return self._number_of_alpha_electrons

    @property
    def number_of_electrons(self):
        return self._number_of_alpha_electrons + self._number_of_beta_electrons

    @property
    def number_of_params(self) -> int:
        """
        Returns number of parameters in the ansatz.
        """
        return uccsd_singlet_paramsize(
            n_qubits=self.number_of_qubits,
            n_electrons=self.number_of_electrons,
        )

    @staticmethod
    def screen_out_operator_terms_below_threshold(
        threshold: float, fermion_generator: FermionOperator, ignore_singles=False
    ) -> Tuple[np.ndarray, FermionOperator]:
        """Screen single and double excitation operators based on a guess
            for the amplitudes
        Args:
            threshold (float): threshold to select excitations. Only those with
                absolute amplitudes above the threshold are kept.
            fermion_generator (openfermion.FermionOperator): Fermion Operator
                containing the generators for the UCC ansatz
        Returns:
            amplitudes (np.array): screened amplitudes
            new_fermion_generator (openfermion.FermionOperator): screened
                Fermion Operator
        """

        new_fermion_generator = FermionOperator()
        amplitudes = []
        for op in fermion_generator.terms:
            if abs(fermion_generator.terms[op]) > threshold or (
                len(op) == 2 and ignore_singles == True
            ):
                new_fermion_generator += FermionOperator(
                    op, fermion_generator.terms[op]
                )
                amplitudes.append(fermion_generator.terms[op])
        amplitudes = np.array(amplitudes)
        return amplitudes, new_fermion_generator

    def compute_uccsd_vector_from_fermion_generator(
        self, raw_fermion_generator: FermionOperator, screening_threshold: float = 0.0
    ) -> np.ndarray:
        _, screened_mp2_operator = self.screen_out_operator_terms_below_threshold(
            screening_threshold, raw_fermion_generator
        )

        ansatz_operator = uccsd_singlet_generator(
            np.arange(1.0, self.number_of_params + 1),
            2 * self.number_of_spatial_orbitals,
            self.number_of_electrons,
            anti_hermitian=True,
        )

        params_vector = np.zeros(self.number_of_params)

        for term, coeff in screened_mp2_operator.terms.items():
            if term in ansatz_operator.terms.keys():
                params_vector[int(ansatz_operator.terms[term]) - 1] = coeff

        return params_vector

    def generate_circuit_from_fermion_generator(
        self, raw_fermion_generator: FermionOperator, screening_threshold: float = 0.0
    ) -> Circuit:
        params_vector = self.compute_uccsd_vector_from_fermion_generator(
            raw_fermion_generator, screening_threshold
        )

        return self.get_executable_circuit(params_vector)

    @overrides
    def _generate_circuit(self, params: Optional[np.ndarray] = None) -> Circuit:
        """
        Returns a parametrizable circuit represention of the ansatz.
        Args:
            params: parameters of the circuit.
        """
        circuit = build_hartree_fock_circuit(
            self.number_of_qubits,
            self.number_of_alpha_electrons,
            self._number_of_beta_electrons,
            self._transformation,
        )
        if params is None:
            params = [
                sympy.Symbol("theta_" + str(i), real=True)
                for i in range(self.number_of_params)
            ]
        # Build UCCSD generator
        fermion_generator = uccsd_singlet_generator(
            params,
            self.number_of_qubits,
            self.number_of_electrons,
            anti_hermitian=True,
        )

        evolution_operator = exponentiate_fermion_operator(
            fermion_generator,
            self._transformation,
            self.number_of_qubits,
        )

        circuit += evolution_operator
        return circuit

    def _assert_number_of_spatial_orbitals(self):
        if self._number_of_spatial_orbitals < 2:
            raise (
                ValueError(
                    "Number of spatials orbitals must be greater or equal 2 and is {0}.".format(
                        self._number_of_spatial_orbitals
                    )
                )
            )
        if self._number_of_spatial_orbitals <= self._number_of_alpha_electrons:
            raise (
                ValueError(
                    "Number of spatial orbitals must be greater than number_of_alpha_electrons and is {0}".format(
                        self._number_of_spatial_orbitals
                    )
                )
            )

    def _assert_number_of_layers(self):
        if self._number_of_layers != 1:
            raise (
                ValueError(
                    "Number of layers must be equal to 1 for Singlet UCCSD Ansatz"
                )
            )
Ejemplo n.º 5
0
class QCBMAnsatz(Ansatz):

    supports_parametrized_circuits = True
    number_of_qubits = ansatz_property("number_of_qubits")
    topology = ansatz_property("topology")

    def __init__(
        self,
        number_of_layers: int,
        number_of_qubits: int,
        topology: str = "all",
        **topology_kwargs,
    ):
        """
        An ansatz implementation used for running the Quantum Circuit Born Machine.
        Args:
            number_of_layers (int): number of entangling layers in the circuit.
            number_of_qubits (int): number of qubits in the circuit.
            topology (str): the topology representing the connectivity of the qubits.
        Attributes:
            number_of_qubits (int): See Args
            number_of_layers (int): See Args
            topology (str): See Args
            number_of_params: number of the parameters that need to be set for the ansatz circuit.
        """
        super().__init__(number_of_layers)
        self._number_of_qubits = number_of_qubits
        self._topology = topology
        self._topology_kwargs = topology_kwargs
        if number_of_layers == 0:
            raise ValueError(
                "QCBMAnsatz is only defined for number_of_layers > 0.")

    @property
    def number_of_params(self) -> int:
        """
        Returns number of parameters in the ansatz.
        """
        return np.sum(self.get_number_of_parameters_by_layer())

    @property
    def n_params_per_ent_layer(self) -> int:
        if self.topology == "all":
            return int(
                (self.number_of_qubits * (self.number_of_qubits - 1)) / 2)
        elif self.topology == "line" or self.topology == "star":
            return self.number_of_qubits - 1
        elif self.topology == "graph":
            if "adjacency_matrix" in self._topology_kwargs.keys():
                n_params = 0
                for qubit1_index in range(0, self._number_of_qubits - 1):
                    for qubit2_index in range(qubit1_index,
                                              self._number_of_qubits):
                        if self._topology_kwargs["adjacency_matrix"][
                                qubit1_index][qubit2_index]:
                            n_params += 1
                        if self._topology_kwargs["adjacency_matrix"][
                                qubit2_index][qubit1_index]:
                            n_params += 1
                return n_params
            elif "adjacency_list" in self._topology_kwargs.keys():
                return self._topology_kwargs["adjacency_list"].shape[1]
        else:
            raise RuntimeError("Topology {} is not supported".format(
                self.topology))

    @overrides
    def _generate_circuit(self,
                          params: Optional[np.ndarray] = None) -> Circuit:
        """Builds a qcbm ansatz circuit, using the ansatz in https://advances.sciencemag.org/content/5/10/eaaw9918/tab-pdf (Fig.2 - top).
        Args:
            params (numpy.array): input parameters of the circuit (1d array).
        Returns:
            Circuit
        """
        if params is None:
            params = np.asarray([
                sympy.Symbol("theta_{}".format(i))
                for i in range(self.number_of_params)
            ])

        assert len(params) == self.number_of_params

        if self.number_of_layers == 1:
            # Only one layer, should be a single layer of rotations with RX
            return create_layer_of_gates(self.number_of_qubits, RX, params)

        circuit = Circuit()
        parameter_index = 0
        for layer_index in range(self.number_of_layers):
            if layer_index == 0:
                # First layer is always 2 single qubit rotations on RX RZ
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    RX,
                    params[parameter_index:parameter_index +
                           self.number_of_qubits],
                )
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    RZ,
                    params[parameter_index +
                           self.number_of_qubits:parameter_index +
                           2 * self.number_of_qubits],
                )
                parameter_index += 2 * self.number_of_qubits
            elif (self.number_of_layers % 2 == 1
                  and layer_index == self.number_of_layers - 1):
                # Last layer for odd number of layers is rotations on RX RZ
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    RZ,
                    params[parameter_index:parameter_index +
                           self.number_of_qubits],
                )
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    RX,
                    params[parameter_index +
                           self.number_of_qubits:parameter_index +
                           2 * self.number_of_qubits],
                )
                parameter_index += 2 * self.number_of_qubits
            elif (self.number_of_layers % 2 == 0
                  and layer_index == self.number_of_layers - 2):
                # Even number of layers, second to last layer is 3 rotation layer with RX RZ RX
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    RX,
                    params[parameter_index:parameter_index +
                           self.number_of_qubits],
                )
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    RZ,
                    params[parameter_index +
                           self.number_of_qubits:parameter_index +
                           2 * self.number_of_qubits],
                )
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    RX,
                    params[parameter_index +
                           2 * self.number_of_qubits:parameter_index +
                           3 * self.number_of_qubits],
                )
                parameter_index += 3 * self.number_of_qubits
            elif (self.number_of_layers % 2 == 1
                  and layer_index == self.number_of_layers - 3):
                # Odd number of layers, third to last layer is 3 rotation layer with RX RZ RX
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    RX,
                    params[parameter_index:parameter_index +
                           self.number_of_qubits],
                )
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    RZ,
                    params[parameter_index +
                           self.number_of_qubits:parameter_index +
                           2 * self.number_of_qubits],
                )
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    RX,
                    params[parameter_index +
                           2 * self.number_of_qubits:parameter_index +
                           3 * self.number_of_qubits],
                )
                parameter_index += 3 * self.number_of_qubits
            elif layer_index % 2 == 1:
                # Currently on an entangling layer
                circuit += get_entangling_layer(
                    params[parameter_index:parameter_index +
                           self.n_params_per_ent_layer],
                    self.number_of_qubits,
                    XX,
                    self.topology,
                )
                parameter_index += self.n_params_per_ent_layer
            else:
                # A normal single qubit rotation layer of RX RZ
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    RX,
                    params[parameter_index:parameter_index +
                           self.number_of_qubits],
                )
                circuit += create_layer_of_gates(
                    self.number_of_qubits,
                    RZ,
                    params[parameter_index +
                           self.number_of_qubits:parameter_index +
                           2 * self.number_of_qubits],
                )
                parameter_index += 2 * self.number_of_qubits

        return circuit

    def get_number_of_parameters_by_layer(self) -> np.ndarray:
        """Determine the number of parameters needed for each layer in the ansatz
        Returns:
            A 1D array of integers
        """
        if self.number_of_layers == 1:
            # If only one layer, then only need parameters for a single layer of RX gates
            return np.asarray([self.number_of_qubits])

        num_params_by_layer = []
        for layer_index in range(self.number_of_layers):
            if layer_index == 0:
                # First layer is always 2 parameters per qubit for 2 single qubit rotations
                num_params_by_layer.append(self.number_of_qubits * 2)
            elif (self.number_of_layers % 2 == 1
                  and layer_index == self.number_of_layers - 1):
                # Last layer for odd number of layers is 2 layer rotations
                num_params_by_layer.append(self.number_of_qubits * 2)
            elif (self.number_of_layers % 2 == 0
                  and layer_index == self.number_of_layers - 2):
                # Even number of layers, second to last layer is 3 rotation layer
                num_params_by_layer.append(self.number_of_qubits * 3)
            elif (self.number_of_layers % 2 == 1
                  and layer_index == self.number_of_layers - 3):
                # Odd number of layers, third to last layer is 3 rotation layer
                num_params_by_layer.append(self.number_of_qubits * 3)
            elif layer_index % 2 == 1:
                # Currently on an entangling layer
                num_params_by_layer.append(self.n_params_per_ent_layer)
            else:
                # A normal single qubit rotation layer
                num_params_by_layer.append(self.number_of_qubits * 2)

        return np.asarray(num_params_by_layer)

    def to_dict(self):
        """Creates a dictionary representing a QCBM ansatz.

        Returns:
            dictionary (dict): the dictionary
        """
        dictionary = {
            "schema": ANSATZ_SCHEMA,
            "number_of_layers": self.number_of_layers,
            "number_of_qubits": self.number_of_qubits,
            "topology": self.topology,
        }

        return dictionary

    @classmethod
    def from_dict(cls, item: dict) -> Ansatz:
        """Creates a QCBM ansatz object from an input dictionary of values.

        Returns:
            QCBMAnsatz (Ansatz): the ansatz with a given number of layers, qubits, and topology
        """
        return QCBMAnsatz(
            number_of_layers=item["number_of_layers"],
            number_of_qubits=item["number_of_qubits"],
            topology=item["topology"],
        )