Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Proto] Integrate the Pyqtorch's noisy simulation #423

Open
EthanObadia opened this issue May 20, 2024 · 3 comments
Open

[Proto] Integrate the Pyqtorch's noisy simulation #423

EthanObadia opened this issue May 20, 2024 · 3 comments
Assignees
Labels
backends Improving the backends proto Experimental features realistic-sim

Comments

@EthanObadia
Copy link
Collaborator

EthanObadia commented May 20, 2024

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 EthanObadia added the backends Improving the backends label May 20, 2024
@EthanObadia EthanObadia self-assigned this May 20, 2024
@EthanObadia EthanObadia changed the title [Backend] Integrate the Pyqtorch's noisy simulation [Proto] Integrate the Pyqtorch's noisy simulation Jun 20, 2024
@EthanObadia EthanObadia added the proto Experimental features label Jun 20, 2024
@EthanObadia
Copy link
Collaborator Author

EthanObadia commented Jun 20, 2024

What do you think @Roland-djee @gvelikova @jpmoutinho @dominikandreasseitz @rajaiitp ?

@EthanObadia
Copy link
Collaborator Author

EthanObadia commented Jun 21, 2024

I have changed my approach because the goal is not to create additional blocks but simply to add a noise parameter to the existing blocks. This parameter will allow me to add a noisy block after the existing blocks. The noise parameter is optional and initialized by default to None. It can be either an instance of Noise or a dictionary of Noise instances if we want to apply multiple types of noise to the same block. It can be initialize as follow:

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
}

Below are the key points of this prototype:

1. Modification of the PrimitiveBlock Class:
• Add an optional noise parameter to the PrimitiveBlock class.
• Add a noise method to handle the noise parameter.
• Modify the representation of the blocks (_block_title) to handle the new parameter.

In blocks/primitive.py:

class PrimitiveBlock(AbstractBlock):
    """
    Primitive blocks represent elementary unitary operations.

    #TODO: Add a description of the noise attribut

    Examples are single/multi-qubit gates or Hamiltonian evolution.
    See [`qadence.operations`](/qadence/operations.md) for a full list of
    primitive blocks.
    """
    name = "PrimitiveBlock"

    def __init__(
        self, qubit_support: tuple[int, ...], noise: Noise | dict[str, Noise] | None = None
    ):
        self._qubit_support = qubit_support
        self._noise = noise

    @property
    def qubit_support(self) -> Tuple[int, ...]:
        return self._qubit_support

    @property
    def noise(self) -> Noise | dict[str, Noise] | None:
        return self._noise

    def digital_decomposition(self) -> AbstractBlock:
        """Decomposition into purely digital gates.

        This method returns a decomposition of the Block in a
        combination of purely digital single-qubit and two-qubit
        'gates', by manual/custom knowledge of how this can be done efficiently.
        :return:
        """
        if self.noise is None:
            raise ValueError(
                "Decomposition into purely digital gates is only avalaible for unitary gate"
            )
        return self

    ...

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, AbstractBlock):
            raise TypeError(f"Cant compare {type(self)} to {type(other)}")
        if isinstance(other, type(self)):
            return self.qubit_support == other.qubit_support and self.noise == self.noise
        return False

    def _to_dict(self) -> dict:
        return {
            "type": type(self).__name__,
            "qubit_support": self.qubit_support,
            "tag": self.tag,
            "noise": self.noise._to_dict()
            if isinstance(self.noise, Noise)
            else {k: v._to_dict() for k, v in self.noise.items()}
            if self.noise
            else None,
        }

    @classmethod
    def _from_dict(cls, d: dict) -> PrimitiveBlock:
        noise = d.get("noise")
        if isinstance(noise, dict):
            noise = {k: Noise._from_dict(v) for k, v in noise.items()}
        elif noise is not None:
            noise = Noise._from_dict(noise)
        return cls(tuple(d["qubit_support"]), noise)

    ...

2.Modify All Subclasses of PrimitiveBlock:
• Update the primitive gate class to support the noise attribute.
• Update the ParametricBlock class to support the noise attribute.
• Adjust all their methods accordingly to handle this new noise parameter.

3.Add Protocols to the Noise Class:
• Implement necessary protocols in the Noise class.
In noise/protocols.py:

@dataclass
class Noise:
    BITFLIP = "BitFlip"
    PHASEFLIP = "PhaseFlip"
    PAULI_CHANNEL = "PauliChannel"
    AMPLITUDE_DAMPING = "AmplitudeDamping"
    PHASE_DAMPING = "PhaseDamping"
    GENERALIZED_AMPLITUDE_DAMPING = "GeneralizedAmplitudeDamping"
    DEPHASING = "dephasing"
    DEPOLARIZING = "depolarizing"  # check if no cap is ok for pyq
    READOUT = "readout"
    ...

4.Modify the convert_block function:
• Ensure the convert_block function in pyq can properly handle these blocks with the noise parameter.
In backends/pyqtorch/convert_ops.py:

def convert_block():
...
    elif block.noise:
        protocols = []
        error_probabilities = []
        if isinstance(block.noise, dict):
            for noise_instance in block.noise.values():
                protocols.append(noise_instance.protocol)
                error_probabilities.append(noise_instance.options.get("error_probability"))
        elif isinstance(block.noise, Noise):
            protocols.append(block.noise.protocol)
            error_probabilities.append(block.noise.options.get("error_probability"))
        return [pyq.Noisy_Sequence(primitive = block.name, noise = protocols, error_probability = error_probabilities)]
    ...

5. Add a constructor in pyq:
• Implement a constructor in pyq to automatically create two blocks: one primitive or parametric and one noise block.
• This should eliminate the need to create them manually.
In Pyqtorch directly, in circuit.py:

class Noisy_Sequence:
     def __init__(primitive: Primitive, noise: Noise | list[Noise, ...], error_probability: float | list[float,...]):

(These are the last two steps; more details will be added later.)

@EthanObadia

This comment was marked as outdated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backends Improving the backends proto Experimental features realistic-sim
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants