pyqtorch icon indicating copy to clipboard operation
pyqtorch copied to clipboard

[Proto] Noise modifications for Qadence implementation

Open EthanObadia opened this issue 8 months ago • 9 comments

This issue presents a prototype idea to modify noise for the Qadence implementation, where the noise is implemented such as:

bitflip_noise = Noise(protocol=Noise.BITFLIP, options={"error_probability": 0.5})
phaseflip_noise = Noise(protocol=Noise.PHASEFLIP, options={"error_probability": 0.2})
noise = {
    "bitflip": bitflip_noise,
    "phaseflip": phaseflip_noise
}
x = X(target = 0, noise = noise)

The goal is to modify the way single qubit gates have been implemented to minimize the number of manipulations in Qadence while maintaining consistent syntax. To achieve this, following the way noise is invoked in Qadence, we will no longer treat noise models as separate gates. Instead, they will be parameters that modify the Primitive gates, which will now be the only gates that can be directly called as seen in the example above.

Prototype:

1. Add a Noise Parameter to Primitive Gates in pyq: • Introduce a noise parameter to Primitive gates in pyq. • By default, this noise parameter is set to None.

For instance:

class X(Primitive):
    def __init__(self, target: int, noise: Noisy_protocols | dict[str, Noisy_protocols] | None = None):
        super().__init__(OPERATIONS_DICT["X"], target, noise)

2. Modification of the Primitive forward function : • If the noise parameter remains None, the gate behaves as a standard Primitive forward pass. • If the noise parameter is an instance of Noise, the Primitive forward function will call the Noise forward pass.

In primitive.py:

    def forward(
        self, state: Tensor, values: dict[str, Tensor] | Tensor = dict()
    ) -> Tensor:
        if self.noise:
            if isinstance(self.noise, dict):
                for noise_instance in self.noise.values():
                    protocol =  noise_instance.protocol_to_gate()
                    noise_gate = protocol(
                        primitive=self.unitary(values),
                        target=self.target,
                        probability= noise_instance.error_probability,
                    )
                # CALCULUS
            else:
                protocol =  self.noise.protocol_to_gate()
                noise_gate = protocol(
                    primitive = self.unitary(values),
                    target = self.target,
                    probability = self.noise.error_probability,
                )
                return noise_gate(state)
        else:
            if isinstance(state, DensityMatrix):
                # FIXME: fix error type int | tuple[int, ...] expected "int"
                # Only supports single-qubit gates
                return operator_product(
                    self.unitary(values),
                    operator_product(state, self.dagger(values), self.target),  # type: ignore [arg-type]
                    self.target,  # type: ignore [arg-type]
                )
            else:
                return apply_operator(
                    state,
                    self.unitary(values),
                    self.qubit_support,
                    len(state.size()) - 1,
                )

3. Modification of the Noise forward function : • Create the noisy primitive tensor as $X_{noisy} = X(\text{Kraus}_1 +\text{Kraus}_2)$, • Using this tensor to compute :

$$ \rho^{\prime} = X_{noisy} \rho X^{\dagger}_{noisy} = X(\text{Kraus}_1 +\text{Kraus}_2) \rho (\text{Kraus}^{\dagger}_1 +\text{Kraus}^{\dagger}_2)X^{\dagger}.$$

In noise.py (will add it later):

    def forward(
        self, state: Tensor, values: dict[str, Tensor] | Tensor = dict()
    ) -> Tensor:

EthanObadia avatar Jun 26 '24 12:06 EthanObadia