from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Callable, List, Optional, Union
import numpy as np
from numpy.typing import NDArray
from qulacs import (
Observable,
ParametricQuantumCircuit,
QuantumGateBase,
QuantumState,
)
class _Axis(Enum):
"""Specifying axis. Used in inner private method in LearningCircuit."""
X = auto()
Y = auto()
Z = auto()
# Depends on x
InputFunc = Callable[[NDArray[np.float_]], float]
# Depends on theta, x
InputFuncWithParam = Callable[[float, NDArray[np.float_]], float]
@dataclass
class _PositionDetail:
"""Manage a parameter of `ParametricQuantumCircuit.positions_in_circuit`.
This class manages indexe and coefficients (optional) of gate.
Args:
gate_pos: Indices of a parameter in LearningCircuit._circuit.
coef: Coefficient of a parameter in LearningCircuit._circuit. It's a optional.
"""
gate_pos: int
coef: Optional[float]
@dataclass
class _LearningParameter:
"""Manage a parameter of `ParametricQuantumCircuit`.
This class manages index and value of parameter.
There is two member variables to note: `positions_in_circuit` and `parameter_id`.
`positions_in_circuit` is indices of parameters in `ParametricQuantumCircuit` held by `LearningCircuit`.
If you change the parameter value of the `_LearningParameter` instance, all of the parameters
specified in `positions_in_circuit` are also updated with that value.
And `parameter_id` is an index of a whole set of learning parameters.
This is used by method of `LearningCircuit` which has "parametric" in its name.
Args:
positions_in_circuit: Indices and coefficient of a parameter in LearningCircuit._circuit.
parameter_id: Index at array of learning parameter(theta).
value: Current `parameter_id`-th parameter of LearningCircuit._circuit.
is_input: Whethter this parameter is used with a input parameter.
"""
positions_in_circuit: List[_PositionDetail]
parameter_id: int
value: float
is_input: bool = field(default=False)
def __init__(self, parameter_id: int, value: float, is_input: bool = False) -> None:
self.positions_in_circuit = []
self.parameter_id = parameter_id
self.value = value
self.is_input = is_input
def append_position(self, position: int, coef: Optional[float]) -> None:
self.positions_in_circuit.append(_PositionDetail(position, coef))
@dataclass
class _InputParameter:
"""Manage transformation of an input.
`func` transforms the given input and the outcome is stored at `pos`-th parameter in `LearningCircuit._circuit`.
If the `func` needs a learning parameter, supply `companion_parameter_id` with the learning parameter's `parameter_id`.
"""
pos: int
func: Union[InputFunc, InputFuncWithParam] = field(compare=False)
companion_parameter_id: Optional[int]
[docs]@dataclass(eq=False)
class LearningCircuit:
"""Construct and run quantum circuit for QNN.
## About parameters
This class manages parameters of underlying `ParametricQuantumCircuit`.
A parameter has either type of features: learning and input.
Learning parameter represents a parameter to be optimized.
This is updated by `LearningCircuit.update_parameter()`.
Input parameter represents a placeholder of circuit input.
This is updated in a execution of `LearningCircuit.run()` while applying `func` of the parameter.
And there is a parameter being both learning and input one.
This parameter transforms its input by applying the parameter's `func` with its learning parameter.
## Execution flow
1. Set up gates by `LearningCircuit.add_*_gate()`.
2. For each execution, at first, feed input parameter with the value computed from input data `x`.
3. Apply |0> state to the circuit.
4. Compute optimized learning parameters in a certain way.
5. Update the learning parameters in the circuit with the optimized ones by `LearningCircuit.update_parameters()`.
Args:
n_qubit: The number of qubits in the circuit.
Examples:
>>> from skqulacs.circuit import LearningCircuit
>>> from skqulacs.qnn.regressor import QNNRegressor
>>> n_qubit = 2
>>> circuit = LearningCircuit(n_qubit)
>>> theta = circuit.add_parametric_RX_gate(0, 0.5)
>>> circuit.add_parametric_RY_gate(1, 0.1, share_with=theta)
>>> circuit.add_input_RZ_gate(1, np.arcsin)
>>> model = QNNRegressor(circuit)
>>> _, theta = model.fit(x_train, y_train, maxiter=1000)
>>> x_list = np.arange(x_min, x_max, 0.02)
>>> y_pred = qnn.predict(theta, x_list)
"""
n_qubit: int
# ParametricQuantumCircuit does not have a function to compare by value, so exclude from comparison of LearningCircuit for now.
_circuit: ParametricQuantumCircuit = field(init=False, compare=False)
_learning_parameter_list: List[_LearningParameter] = field(
init=False, default_factory=list
)
_input_parameter_list: List[_InputParameter] = field(
init=False, default_factory=list
)
def __post_init__(self) -> None:
self._circuit = ParametricQuantumCircuit(self.n_qubit)
[docs] def update_parameters(self, theta: List[float]) -> None:
"""Update learning parameter of the circuit with given `theta`.
Args:
theta: New learning parameters.
"""
for parameter in self._learning_parameter_list:
parameter_value = theta[parameter.parameter_id]
parameter.value = parameter_value
for pos in parameter.positions_in_circuit:
self._circuit.set_parameter(
pos.gate_pos,
parameter_value * (pos.coef or 1.0),
)
[docs] def get_parameters(self) -> List[float]:
"""Get a list of learning parameters' values."""
theta_list = [p.value for p in self._learning_parameter_list]
return theta_list
def _set_input(self, x: NDArray[np.float_]) -> None:
for parameter in self._input_parameter_list:
# Input parameter is updated here, not update_parameters(),
# because input parameter is determined with the input data `x`.
if parameter.companion_parameter_id is None:
# If `companion_parameter_id` is `None`, `func` does not need a learning parameter.
angle = parameter.func(x)
else:
theta = self._learning_parameter_list[parameter.companion_parameter_id]
angle = parameter.func(theta.value, x)
theta.value = angle
self._circuit.set_parameter(parameter.pos, angle)
[docs] def run(self, x: List[float] = list()) -> QuantumState:
"""Determine parameters for input gate based on `x` and apply the circuit to |0> state.
Arguments:
x: Input data whose shape is (n_features,).
Returns:
Quantum state applied the circuit.
"""
state = QuantumState(self.n_qubit)
state.set_zero_state()
self._set_input(np.array(x))
self._circuit.update_quantum_state(state)
return state
[docs] def run_x_no_change(self) -> QuantumState:
"""
Run the circuit while x is not changed from the previous run.
(can change parameters)
"""
state = QuantumState(self.n_qubit)
state.set_zero_state()
self._circuit.update_quantum_state(state)
return state
[docs] def backprop(self, x: List[float], obs: Observable) -> List[float]:
"""
backprop(self, x: List[float], obs)->List[Float]
xは入力の状態で、yは出力値の微分値
帰ってくるのは、それぞれのパラメータに関する微分値
例えば、出力が[0,2]
だったらパラメータの1項目は期待する出力に関係しない、2項目をa上げると回路の出力は2a上がる?
->
c++のParametricQuantumCircuitクラスを呼び出す
backprop(GeneralQuantumOperator* obs)
->うまくやってbackpropする。
現実だと不可能な演算も含むが、気にしない
"""
self._set_input(np.array(x))
ret = self._circuit.backprop(obs)
ans = [0.0] * len(self._learning_parameter_list)
for parameter in self._learning_parameter_list:
if not parameter.is_input:
for pos in parameter.positions_in_circuit:
ans[parameter.parameter_id] += ret[pos.gate_pos] * (pos.coef or 1.0)
return ans
[docs] def backprop_inner_product(
self, x: List[float], state: QuantumState
) -> List[float]:
"""
backprop(self, x: List[float], state)->List[Float]
inner_productでbackpropします。
"""
self._set_input(np.array(x))
ret = self._circuit.backprop_inner_product(state)
ans = [0.0] * len(self._learning_parameter_list)
for parameter in self._learning_parameter_list:
if not parameter.is_input:
for pos in parameter.positions_in_circuit:
ans[parameter.parameter_id] += ret[pos.gate_pos] * (pos.coef or 1.0)
return ans
def _new_parameter_position(self) -> int:
"""Return a position of a new parameter to be registered to `ParametricQuantumCircuit`.
This function does not actually register a new parameter.
"""
return self._circuit.get_parameter_count()
[docs] def add_gate(self, gate: QuantumGateBase) -> None:
"""Add arbitrary gate.
Args:
gate: Gate to add.
"""
self._circuit.add_gate(gate)
[docs] def add_X_gate(self, index: int) -> None:
"""
Args:
index: Index of qubit to add X gate.
"""
self._circuit.add_X_gate(index)
[docs] def add_Y_gate(self, index: int) -> None:
"""
Args:
index: Index of qubit to add Y gate.
"""
self._circuit.add_Y_gate(index)
[docs] def add_Z_gate(self, index: int) -> None:
"""
Args:
index: Index of qubit to add Z gate.
"""
self._circuit.add_Z_gate(index)
[docs] def add_RX_gate(self, index: int, angle: float) -> None:
"""
Args:
index: Index of qubit to add RX gate.
angle: Rotation angle.
"""
self._add_R_gate_inner(index, angle, _Axis.X)
[docs] def add_RY_gate(self, index: int, parameter: float) -> None:
"""
Args:
index: Index of qubit to add RY gate.
angle: Rotation angle.
"""
self._add_R_gate_inner(index, parameter, _Axis.Y)
[docs] def add_RZ_gate(self, index: int, parameter: float) -> None:
"""
Args:
index: Index of qubit to add RZ gate.
angle: Rotation angle.
"""
self._add_R_gate_inner(index, parameter, _Axis.Z)
[docs] def add_CNOT_gate(self, control_index: int, target_index: int) -> None:
"""
Args:
control_index: Index of control qubit.
target_index: Index of target qubit.
"""
self._circuit.add_CNOT_gate(control_index, target_index)
[docs] def add_H_gate(self, index: int) -> None:
"""
Args:
index: Index of qubit to put H gate.
"""
self._circuit.add_H_gate(index)
[docs] def add_parametric_RX_gate(
self,
index: int,
parameter: float,
share_with: Optional[int] = None,
share_with_coef: Optional[float] = None,
) -> int:
"""
Args:
index: Index of qubit to add RX gate.
parameter: Initial parameter of this gate.
share_with: parameter_id to share the parameter in `ParametricQuantumCircuit`.
share_with_coef: Coefficients for shared parameters which is `share_with`. if 'share_with' is none, share_with_coef is skiped.
Returns:
parameter_id which is added or updated.
"""
return self._add_parametric_R_gate_inner(
index, parameter, _Axis.X, share_with, share_with_coef
)
[docs] def add_parametric_RY_gate(
self,
index: int,
parameter: float,
share_with: Optional[int] = None,
share_with_coef: Optional[float] = None,
) -> int:
"""
Args:
index: Index of qubit to add RY gate.
parameter: Initial parameter of this gate.
share_with: parameter_id to share the parameter in `ParametricQuantumCircuit`.
share_with_coef: Coefficients for shared parameters which is `share_with`.
Returns:
parameter_id which is added or updated.
"""
return self._add_parametric_R_gate_inner(
index, parameter, _Axis.Y, share_with, share_with_coef
)
[docs] def add_parametric_RZ_gate(
self,
index: int,
parameter: float,
share_with: Optional[int] = None,
share_with_coef: Optional[float] = None,
) -> int:
"""
Args:
index: Index of qubit to add RZ gate.
parameter: Initial parameter of this gate.
share_with: parameter_id to share the parameter in `ParametricQuantumCircuit`.
share_with_coef: Coefficients for shared parameters which is `share_with`. if 'share_with' is none, share_with_coef is skiped.
Returns:
parameter_id which is added or updated.
"""
return self._add_parametric_R_gate_inner(
index, parameter, _Axis.Z, share_with, share_with_coef
)
[docs] def add_parametric_multi_Pauli_rotation_gate(
self, target: List[int], pauli_id: List[int], initial_angle: float
) -> None:
self._circuit.add_parametric_multi_Pauli_rotation_gate(
target, pauli_id, initial_angle
)
def _add_R_gate_inner(
self,
index: int,
angle: float,
target: _Axis,
) -> None:
if target == _Axis.X:
self._circuit.add_RX_gate(index, angle)
elif target == _Axis.Y:
self._circuit.add_RY_gate(index, angle)
elif target == _Axis.Z:
self._circuit.add_RZ_gate(index, angle)
else:
raise NotImplementedError
def _add_parametric_R_gate_inner(
self,
index: int,
parameter: float,
target: _Axis,
share_with: Optional[int],
share_with_coef: Optional[float],
) -> int:
new_gate_pos = self._new_parameter_position()
if share_with is None:
parameter_id = len(self._learning_parameter_list)
learning_parameter = _LearningParameter(
parameter_id,
parameter,
)
learning_parameter.append_position(new_gate_pos, None)
self._learning_parameter_list.append(learning_parameter)
else:
parameter_id = share_with
sharing_parameter = self._learning_parameter_list[parameter_id]
sharing_parameter.append_position(new_gate_pos, share_with_coef)
if target == _Axis.X:
self._circuit.add_parametric_RX_gate(index, parameter)
elif target == _Axis.Y:
self._circuit.add_parametric_RY_gate(index, parameter)
elif target == _Axis.Z:
self._circuit.add_parametric_RZ_gate(index, parameter)
else:
raise NotImplementedError
return parameter_id
def _add_multi_qubit_parametric_R_gate_inner(
self,
target: List[int],
pauli_id: List[int],
initial_angle: float,
share_with: Optional[int],
share_with_coef: Optional[float],
) -> int:
new_gate_pos = self._new_parameter_position()
if share_with is None:
parameter_id = len(self._learning_parameter_list)
learning_parameter = _LearningParameter(
parameter_id,
initial_angle,
)
learning_parameter.append_position(new_gate_pos, None)
self._learning_parameter_list.append(learning_parameter)
self._circuit.add_parametric_multi_Pauli_rotation_gate(
target, pauli_id, initial_angle
)
return parameter_id
def _add_input_R_gate_inner(
self,
index: int,
target: _Axis,
input_func: InputFunc,
) -> None:
self._input_parameter_list.append(
_InputParameter(self._new_parameter_position(), input_func, None)
)
# Input gate is implemented with parametric gate because this gate should be
# updated with input data in every iteration.
if target == _Axis.X:
self._circuit.add_parametric_RX_gate(index, 0.0)
elif target == _Axis.Y:
self._circuit.add_parametric_RY_gate(index, 0.0)
elif target == _Axis.Z:
self._circuit.add_parametric_RZ_gate(index, 0.0)
else:
raise NotImplementedError
def _add_parametric_input_R_gate_inner(
self,
index: int,
parameter: float,
target: _Axis,
input_func: InputFuncWithParam,
) -> None:
pos = self._circuit.get_parameter_count()
learning_parameter = _LearningParameter(
len(self._learning_parameter_list), parameter, True
)
learning_parameter.append_position(pos, None)
self._learning_parameter_list.append(learning_parameter)
self._input_parameter_list.append(
_InputParameter(pos, input_func, learning_parameter.parameter_id)
)
if target == _Axis.X:
self._circuit.add_parametric_RX_gate(index, parameter)
elif target == _Axis.Y:
self._circuit.add_parametric_RY_gate(index, parameter)
elif target == _Axis.Z:
self._circuit.add_parametric_RZ_gate(index, parameter)
else:
raise NotImplementedError
[docs] def get_circuit_info(self) -> ParametricQuantumCircuit:
return self._circuit
[docs] def get_circuit_depth(self) -> int:
return self._circuit.calculate_depth()