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, ode_solver=SolverType.DP5_SE, 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.

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.

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