Open EthanObadia opened 1 month ago
What do you think @Roland-djee @gvelikova @jpmoutinho @dominikandreasseitz @rajaiitp ?
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.)
After further consideration, step 5, which involves adding a constructor in pyq
, may not be strictly necessary.
I would like to present an alternative solution where I use the existing sequence constructors in pyq
from qadence
. This approach leverages the already established constructors:
In backends/pyqtorch/convert_ops.py:
def noisy_block(block: AbstractBlock) -> list:
operators = []
def process_block(single_block):
pyq_cls = getattr(pyq, single_block.name)
operators.append(pyq_cls(single_block.qubit_support[0]))
if isinstance(single_block.noise, dict):
for noise_instance in single_block.noise.values():
pyq_noise = getattr(pyq, noise_instance.protocol)
op = pyq_noise(single_block.qubit_support[0], noise_instance.options.get("error_probability"))
operators.append(op)
elif isinstance(single_block.noise, Noise):
pyq_noise = getattr(pyq, single_block.noise.protocol)
op = pyq_noise(single_block.qubit_support[0], single_block.noise.options.get("error_probability"))
operators.append(op)
if isinstance(block, CompositeBlock):
for sub_block in block.blocks:
process_block(sub_block)
else:
process_block(block)
return operators
def convert_block(
block: AbstractBlock, n_qubits: int = None, config: Configuration = None
) -> Sequence[Module | Tensor | str | sympy.Expr]:
...
elif isinstance(block, CompositeBlock):
if all([b.noise is None for b in block]):
ops = list(flatten(*(convert_block(b, n_qubits, config) for b in block.blocks)))
else:
ops = noisy_block(block)
if isinstance(block, AddBlock):
return [pyq.Add(ops)] # add
elif (
is_single_qubit_chain(block)
and config.use_single_qubit_composition
and all([b.noise is None for b in block])
):
return [
pyq.Merge(ops)
] # for chains of single qubit ops on the same qubit without noise
else:
return [pyq.Sequence(ops)] # for kron and chain with multiple qubits/1-qubit with noise
...
elif isinstance(block, tuple(single_qubit_gateset)):
if block.noise:
return [pyq.Sequence(noisy_block(block))]
else:
pyq_cls = getattr(pyq,block.name)
if isinstance(block, ParametricBlock):
if isinstance(block, U):
op = pyq_cls(qubit_support[0], *config.get_param_name(block))
else:
op = pyq_cls(qubit_support[0], config.get_param_name(block)[0])
else:
op = pyq_cls(qubit_support[0])
return [op]
This issue is to present a prototype for integrating
pyq
's noise gates intoqadence
. 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, calledNoisyPrimitivesBlock
. • These blocks introduce a new input parameter,noise_probability
, which allows specifying the probability of noise when creating a block. Inblocks/primitive.py
:2. Create Noisy Gates in Qadence: • Prior to this, we added the noise gates name to the
OpName
class. • Develop blocks that will represent our noisy gates withinqadence
. • Create subclasses ofNoisyPrimitiveBlock
to implement these noise gates. For instance with theBitFlip
gate. Inoperations/noise.py
:3.Modify convert_block function for pyq: • Prior to this, we created the
single_qubit_noise_gateset
type list . • During the conversion of blocks to gates forpyq
, add a condition for noise block inconvert_block
. • Ensure that the newnoise_probability
parameter is taken into account during this conversion. Inoperations/__init__.py
:In
backends/pyqtorch/convert_ops.py
: