Skip to content

PyQTorch

Fast differentiable statevector emulator based on PyTorch. The code is open source, hosted on Github and maintained by Pasqal.

Backend(name=BackendName.PYQTORCH, supports_ad=True, support_bp=True, supports_adjoint=True, is_remote=False, with_measurements=True, native_endianness=Endianness.BIG, engine=Engine.TORCH, with_noise=False, config=Configuration()) dataclass

Bases: Backend

PyQTorch backend.

circuit(circuit)

Return the converted circuit.

Note that to get a representation with noise, noise should be passed within the config.

PARAMETER DESCRIPTION
circuit

Original circuit

TYPE: QuantumCircuit

RETURNS DESCRIPTION
ConvertedCircuit

ConvertedCircuit instance for backend.

TYPE: ConvertedCircuit

Source code in qadence/backends/pyqtorch/backend.py
def circuit(self, circuit: QuantumCircuit) -> ConvertedCircuit:
    """Return the converted circuit.

    Note that to get a representation with noise, noise
    should be passed within the config.

    Args:
        circuit (QuantumCircuit): Original circuit

    Returns:
        ConvertedCircuit: ConvertedCircuit instance for backend.
    """
    passes = self.config.transpilation_passes
    if passes is None:
        passes = default_passes(self.config)

    original_circ = circuit
    if len(passes) > 0:
        circuit = transpile(*passes)(circuit)
    # Setting noise in the circuit.
    if self.config.noise:
        set_noise(circuit, self.config.noise)

    ops = convert_block(circuit.block, n_qubits=circuit.n_qubits, config=self.config)
    readout_noise = (
        convert_readout_noise(circuit.n_qubits, self.config.noise)
        if self.config.noise
        else None
    )
    if self.config.dropout_probability == 0:
        native = pyq.QuantumCircuit(
            circuit.n_qubits,
            ops,
            readout_noise,
        )
    else:
        native = pyq.DropoutQuantumCircuit(
            circuit.n_qubits,
            ops,
            readout_noise,
            dropout_prob=self.config.dropout_probability,
            dropout_mode=self.config.dropout_mode,
        )
    return ConvertedCircuit(native=native, abstract=circuit, original=original_circ)

convert(circuit, observable=None)

Convert an abstract circuit and an optional observable to their native representation.

Additionally, this function constructs an embedding function which maps from user-facing parameters to device parameters (read more on parameter embedding here).

Source code in qadence/backend.py
def convert(
    self, circuit: QuantumCircuit, observable: list[AbstractBlock] | AbstractBlock | None = None
) -> Converted:
    """Convert an abstract circuit and an optional observable to their native representation.

    Additionally, this function constructs an embedding function which maps from
    user-facing parameters to device parameters (read more on parameter embedding
    [here][qadence.blocks.embedding.embedding]).
    """

    def check_observable(obs_obj: Any) -> AbstractBlock:
        if isinstance(obs_obj, QubitOperator):
            from qadence.blocks.manipulate import from_openfermion

            assert len(obs_obj.terms) > 0, "Make sure to give a non-empty qubit hamiltonian"

            return from_openfermion(obs_obj)

        elif isinstance(obs_obj, (CompositeBlock, PrimitiveBlock, ScaleBlock)):
            from qadence.blocks.utils import block_is_qubit_hamiltonian

            assert block_is_qubit_hamiltonian(
                obs_obj
            ), "Make sure the QubitHamiltonian consists only of Pauli operators X, Y, Z, I"
            return obs_obj
        raise TypeError(
            "qubit_hamiltonian should be a Pauli-like AbstractBlock or a QubitOperator"
        )

    conv_circ = self.circuit(circuit)
    circ_params, circ_embedding_fn = embedding(
        conv_circ.abstract.block, self.config._use_gate_params, self.engine
    )
    params = circ_params
    if observable is not None:
        observable = observable if isinstance(observable, list) else [observable]
        conv_obs = []
        obs_embedding_fn_list = []

        for obs in observable:
            obs = check_observable(obs)
            c_obs = self.observable(obs, max(circuit.n_qubits, obs.n_qubits))
            obs_params, obs_embedding_fn = embedding(
                c_obs.abstract, self.config._use_gate_params, self.engine
            )
            params.update(obs_params)
            obs_embedding_fn_list.append(obs_embedding_fn)
            conv_obs.append(c_obs)

        def embedding_fn_dict(a: dict, b: dict) -> dict:
            embedding_dict = circ_embedding_fn(a, b)
            for o in obs_embedding_fn_list:
                embedding_dict.update(o(a, b))
            return embedding_dict

        return Converted(conv_circ, conv_obs, embedding_fn_dict, params)

    def embedding_fn(a: dict, b: dict) -> dict:
        return circ_embedding_fn(a, b)

    return Converted(conv_circ, None, embedding_fn, params)

set_block_and_readout_noises(circuit, noise, config)

Add noise on blocks and readout on circuit.

We first start by adding noise to the abstract blocks. Then we do a conversion to their native representation. Finally, we add readout.

PARAMETER DESCRIPTION
circuit

Input circuit.

TYPE: ConvertedCircuit

noise

Noise to add.

TYPE: NoiseHandler | None

Source code in qadence/backends/pyqtorch/backend.py
def set_block_and_readout_noises(
    circuit: ConvertedCircuit, noise: NoiseHandler | None, config: Configuration
) -> None:
    """Add noise on blocks and readout on circuit.

    We first start by adding noise to the abstract blocks. Then we do a conversion to their
    native representation. Finally, we add readout.

    Args:
        circuit (ConvertedCircuit): Input circuit.
        noise (NoiseHandler | None): Noise to add.
    """
    if noise:
        set_noise(circuit, noise)
        set_noise_abstract_to_native(circuit, config)
        set_readout_noise(circuit, noise)

set_noise_abstract_to_native(circuit, config)

Set noise in native blocks from the abstract ones with noise.

PARAMETER DESCRIPTION
circuit

Input converted circuit.

TYPE: ConvertedCircuit

Source code in qadence/backends/pyqtorch/backend.py
def set_noise_abstract_to_native(circuit: ConvertedCircuit, config: Configuration) -> None:
    """Set noise in native blocks from the abstract ones with noise.

    Args:
        circuit (ConvertedCircuit): Input converted circuit.
    """
    ops = convert_block(circuit.abstract.block, n_qubits=circuit.native.n_qubits, config=config)
    circuit.native = pyq.QuantumCircuit(circuit.native.n_qubits, ops, circuit.native.readout_noise)

set_readout_noise(circuit, noise)

Set readout noise in place in native.

PARAMETER DESCRIPTION
circuit

Input converted circuit.

TYPE: ConvertedCircuit

noise

Noise.

TYPE: NoiseHandler | None

Source code in qadence/backends/pyqtorch/backend.py
def set_readout_noise(circuit: ConvertedCircuit, noise: NoiseHandler) -> None:
    """Set readout noise in place in native.

    Args:
        circuit (ConvertedCircuit):  Input converted circuit.
        noise (NoiseHandler | None): Noise.
    """
    readout = convert_readout_noise(circuit.abstract.n_qubits, noise)
    if readout:
        circuit.native.readout_noise = readout

Configuration(_use_gate_params=True, use_sparse_observable=False, use_gradient_checkpointing=False, use_single_qubit_composition=False, transpilation_passes=None, algo_hevo=AlgoHEvo.EXP, ode_solver=SolverType.DP5_SE, n_steps_hevo=100, loop_expectation=False, noise=None, dropout_probability=0.0, dropout_mode=DropoutMode.ROTATIONAL) dataclass

Bases: BackendConfiguration

algo_hevo: AlgoHEvo = AlgoHEvo.EXP class-attribute instance-attribute

Determine which kind of Hamiltonian evolution algorithm to use.

dropout_mode: DropoutMode = DropoutMode.ROTATIONAL class-attribute instance-attribute

Type of quantum dropout to perform.

dropout_probability: float = 0.0 class-attribute instance-attribute

Quantum dropout probability (0 means no dropout).

loop_expectation: bool = False class-attribute instance-attribute

When computing batches of expectation values, only allocate one wavefunction.

Loop over the batch of parameters to only allocate a single wavefunction at any given time.

n_steps_hevo: int = 100 class-attribute instance-attribute

Default number of steps for the Hamiltonian evolution.

noise: NoiseHandler | None = None class-attribute instance-attribute

NoiseHandler containing readout noise applied in backend.

ode_solver: SolverType = SolverType.DP5_SE class-attribute instance-attribute

Determine which ODE solver to use for time-dependent blocks.

use_gradient_checkpointing: bool = False class-attribute instance-attribute

Use gradient checkpointing.

Recommended for higher-order optimization tasks.

use_single_qubit_composition: bool = False class-attribute instance-attribute

Composes chains of single qubit gates into a single matmul if possible.

supported_gates = list(set(OpName.list()) - set([OpName.TDAGGER])) module-attribute

The set of supported gates.

Tdagger is currently not supported.

convert_block(block, n_qubits=None, config=None)

Convert block to native Pyqtorch representation.

PARAMETER DESCRIPTION
block

Block to convert.

TYPE: AbstractBlock

n_qubits

Number of qubits. Defaults to None.

TYPE: int DEFAULT: None

config

Backend configuration instance. Defaults to None.

TYPE: Configuration DEFAULT: None

RAISES DESCRIPTION
NotImplementedError

For non supported blocks.

RETURNS DESCRIPTION
Sequence[Module | Tensor | str | Expr]

Sequence[Module | Tensor | str | sympy.Expr]: List of native operations.

Source code in qadence/backends/pyqtorch/convert_ops.py
def convert_block(
    block: AbstractBlock,
    n_qubits: int = None,
    config: Configuration = None,
) -> Sequence[Module | Tensor | str | sympy.Expr]:
    """Convert block to native Pyqtorch representation.

    Args:
        block (AbstractBlock): Block to convert.
        n_qubits (int, optional): Number of qubits. Defaults to None.
        config (Configuration, optional): Backend configuration instance. Defaults to None.

    Raises:
        NotImplementedError: For non supported blocks.

    Returns:
        Sequence[Module | Tensor | str | sympy.Expr]: List of native operations.
    """
    if isinstance(block, (Tensor, str, sympy.Expr)):  # case for hamevo generators
        if isinstance(block, Tensor):
            block = block.permute(1, 2, 0)  # put batch size in the back
        return [block]
    qubit_support = block.qubit_support
    if n_qubits is None:
        n_qubits = max(qubit_support) + 1

    if config is None:
        config = Configuration()

    noise: NoiseHandler | None = None
    if hasattr(block, "noise") and block.noise:
        noise = convert_digital_noise(block.noise)

    if isinstance(block, ScaleBlock):
        scaled_ops = convert_block(block.block, n_qubits, config)
        scale = extract_parameter(block, config=config)

        # replace underscore by dot when underscore is between two numbers in string
        if isinstance(scale, str):
            scale = replace_underscore_floats(scale)

        if isinstance(scale, str) and not config._use_gate_params:
            param = sympy_to_pyq(sympy.parse_expr(scale))
        else:
            param = scale

        return [pyq.Scale(pyq.Sequence(scaled_ops), param)]

    elif isinstance(block, TimeEvolutionBlock):
        duration = block.duration  # type: ignore [attr-defined]
        if getattr(block.generator, "is_time_dependent", False):
            config._use_gate_params = False
            duration = config.get_param_name(block)[1]
            generator = convert_block(block.generator, config=config)[0]  # type: ignore [arg-type]
        elif isinstance(block.generator, sympy.Basic):
            generator = config.get_param_name(block)[1]

        elif isinstance(block.generator, Tensor):
            m = block.generator.to(dtype=cdouble)
            generator = convert_block(
                MatrixBlock(
                    m,
                    qubit_support=qubit_support,
                    check_unitary=False,
                    check_hermitian=True,
                )
            )[0]
        else:
            generator = convert_block(block.generator, n_qubits, config)[0]  # type: ignore[arg-type]
        time_param = config.get_param_name(block)[0]

        # convert noise operators here
        noise_operators: list = [
            convert_block(noise_block, config=config)[0] for noise_block in block.noise_operators
        ]
        if len(noise_operators) > 0:
            # squeeze batch size for noise operators
            noise_operators = [
                pyq_op.tensor(full_support=qubit_support).squeeze(-1) for pyq_op in noise_operators
            ]

        return [
            pyq.HamiltonianEvolution(
                qubit_support=qubit_support,
                generator=generator,
                time=time_param,
                cache_length=0,
                duration=duration,
                solver=config.ode_solver,
                steps=config.n_steps_hevo,
                noise_operators=noise_operators,
            )
        ]

    elif isinstance(block, MatrixBlock):
        return [pyq.primitives.Primitive(block.matrix, block.qubit_support, noise=noise)]
    elif isinstance(block, CompositeBlock):
        ops = list(flatten(*(convert_block(b, n_qubits, config) for b in block.blocks)))
        if isinstance(block, AddBlock):
            return [pyq.Add(ops)]  # add
        elif is_single_qubit_chain(block) and config.use_single_qubit_composition:
            return [pyq.Merge(ops)]  # for chains of single qubit ops on the same qubit
        else:
            return [pyq.Sequence(ops)]  # for kron and chain
    elif isinstance(block, tuple(non_unitary_gateset)):
        if isinstance(block, ProjectorBlock):
            projector = getattr(pyq, block.name)
            if block.name == OpName.N:
                return [projector(target=qubit_support, noise=noise)]
            else:
                return [
                    projector(
                        qubit_support=qubit_support,
                        ket=block.ket,
                        bra=block.bra,
                        noise=noise,
                    )
                ]
        else:
            return [getattr(pyq, block.name)(qubit_support[0])]
    elif isinstance(block, tuple(single_qubit_gateset)):
        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),
                    noise=noise,
                )
            else:
                param = extract_parameter(block, config)
                op = pyq_cls(qubit_support[0], param, noise=noise)
        else:
            op = pyq_cls(qubit_support[0], noise=noise)  # type: ignore [attr-defined]
        return [op]
    elif isinstance(block, tuple(two_qubit_gateset)):
        pyq_cls = getattr(pyq, block.name)
        if isinstance(block, ParametricBlock):
            op = pyq_cls(
                qubit_support[0],
                qubit_support[1],
                extract_parameter(block, config),
                noise=noise,
            )
        else:
            op = pyq_cls(
                qubit_support[0], qubit_support[1], noise=noise  # type: ignore [attr-defined]
            )
        return [op]
    elif isinstance(block, tuple(three_qubit_gateset) + tuple(multi_qubit_gateset)):
        block_name = block.name[1:] if block.name.startswith("M") else block.name
        pyq_cls = getattr(pyq, block_name)
        if isinstance(block, ParametricBlock):
            op = pyq_cls(
                qubit_support[:-1],
                qubit_support[-1],
                extract_parameter(block, config),
                noise=noise,
            )
        else:
            if "CSWAP" in block_name:
                op = pyq_cls(
                    qubit_support[:-2], qubit_support[-2:], noise=noise  # type: ignore [attr-defined]
                )
            else:
                op = pyq_cls(
                    qubit_support[:-1], qubit_support[-1], noise=noise  # type: ignore [attr-defined]
                )
        return [op]
    else:
        raise NotImplementedError(
            f"Non supported operation of type {type(block)}. "
            "In case you are trying to run an `AnalogBlock`, make sure you "
            "specify the `device_specs` in your `Register` first."
        )

convert_digital_noise(noise)

Convert the digital noise into pyqtorch NoiseProtocol.

PARAMETER DESCRIPTION
noise

Noise to convert.

TYPE: NoiseHandler

RETURNS DESCRIPTION
NoiseProtocol | None

pyq.noise.NoiseProtocol | None: Pyqtorch native noise protocol if there are any digital noise protocols.

Source code in qadence/backends/pyqtorch/convert_ops.py
def convert_digital_noise(noise: NoiseHandler) -> pyq.noise.NoiseProtocol | None:
    """Convert the digital noise into pyqtorch NoiseProtocol.

    Args:
        noise (NoiseHandler): Noise to convert.

    Returns:
        pyq.noise.NoiseProtocol | None: Pyqtorch native noise protocol
            if there are any digital noise protocols.
    """
    digital_part = noise.filter(NoiseProtocol.DIGITAL)
    if digital_part is None:
        return None
    return pyq.noise.NoiseProtocol(
        [
            pyq.noise.NoiseProtocol(proto, option.get("error_probability"))
            for proto, option in zip(digital_part.protocol, digital_part.options)
        ]
    )

convert_readout_noise(n_qubits, noise)

Convert the readout noise into pyqtorch ReadoutNoise.

PARAMETER DESCRIPTION
n_qubits

Number of qubits

TYPE: int

noise

Noise to convert.

TYPE: NoiseHandler

RETURNS DESCRIPTION
ReadoutNoise | None

pyq.noise.ReadoutNoise | None: Pyqtorch native ReadoutNoise instance if readout is is noise.

Source code in qadence/backends/pyqtorch/convert_ops.py
def convert_readout_noise(n_qubits: int, noise: NoiseHandler) -> pyq.noise.ReadoutNoise | None:
    """Convert the readout noise into pyqtorch ReadoutNoise.

    Args:
        n_qubits (int): Number of qubits
        noise (NoiseHandler):  Noise to convert.

    Returns:
        pyq.noise.ReadoutNoise | None: Pyqtorch native ReadoutNoise instance
            if readout is is noise.
    """
    readout_part = noise.filter(NoiseProtocol.READOUT)
    if readout_part is None:
        return None

    if readout_part.protocol[0] == NoiseProtocol.READOUT.INDEPENDENT:
        return pyq.noise.ReadoutNoise(n_qubits, **readout_part.options[0])
    else:
        return pyq.noise.CorrelatedReadoutNoise(**readout_part.options[0])

extract_parameter(block, config)

Extract the parameter as string or its tensor value.

PARAMETER DESCRIPTION
block

Block to extract parameter from.

TYPE: ScaleBlock | ParametricBlock

config

Configuration instance.

TYPE: Configuration

RETURNS DESCRIPTION
str | Tensor

str | Tensor: Parameter value or symbol.

Source code in qadence/backends/pyqtorch/convert_ops.py
def extract_parameter(block: ScaleBlock | ParametricBlock, config: Configuration) -> str | Tensor:
    """Extract the parameter as string or its tensor value.

    Args:
        block (ScaleBlock | ParametricBlock): Block to extract parameter from.
        config (Configuration): Configuration instance.

    Returns:
        str | Tensor: Parameter value or symbol.
    """
    if not block.is_parametric:
        tensor_val = tensor([block.parameters.parameter], dtype=complex64)
        return (
            tensor([block.parameters.parameter], dtype=float64)
            if torch.all(tensor_val.imag == 0)
            else tensor_val
        )

    return config.get_param_name(block)[0]

replace_underscore_floats(s)

Replace underscores with periods for all floats in given string.

Needed for correct parsing of string by sympy parser.

PARAMETER DESCRIPTION
s

string expression

TYPE: str

RETURNS DESCRIPTION
str

transformed string expression

TYPE: str

Source code in qadence/backends/pyqtorch/convert_ops.py
def replace_underscore_floats(s: str) -> str:
    """Replace underscores with periods for all floats in given string.

    Needed for correct parsing of string by sympy parser.

    Args:
        s (str): string expression

    Returns:
        str: transformed string expression
    """

    # Regular expression to match floats written with underscores instead of dots
    float_with_underscore_pattern = r"""
        (?<!\w)            # Negative lookbehind to ensure not part of a word
        -?                 # Optional negative sign
        \d+                # One or more digits (before underscore)
        _                  # The underscore acting as decimal separator
        \d+                # One or more digits (after underscore)
        ([eE][-+]?\d+)?    # Optional exponent part for scientific notation
        (?!\w)             # Negative lookahead to ensure not part of a word
    """

    # Function to replace the underscore with a dot
    def underscore_to_dot(match: re.Match) -> Any:
        return match.group(0).replace("_", ".")

    # Compile the regular expression
    pattern = re.compile(float_with_underscore_pattern, re.VERBOSE)

    return pattern.sub(underscore_to_dot, s)

sympy_to_pyq(expr)

Convert sympy expression to pyqtorch ConcretizedCallable object.

PARAMETER DESCRIPTION
expr

sympy expression

TYPE: Expr

RETURNS DESCRIPTION
ConcretizedCallable

expression encoded as ConcretizedCallable

TYPE: ConcretizedCallable | Tensor

Source code in qadence/backends/pyqtorch/convert_ops.py
def sympy_to_pyq(expr: sympy.Expr) -> ConcretizedCallable | Tensor:
    """Convert sympy expression to pyqtorch ConcretizedCallable object.

    Args:
        expr (sympy.Expr): sympy expression

    Returns:
        ConcretizedCallable: expression encoded as ConcretizedCallable
    """

    # base case - independent argument
    if len(expr.args) == 0:
        try:
            res = torch.as_tensor(float(expr))
        except Exception as e:
            res = str(expr)

            if "/" in res:  # Found a rational
                res = torch.as_tensor(float(sympy.Rational(res).evalf()))
        return res

    # Recursively iterate through current function arguments
    all_results = []
    for arg in expr.args:
        res = sympy_to_pyq(arg)
        all_results.append(res)

    # deal with multi-argument (>2) sympy functions: converting to nested
    # ConcretizedCallable objects
    if len(all_results) > 2:

        def fn(x: str | ConcretizedCallable, y: str | ConcretizedCallable) -> Callable:
            return partial(ConcretizedCallable, call_name=SYMPY_TO_PYQ_MAPPING[expr.func])(  # type: ignore [no-any-return]
                abstract_args=[x, y]
            )

        concretized_callable = reduce(fn, all_results)
    else:
        concretized_callable = ConcretizedCallable(SYMPY_TO_PYQ_MAPPING[expr.func], all_results)
    return concretized_callable