qadence icon indicating copy to clipboard operation
qadence copied to clipboard

[Proto] Integrate the Pyqtorch's noisy simulation

Open EthanObadia opened this issue 9 months ago • 3 comments

This issue is to present a prototype for integrating pyq's noise gates into qadence. The primary goal is to ensure that noise handling is effectively incorporated without disrupting the existing functionalities. Below are the key points of this prototype:

1. Creation of NoisyPrimitivesBlocks: • New blocks derived from PrimitivesBlock have been created, called NoisyPrimitivesBlock. • These blocks introduce a new input parameter, noise_probability, which allows specifying the probability of noise when creating a block. In blocks/primitive.py :

class NoisyPrimitiveBlock(PrimitiveBlock):
    """
    NoisyPrimitiveBlock represents elementary unitary operations with noise.
    This class adds a noise probability parameter to the primitive block,
    representing the likelihood of an error occurring during the operation.
    """

    name = "NoisyPrimitiveBlock"

    def __init__(
        self, qubit_support: tuple[int, ...], noise_probability: float | tuple[float, ...]
    ):
        super().__init__(qubit_support)
        self._noise_probability = noise_probability

    @property
    def noise_probability(self) -> float | tuple[float, ...]:
        return self._noise_probability

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, NoisyPrimitiveBlock):
            return False
        return super().__eq__(other) and self.noise_probability == other.noise_probability

    def _to_dict(self) -> dict:
        block_dict = super()._to_dict()
        block_dict.update({"noise_probability": self.noise_probability})
        return block_dict

    @classmethod
    def _from_dict(cls, d: dict) -> NoisyPrimitiveBlock:
        return cls(d["qubit_support"], d["noise_probability"])

    def __hash__(self) -> int:
        return hash((super().__hash__(), self.noise_probability))

    def dagger(self) -> PrimitiveBlock:
        raise ValueError("Property `dagger` not available for noise gate.")

2. Create Noisy Gates in Qadence: • Prior to this, we added the noise gates name to theOpName class. • Develop blocks that will represent our noisy gates within qadence. • Create subclasses of NoisyPrimitiveBlock to implement these noise gates. For instance with the BitFlip gate. In operations/noise.py:

class BitFlip(NoisyPrimitiveBlock):
    """The Bitflip noise gate."""

    name = OpName.BITFLIP

    def __init__(self, target: int, noise_probability: float | tuple[float, ...]):
        super().__init__((target,), noise_probability)

    @property
    def generator(self) -> None:
        raise ValueError("Property `generator` not available for non-unitary operator.")

    @property
    def eigenvalues_generator(self) -> None:
        raise ValueError("Property `eigenvalues_generator` not available for non-unitary operator.")

3.Modify convert_block function for pyq: • Prior to this, we created thesingle_qubit_noise_gateset type list . • During the conversion of blocks to gates for pyq, add a condition for noise block in convert_block. • Ensure that the new noise_probability parameter is taken into account during this conversion. In operations/__init__.py:

single_qubit_gateset = [X, Y, Z, H, I, RX, RY, RZ, U, S, SDagger, T, PHASE]
single_qubit_noise_gateset = [BitFlip]

In backends/pyqtorch/convert_ops.py:

def convert_block():
...
     elif isinstance(block, tuple(single_qubit_noise_gateset)):
         pyq_cls = getattr(pyq, block.name)
         op = pyq_cls(qubit_support[0], block.noise_probability)  # type: ignore[attr-defined]
         return [op]

EthanObadia avatar May 20 '24 07:05 EthanObadia