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.

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)

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, n_steps_hevo=100, loop_expectation=False) dataclass

Bases: BackendConfiguration

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

Determine which kind of Hamiltonian evolution algorithm to use.

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.

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.

PyQHamiltonianEvolution(qubit_support, n_qubits, block, config)

Bases: Module

Source code in qadence/backends/pyqtorch/convert_ops.py
def __init__(
    self,
    qubit_support: Tuple[int, ...],
    n_qubits: int,
    block: TimeEvolutionBlock,
    config: Configuration,
):
    super().__init__()
    self.qubit_support = qubit_support
    self.n_qubits = n_qubits
    self.param_names = config.get_param_name(block)
    self.block = block
    self.hmat: Tensor

    if isinstance(block.generator, AbstractBlock) and not block.generator.is_parametric:
        hmat = block_to_tensor(
            block.generator,
            qubit_support=self.qubit_support,
            use_full_support=False,
        )
        hmat = hmat.permute(1, 2, 0)
        self.register_buffer("hmat", hmat)
        self._hamiltonian = lambda self, values: self.hmat

    elif isinstance(block.generator, Tensor):
        m = block.generator.to(dtype=cdouble)
        hmat = block_to_tensor(
            MatrixBlock(m, qubit_support=block.qubit_support),
            qubit_support=self.qubit_support,
            use_full_support=False,
        )
        hmat = hmat.permute(1, 2, 0)
        self.register_buffer("hmat", hmat)
        self._hamiltonian = lambda self, values: self.hmat

    elif isinstance(block.generator, sympy.Basic):
        self._hamiltonian = (
            lambda self, values: values[self.param_names[1]].squeeze(3).permute(1, 2, 0)
        )
        # FIXME Why are we squeezing
    else:

        def _hamiltonian(self: PyQHamiltonianEvolution, values: dict[str, Tensor]) -> Tensor:
            hmat = _block_to_tensor_embedded(
                block.generator,  # type: ignore[arg-type]
                values=values,
                qubit_support=self.qubit_support,
                use_full_support=False,
                device=self.device,
            )
            return hmat.permute(1, 2, 0)

        self._hamiltonian = _hamiltonian

    self._time_evolution = lambda values: values[self.param_names[0]]
    self._device: torch_device = (
        self.hmat.device if hasattr(self, "hmat") else torch_device("cpu")
    )
    self._dtype: torch_dtype = self.hmat.dtype if hasattr(self, "hmat") else cdouble

dagger(values)

Dagger of the evolved operator given the current parameter values.

Source code in qadence/backends/pyqtorch/convert_ops.py
def dagger(self, values: dict[str, Tensor]) -> Tensor:
    """Dagger of the evolved operator given the current parameter values."""
    return _dagger(self.unitary(values))

jacobian_generator(values)

Approximate jacobian of the evolved operator with respect to generator parameter(s).

Source code in qadence/backends/pyqtorch/convert_ops.py
def jacobian_generator(self, values: dict[str, Tensor]) -> Tensor:
    """Approximate jacobian of the evolved operator with respect to generator parameter(s)."""
    if len(self.param_names) > 2:
        raise NotImplementedError(
            "jacobian_generator does not support generators\
                                    with more than 1 parameter."
        )

    def _generator(val: Tensor) -> Tensor:
        val_copy = values.copy()
        val_copy[self.param_names[1]] = val
        hmat = _block_to_tensor_embedded(
            self.block.generator,  # type: ignore[arg-type]
            values=val_copy,
            qubit_support=self.qubit_support,
            use_full_support=False,
            device=self.device,
        )
        return hmat.permute(1, 2, 0)

    return finitediff(
        lambda v: self._unitary(
            time_evolution=self._time_evolution(values), hamiltonian=_generator(v)
        ),
        values[self.param_names[1]],
    )

jacobian_time(values)

Approximate jacobian of the evolved operator with respect to time evolution.

Source code in qadence/backends/pyqtorch/convert_ops.py
def jacobian_time(self, values: dict[str, Tensor]) -> Tensor:
    """Approximate jacobian of the evolved operator with respect to time evolution."""
    return finitediff(
        lambda t: self._unitary(time_evolution=t, hamiltonian=self._hamiltonian(self, values)),
        values[self.param_names[0]],
    )

unitary(values)

The evolved operator given current parameter values for generator and time evolution.

Source code in qadence/backends/pyqtorch/convert_ops.py
def unitary(self, values: dict[str, Tensor]) -> Tensor:
    """The evolved operator given current parameter values for generator and time evolution."""
    return self._unitary(self._hamiltonian(self, values), self._time_evolution(values))