diff --git a/docs/digital_analog_qc/rydberg-hea.md b/docs/digital_analog_qc/rydberg-hea.md deleted file mode 100644 index 1530c54c..00000000 --- a/docs/digital_analog_qc/rydberg-hea.md +++ /dev/null @@ -1,116 +0,0 @@ -Qadence simplifies the execution of digital-analog -workloads on neutral atom quantum computers where the local -addressability is restricted. - -In this regime, which we will refer to as *semi-local addressing*, -the full Hamiltonian of the qubit system realized with neutral -atoms comprises the following terms: - -$$ -\mathcal{H} = \mathcal{H}_{\textrm{global}} + \mathcal{H}_{\textrm{int}} + \mathcal{H}_{\textrm{local}} -$$ - -The first two terms are the standard components of a neutral atom Hamiltonians -and read as follows: - -$$ -\mathcal{H}_{\textrm{global}} = \frac{\Omega}{2}\sum_{i}^N \left( - \textrm{cos}(\phi)\sigma^x_i - \textrm{sin}(\phi)\sigma^y_i \right) - - \delta \sum_{i}^N \hat{n}_i \\ -\mathcal{H}_{\textrm{int}} = \sum_{i tuple[float, float]: } -def feature_map( - n_qubits: int, - support: tuple[int, ...] | None = None, +def fm_parameter( + fm_type: BasisSet | type[Function] | str, param: Parameter | str = "phi", - op: RotationTypes = RX, - fm_type: BasisSet | type[Function] | str = BasisSet.FOURIER, - reupload_scaling: ReuploadScaling | Callable | str = ReuploadScaling.CONSTANT, feature_range: tuple[float, float] | None = None, target_range: tuple[float, float] | None = None, - multiplier: Parameter | TParameter | None = None, -) -> KronBlock: - """Construct a feature map of a given type. - - Arguments: - n_qubits: Number of qubits the feature map covers. Results in `support=range(n_qubits)`. - support: Puts one feature-encoding rotation gate on every qubit in `support`. n_qubits in - this case specifies the total overall qubits of the circuit, which may be wider than the - support itself, but not narrower. - param: Parameter of the feature map; you can pass a string or Parameter; - it will be set as non-trainable (FeatureParameter) regardless. - op: Rotation operation of the feature map; choose from RX, RY, RZ or PHASE. - fm_type: Basis set for data encoding; choose from `BasisSet.FOURIER` for Fourier - encoding, or `BasisSet.CHEBYSHEV` for Chebyshev polynomials of the first kind. - reupload_scaling: how the feature map scales the data that is re-uploaded for each qubit. - choose from `ReuploadScaling` enumeration or provide your own function with a single - int as input and int or float as output. - feature_range: range of data that the input data is assumed to come from. - target_range: range of data the data encoder assumes as the natural range. For example, - in Chebyshev polynomials it is (-1, 1), while for Fourier it may be chosen as (0, 2*pi). - multiplier: overall multiplier; this is useful for reuploading the feature map serially with - different scalings; can be a number or parameter/expression. - - Example: - ```python exec="on" source="material-block" result="json" - from qadence import feature_map, BasisSet, ReuploadScaling - - fm = feature_map(3, fm_type=BasisSet.FOURIER) - print(f"{fm = }") - - fm = feature_map(3, fm_type=BasisSet.CHEBYSHEV) - print(f"{fm = }") - - fm = feature_map(3, fm_type=BasisSet.FOURIER, reupload_scaling = ReuploadScaling.TOWER) - print(f"{fm = }") - ``` - """ - - # Process input - if support is None: - support = tuple(range(n_qubits)) - elif len(support) != n_qubits: - raise ValueError("Wrong qubit support supplied") - - if op not in ROTATIONS: - raise ValueError( - f"Operation {op} not supported. " - f"Please provide one from {[rot.__name__ for rot in ROTATIONS]}." - ) - +) -> Parameter | Basic: # Backwards compatibility if fm_type in ("fourier", "chebyshev", "tower"): logger.warning( @@ -108,7 +55,6 @@ def feature_map( fm_type = BasisSet.CHEBYSHEV elif fm_type == "tower": fm_type = BasisSet.CHEBYSHEV - reupload_scaling = ReuploadScaling.TOWER if isinstance(param, Parameter): fparam = param @@ -144,8 +90,12 @@ def feature_map( "the given feature parameter with." ) - basis_tag = fm_type.value if isinstance(fm_type, BasisSet) else str(fm_type) + return transformed_feature + +def fm_reupload_scaling_fn( + reupload_scaling: ReuploadScaling | Callable | str = ReuploadScaling.CONSTANT, +) -> tuple[Callable, str]: # Set reupload scaling function if callable(reupload_scaling): rs_func = reupload_scaling @@ -163,8 +113,82 @@ def feature_map( else: rs_tag = reupload_scaling + return rs_func, rs_tag + + +def feature_map( + n_qubits: int, + support: tuple[int, ...] | None = None, + param: Parameter | str = "phi", + op: RotationTypes = RX, + fm_type: BasisSet | type[Function] | str = BasisSet.FOURIER, + reupload_scaling: ReuploadScaling | Callable | str = ReuploadScaling.CONSTANT, + feature_range: tuple[float, float] | None = None, + target_range: tuple[float, float] | None = None, + multiplier: Parameter | TParameter | None = None, +) -> KronBlock: + """Construct a feature map of a given type. + + Arguments: + n_qubits: Number of qubits the feature map covers. Results in `support=range(n_qubits)`. + support: Puts one feature-encoding rotation gate on every qubit in `support`. n_qubits in + this case specifies the total overall qubits of the circuit, which may be wider than the + support itself, but not narrower. + param: Parameter of the feature map; you can pass a string or Parameter; + it will be set as non-trainable (FeatureParameter) regardless. + op: Rotation operation of the feature map; choose from RX, RY, RZ or PHASE. + fm_type: Basis set for data encoding; choose from `BasisSet.FOURIER` for Fourier + encoding, or `BasisSet.CHEBYSHEV` for Chebyshev polynomials of the first kind. + reupload_scaling: how the feature map scales the data that is re-uploaded for each qubit. + choose from `ReuploadScaling` enumeration or provide your own function with a single + int as input and int or float as output. + feature_range: range of data that the input data is assumed to come from. + target_range: range of data the data encoder assumes as the natural range. For example, + in Chebyshev polynomials it is (-1, 1), while for Fourier it may be chosen as (0, 2*pi). + multiplier: overall multiplier; this is useful for reuploading the feature map serially with + different scalings; can be a number or parameter/expression. + + Example: + ```python exec="on" source="material-block" result="json" + from qadence import feature_map, BasisSet, ReuploadScaling + + fm = feature_map(3, fm_type=BasisSet.FOURIER) + print(f"{fm = }") + + fm = feature_map(3, fm_type=BasisSet.CHEBYSHEV) + print(f"{fm = }") + + fm = feature_map(3, fm_type=BasisSet.FOURIER, reupload_scaling = ReuploadScaling.TOWER) + print(f"{fm = }") + ``` + """ + + # Process input + if support is None: + support = tuple(range(n_qubits)) + elif len(support) != n_qubits: + raise ValueError("Wrong qubit support supplied") + + if op not in ROTATIONS: + raise ValueError( + f"Operation {op} not supported. " + f"Please provide one from {[rot.__name__ for rot in ROTATIONS]}." + ) + + transformed_feature = fm_parameter( + fm_type, param, feature_range=feature_range, target_range=target_range + ) + + # Backwards compatibility + if fm_type == "tower": + logger.warning("Forcing reupload scaling strategy to TOWER") + reupload_scaling = ReuploadScaling.TOWER + + basis_tag = fm_type.value if isinstance(fm_type, BasisSet) else str(fm_type) + rs_func, rs_tag = fm_reupload_scaling_fn(reupload_scaling) + # Set overall multiplier - multiplier = 1 if multiplier is None else multiplier + multiplier = 1 if multiplier is None else Parameter(multiplier) # Build feature map op_list = [] diff --git a/qadence/constructors/rydberg_feature_maps.py b/qadence/constructors/rydberg_feature_maps.py new file mode 100644 index 00000000..4cc8c09a --- /dev/null +++ b/qadence/constructors/rydberg_feature_maps.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +from typing import Callable + +import numpy as np +from sympy import Basic, Function + +from qadence.blocks import AnalogBlock, KronBlock, kron +from qadence.constructors.feature_maps import fm_parameter +from qadence.logger import get_logger +from qadence.operations import AnalogRot, AnalogRX, AnalogRY, AnalogRZ +from qadence.parameters import FeatureParameter, Parameter, VariationalParameter +from qadence.types import BasisSet, ReuploadScaling, TParameter + +logger = get_logger(__file__) + +AnalogRotationTypes = [AnalogRX, AnalogRY, AnalogRZ] + + +def rydberg_feature_map( + n_qubits: int, + param: str = "phi", + max_abs_detuning: float = 2 * np.pi * 10, + weights: list[float] | None = None, +) -> KronBlock: + """Feature map using semi-local addressing patterns. + + If not weights are specified, variational parameters are created + for the pattern + + Args: + n_qubits (int): number of qubits + param: the name of the feature parameter + max_abs_detuning: maximum value of absolute detuning for each qubit. Defaulted at 10 MHz. + weights: a list of wegiths to assign to each qubit parameter in the feature map + + Returns: + The block representing the feature map + """ + + tower_coeffs: list[float | Parameter] + tower_coeffs = ( + [VariationalParameter(f"w_{param}_{i}") for i in range(n_qubits)] + if weights is None + else weights + ) + tower_detuning = max_abs_detuning / (sum(tower_coeffs[i] for i in range(n_qubits))) + + param = FeatureParameter(param) + duration = 1000 * param / tower_detuning + return kron( + AnalogRot( + duration=duration, + delta=-tower_detuning * tower_coeffs[i], + phase=0.0, + qubit_support=(i,), + ) + for i in range(n_qubits) + ) + + +def rydberg_tower_feature_map( + n_qubits: int, param: str = "phi", max_abs_detuning: float = 2 * np.pi * 10 +) -> KronBlock: + weights = list(np.arange(1, n_qubits + 1)) + return rydberg_feature_map( + n_qubits, param=param, max_abs_detuning=max_abs_detuning, weights=weights + ) + + +def analog_feature_map( + param: str = "phi", + op: Callable[[Parameter | Basic], AnalogBlock] = AnalogRX, + fm_type: BasisSet | type[Function] | str = BasisSet.FOURIER, + reupload_scaling: ReuploadScaling | Callable | str = ReuploadScaling.CONSTANT, + feature_range: tuple[float, float] | None = None, + target_range: tuple[float, float] | None = None, + multiplier: Parameter | TParameter | None = None, +) -> AnalogBlock: + """Generate a fully analog feature map. + + Args: + param: Parameter of the feature map; you can pass a string or Parameter; + it will be set as non-trainable (FeatureParameter) regardless. + op: type of operation. Choose among AnalogRX, AnalogRY, AnalogRZ or a custom + callable function returning an AnalogBlock instance + fm_type: Basis set for data encoding; choose from `BasisSet.FOURIER` for Fourier + encoding, or `BasisSet.CHEBYSHEV` for Chebyshev polynomials of the first kind. + reupload_scaling: how the feature map scales the data that is re-uploaded. Given that + this feature map uses analog rotations, the reuploading works by simply + adding additional operations with different scaling factors in the parameter. + Choose from `ReuploadScaling` enumeration, currently only CONSTANT works, + or provide your own function with the first argument being the given + operation `op` and the second argument the feature parameter + feature_range: range of data that the input data is assumed to come from. + target_range: range of data the data encoder assumes as the natural range. For example, + in Chebyshev polynomials it is (-1, 1), while for Fourier it may be chosen as (0, 2*pi). + multiplier: overall multiplier; this is useful for reuploading the feature map serially with + different scalings; can be a number or parameter/expression. + """ + transformed_feature = fm_parameter( + fm_type, param, feature_range=feature_range, target_range=target_range + ) + multiplier = 1.0 if multiplier is None else Parameter(multiplier) + + if callable(reupload_scaling): + return reupload_scaling(op, multiplier * transformed_feature) # type: ignore[no-any-return] + elif reupload_scaling == ReuploadScaling.CONSTANT: + return op(multiplier * transformed_feature) + # TODO: implement tower scaling by reuploading multiple times + # using different analog rotations + else: + raise NotImplementedError(f"Reupload scaling {str(reupload_scaling)} not implemented!") diff --git a/tests/constructors/test_rydberg_hea.py b/tests/constructors/test_rydberg_hea.py index aa74d0b1..8dc0c903 100644 --- a/tests/constructors/test_rydberg_hea.py +++ b/tests/constructors/test_rydberg_hea.py @@ -1,9 +1,11 @@ from __future__ import annotations +import numpy as np import pytest +import torch import qadence as qd -from qadence import rydberg_hea +from qadence import analog_feature_map, rydberg_feature_map, rydberg_hea, rydberg_tower_feature_map @pytest.mark.parametrize("detunings", [True, False]) @@ -62,3 +64,49 @@ def test_rydberg_hea_differentiation() -> None: for p in model.parameters(): if p.requires_grad: assert p.grad is not None + + +@pytest.mark.parametrize("basis", [qd.BasisSet.FOURIER, qd.BasisSet.CHEBYSHEV]) +def test_analog_feature_map(basis: qd.BasisSet) -> None: + pname = "x" + mname = "mult" + fm = analog_feature_map( + param=pname, op=qd.AnalogRY, fm_type=basis, multiplier=qd.VariationalParameter(mname) + ) + assert isinstance(fm, qd.ConstantAnalogRotation) + assert fm.parameters.phase == -np.pi / 2 + assert fm.parameters.delta == 0.0 + + params = list(fm.parameters.alpha.free_symbols) + assert len(params) == 2 + assert pname in params and mname in params + + +@pytest.mark.parametrize("weights", [None, [1.0, 2.0, 3.0, 4.0]]) +def test_rydberg_feature_map(weights: list[float] | None) -> None: + n_qubits = 4 + + fm = rydberg_feature_map(n_qubits, param="x", weights=weights) + assert len(fm) == n_qubits + assert all([isinstance(b, qd.ConstantAnalogRotation) for b in fm.blocks]) + + circuit = qd.QuantumCircuit(n_qubits, fm) + observable = qd.total_magnetization(n_qubits) + model = qd.QuantumModel(circuit, observable=observable) + + values = {"x": torch.rand(1)} + expval = model.expectation(values) + expval.backward() + for p in model.parameters(): + if p.requires_grad: + assert p.grad is not None + + +def test_rydberg_tower_feature_map() -> None: + n_qubits = 4 + + fm1 = rydberg_tower_feature_map(n_qubits, param="x") + fm2 = rydberg_feature_map(n_qubits, param="x", weights=[1.0, 2.0, 3.0, 4.0]) + + for b1, b2 in zip(fm1.blocks, fm2.blocks): + assert b1.parameters.alpha == b2.parameters.alpha # type:ignore [attr-defined]