|
| 1 | +import numpy as np |
| 2 | +from typing import Union |
| 3 | + |
| 4 | +from qiskit.circuit import ParameterVector |
| 5 | +from qiskit import QuantumCircuit |
| 6 | + |
| 7 | +from ..encoding_circuit_base import EncodingCircuitBase |
| 8 | +from ..layered_encoding_circuit import LayeredEncodingCircuit, Layer |
| 9 | + |
| 10 | + |
| 11 | +class KyriienkoEncodingCircuit(EncodingCircuitBase): |
| 12 | + r""" |
| 13 | + Collection of encoding circuits introduced by Kyriienko et al. in reference [1]. |
| 14 | +
|
| 15 | + The following circuits are implemented: |
| 16 | +
|
| 17 | + * ``chebyshev_tower`` encoding (Eq. 15), |
| 18 | + * ``chebyshev_sparse`` encoding (Eq. 14) |
| 19 | + * ``chebyshev_product`` encoding (Eq. 5). |
| 20 | +
|
| 21 | + Each encoding circuit is followed by a variational circuit as defined in reference [1], |
| 22 | + RZ-RX-RZ layers followed by entangling layers. Two arrangements are possible: |
| 23 | +
|
| 24 | + * ``HEA``: Hardware Efficient Ansatz, with consecutive entangling layers |
| 25 | + (See Figure 5a or Section IIIB in [1]) |
| 26 | + * ``ABA``: Alternating Block Ansatz with consecutive shifted entangling layers in |
| 27 | + each block (See Figure 5b or Section IIIB in [1]) |
| 28 | +
|
| 29 | + **Example: 4 qubits, a 2 dimensional feature vector, 1 encoding layer, 2 variational layers, variational arrangement ABA and Chebyshev tower encoding:** |
| 30 | +
|
| 31 | + .. plot:: |
| 32 | +
|
| 33 | + from squlearn.encoding_circuit import KyriienkoEncodingCircuit |
| 34 | + pqc = KyriienkoEncodingCircuit(4, 1, num_encoding_layers=1, num_variational_layers=2, |
| 35 | + variational_arrangement="ABA") |
| 36 | + pqc.draw(output="mpl", style={'fontsize':15,'subfontsize': 10}) |
| 37 | +
|
| 38 | +
|
| 39 | + Args: |
| 40 | + num_qubits (int): Number of qubits of the encoding circuit |
| 41 | + encoding_style (str): Style of the encoding. Options are ``'chebyshev_tower'`` (default), |
| 42 | + ``'chebyshev_sparse'`` and ``'chebyshev_product'`` |
| 43 | + (see reference [1], Equation 15, 14 and 5 respectively) |
| 44 | + variational_arrangement (str): Arrangement of the variational layers. Options are |
| 45 | + ``'HEA'`` (default) and ``'ABA'`` (see reference [1], |
| 46 | + section IIIB) |
| 47 | + num_encoding_layers (int): Number of encoding layers (default: 1) |
| 48 | + num_variational_layers (int): Number of variational layers (default: 1) |
| 49 | + rotation_gate (str): Rotation gate to use. Either ``'rx'``, ``'ry'`` or |
| 50 | + ``'rz'`` (default: ``'ry'`` as in reference [1]) |
| 51 | + num_features (int): Dimension of the feature vector (default: 1) |
| 52 | + block_width (int): Only necessary for arrangement ``'ABA'``. Width (vertical) of each |
| 53 | + blocks for the ABA arrangement (default: 2), also refered as Nb in |
| 54 | + the paper. Must be a divisor of the number of qubits |
| 55 | + block_depth (int): Only necessary for arrangement ``'ABA'``. Depth (horizontal) of each |
| 56 | + blocks for the ABA arrangement (default: 1), also refered as b in |
| 57 | + the paper. |
| 58 | +
|
| 59 | + References |
| 60 | + ---------- |
| 61 | + [1]: O. Kyriienko et al., "Solving nonlinear differential equations with differentiable |
| 62 | + quantum circuits", `arXiv:2011.10395 (2021). <https://arxiv.org/pdf/2011.10395>`_ |
| 63 | + """ |
| 64 | + |
| 65 | + def __init__( |
| 66 | + self, |
| 67 | + num_qubits: int, |
| 68 | + num_features: int = 1, |
| 69 | + encoding_style: str = "chebyshev_tower", |
| 70 | + variational_arrangement: str = "HEA", |
| 71 | + num_encoding_layers: int = 1, |
| 72 | + num_variational_layers: int = 1, |
| 73 | + rotation_gate: str = "ry", |
| 74 | + block_width: int = 2, |
| 75 | + block_depth: int = 1, |
| 76 | + ) -> None: |
| 77 | + super().__init__(num_qubits, num_features) |
| 78 | + |
| 79 | + self.num_encoding_layers = num_encoding_layers |
| 80 | + self.num_variational_layers = num_variational_layers |
| 81 | + self.variational_arrangement = variational_arrangement |
| 82 | + self.encoding_style = encoding_style |
| 83 | + self.block_width = block_width |
| 84 | + self.block_depth = block_depth |
| 85 | + self.alpha = 2.0 |
| 86 | + self.rotation_gate = rotation_gate |
| 87 | + self.num_chebyshev = self.num_qubits |
| 88 | + self.tower = True if encoding_style == "chebyshev_tower" else False |
| 89 | + |
| 90 | + if self.variational_arrangement not in ("HEA", "ABA"): |
| 91 | + raise ValueError("Arrangement must be either 'HEA' or 'ABA'") |
| 92 | + |
| 93 | + if self.num_qubits < 2: |
| 94 | + raise ValueError("Variational circuit HEE_rzrxrz requires at least two qubits.") |
| 95 | + |
| 96 | + if self.rotation_gate not in ("rx", "ry", "rz"): |
| 97 | + raise ValueError("Rotation gate must be either 'rx', 'ry' or 'rz'") |
| 98 | + |
| 99 | + @property |
| 100 | + def parameter_bounds(self) -> np.ndarray: |
| 101 | + """The bounds of the trainable parameters of the HEE_rzrxrz encoding circuit.""" |
| 102 | + return np.array([[-2.0 * np.pi, 2.0 * np.pi]] * self.num_parameters) |
| 103 | + |
| 104 | + @property |
| 105 | + def num_parameters(self) -> int: |
| 106 | + """The number of trainable parameters of the variational circuit.""" |
| 107 | + if self.variational_arrangement == "HEA": |
| 108 | + num_param = 3 * self.num_qubits * self.num_variational_layers |
| 109 | + elif self.variational_arrangement == "ABA": |
| 110 | + num_param = 3 * self.num_qubits * self.block_depth * self.num_variational_layers |
| 111 | + return num_param |
| 112 | + |
| 113 | + def get_params(self, deep: bool = True) -> dict: |
| 114 | + """ |
| 115 | + Returns hyper-parameters and their values of the Kyriienko encoding circuit |
| 116 | +
|
| 117 | + Args: |
| 118 | + deep (bool): If True, also the parameters for |
| 119 | + contained objects are returned (default=True). |
| 120 | +
|
| 121 | + Return: |
| 122 | + Dictionary with hyper-parameters and values. |
| 123 | + """ |
| 124 | + params = super().get_params() |
| 125 | + params["num_encoding_layers"] = self.num_encoding_layers |
| 126 | + params["num_variational_layers"] = self.num_variational_layers |
| 127 | + params["variational_arrangement"] = self.variational_arrangement |
| 128 | + params["encoding_style"] = self.encoding_style |
| 129 | + params["block_width"] = self.block_width |
| 130 | + params["block_depth"] = self.block_depth |
| 131 | + params["rotation_gate"] = self.rotation_gate |
| 132 | + |
| 133 | + return params |
| 134 | + |
| 135 | + def get_circuit( |
| 136 | + self, |
| 137 | + features: Union[ParameterVector, np.ndarray], |
| 138 | + parameters: Union[ParameterVector, np.ndarray], |
| 139 | + ) -> QuantumCircuit: |
| 140 | + """ |
| 141 | + Generates and returns the circuit of the Kyriienko encoding circuit |
| 142 | +
|
| 143 | + Args: |
| 144 | + features (Union[ParameterVector, np.ndarray]): The features to encode |
| 145 | + parameters (Union[ParameterVector, np.ndarray]): The parameters of the encoding circuit |
| 146 | +
|
| 147 | + Returns: |
| 148 | + QuantumCircuit: The encoding circuit |
| 149 | + """ |
| 150 | + |
| 151 | + def mapping(x, i): |
| 152 | + """Non-linear mapping for x: alpha*i*arccos(x)""" |
| 153 | + return self.alpha * i * np.arccos(x) |
| 154 | + |
| 155 | + def variational_gate_block( |
| 156 | + QC, |
| 157 | + num_layers, |
| 158 | + num_qubits, |
| 159 | + shift_parameter=0, |
| 160 | + index_offset=0, |
| 161 | + variational_arrangement="HEA", |
| 162 | + ): |
| 163 | + """ |
| 164 | + Implements an Encoding circuit with layers of RZRXRZ followed by entangling layers. |
| 165 | +
|
| 166 | + """ |
| 167 | + qubit_starting_index = shift_parameter |
| 168 | + num_qubits += shift_parameter |
| 169 | + for layer in range(num_layers): |
| 170 | + for i in range(qubit_starting_index, num_qubits): |
| 171 | + if i >= self.num_qubits: |
| 172 | + i -= self.num_qubits |
| 173 | + QC.rz(parameters[index_offset], i) |
| 174 | + QC.rx(parameters[index_offset + 1], i) |
| 175 | + QC.rz(parameters[index_offset + 2], i) |
| 176 | + index_offset += 3 |
| 177 | + if variational_arrangement == "HEA": |
| 178 | + for start in (0, 1): |
| 179 | + for i in range(start, num_qubits - 1, 2): |
| 180 | + QC.cx(i, i + 1) |
| 181 | + |
| 182 | + elif variational_arrangement == "ABA": |
| 183 | + for start in (qubit_starting_index, qubit_starting_index + 1): |
| 184 | + for i in range(start, num_qubits - 1, 2): |
| 185 | + if i + 1 < self.num_qubits: |
| 186 | + QC.cx(i, i + 1) |
| 187 | + else: |
| 188 | + QC.cx(i, 0) |
| 189 | + return QC, index_offset |
| 190 | + |
| 191 | + nfeature = len(features) |
| 192 | + |
| 193 | + if self.encoding_style == "chebyshev_product": |
| 194 | + QC = LayeredEncodingCircuit(num_qubits=self.num_qubits, num_features=self.num_features) |
| 195 | + layer = Layer(QC) |
| 196 | + {"rx": layer.Rx, "ry": layer.Ry, "rz": layer.Rz}[self.rotation_gate]( |
| 197 | + "x", encoding=np.arcsin |
| 198 | + ) |
| 199 | + QC.add_layer(layer, num_layers=self.num_encoding_layers) |
| 200 | + |
| 201 | + elif self.encoding_style in ("chebyshev_tower", "chebyshev_sparse"): |
| 202 | + QC = QuantumCircuit(self.num_qubits) |
| 203 | + for layer in range(self.num_encoding_layers): |
| 204 | + index_offset_encoding = 0 |
| 205 | + iqubit = 0 |
| 206 | + icheb = 1 |
| 207 | + |
| 208 | + inner = self.num_features |
| 209 | + outer = self.num_chebyshev |
| 210 | + |
| 211 | + for outer_ in range(outer): |
| 212 | + for inner_ in range(inner): |
| 213 | + {"rx": QC.rx, "ry": QC.ry, "rz": QC.rz}[self.rotation_gate]( |
| 214 | + mapping(features[index_offset_encoding % nfeature], icheb), |
| 215 | + iqubit % self.num_qubits, |
| 216 | + ) |
| 217 | + iqubit += 1 |
| 218 | + index_offset_encoding += 1 |
| 219 | + icheb = 1 + icheb if self.tower else 1 |
| 220 | + |
| 221 | + index_offset_variational = 0 |
| 222 | + if self.variational_arrangement == "HEA": |
| 223 | + QC = variational_gate_block(QC, self.num_variational_layers, self.num_qubits)[0] |
| 224 | + elif self.variational_arrangement == "ABA": |
| 225 | + if self.num_qubits % self.block_width != 0: |
| 226 | + raise ValueError("Block width must be a divisor of the number of qubits.") |
| 227 | + |
| 228 | + number_of_blocks = int(np.ceil(self.num_qubits / self.block_width)) # vertical blocks |
| 229 | + shifting_factor = np.floor(self.block_width / 2) |
| 230 | + for layer in range(self.num_variational_layers): |
| 231 | + if layer % 2 == 0: # even layer |
| 232 | + for block in range(number_of_blocks): |
| 233 | + QC, index_offset_variational = variational_gate_block( |
| 234 | + QC, |
| 235 | + self.block_depth, |
| 236 | + self.block_width, |
| 237 | + int(block * self.block_width), |
| 238 | + index_offset_variational, |
| 239 | + variational_arrangement="ABA", |
| 240 | + ) |
| 241 | + else: |
| 242 | + for block in range(number_of_blocks): |
| 243 | + if ( |
| 244 | + shifting_factor + block * self.block_width + self.block_width |
| 245 | + <= self.num_qubits |
| 246 | + ): |
| 247 | + QC, index_offset_variational = variational_gate_block( |
| 248 | + QC, |
| 249 | + self.block_depth, |
| 250 | + self.block_width, |
| 251 | + int(shifting_factor + block * self.block_width), |
| 252 | + index_offset_variational, |
| 253 | + variational_arrangement="ABA", |
| 254 | + ) |
| 255 | + else: # if the block is out of the range of the qubits, we need to wrap around |
| 256 | + QC, index_offset_variational = variational_gate_block( |
| 257 | + QC, |
| 258 | + self.block_depth, |
| 259 | + self.block_width, |
| 260 | + int(shifting_factor + block * self.block_width), |
| 261 | + index_offset_variational, |
| 262 | + variational_arrangement="ABA", |
| 263 | + ) |
| 264 | + QC.barrier() |
| 265 | + |
| 266 | + return QC |
0 commit comments