diff --git a/docs/content/time_dependent.md b/docs/content/time_dependent.md index e5540791..30f047a3 100644 --- a/docs/content/time_dependent.md +++ b/docs/content/time_dependent.md @@ -67,7 +67,7 @@ omega_param = FeatureParameter("omega") td_generator = omega_param * (t * X(0) + t**2 * Y(1)) # Create parameterized HamEvo block -noise_operators = [torch.eye(4, dtype=torch.complex128)] +noise_operators = [X(0) + Y(0)] hamevo = HamEvo(td_generator, t, noise_operators = noise_operators) values = {"omega": torch.tensor(10.0), "duration": torch.tensor(1.0)} @@ -78,3 +78,6 @@ out_state = run(hamevo, values = values, configuration = config) print(out_state) ``` + +!!! warning "Noise operators definition" + Note it is not possible to define `noise_operators` with parametric operators, and `noise_operators` should have the same or a subset of the qubit support of the `HamEvo` instance. diff --git a/qadence/backends/pyqtorch/convert_ops.py b/qadence/backends/pyqtorch/convert_ops.py index d3f35bc0..f080d198 100644 --- a/qadence/backends/pyqtorch/convert_ops.py +++ b/qadence/backends/pyqtorch/convert_ops.py @@ -244,6 +244,17 @@ def convert_block( 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, @@ -253,7 +264,7 @@ def convert_block( duration=duration, solver=config.ode_solver, steps=config.n_steps_hevo, - noise_operators=block.noise_operators, + noise_operators=noise_operators, ) ] diff --git a/qadence/operations/ham_evo.py b/qadence/operations/ham_evo.py index 140fa2a0..8e1cd521 100644 --- a/qadence/operations/ham_evo.py +++ b/qadence/operations/ham_evo.py @@ -98,7 +98,7 @@ def __init__( parameter: TParameter, qubit_support: tuple[int, ...] = None, duration: TParameter | None = None, - noise_operators: list[Tensor] = list(), + noise_operators: list[AbstractBlock] = list(), ): params = {} if qubit_support is None and not isinstance(generator, AbstractBlock): @@ -153,14 +153,20 @@ def __init__( self.time_param = parameter self.generator = generator self.duration = duration - if len(noise_operators) > 0 and not all( - [ - len(op.size()) == 2 and op.size(0) == op.size(1) == 2 ** len(self.qubit_support) - for op in noise_operators - ] - ): - correct_shape = (2 ** len(self.qubit_support),) * 2 - raise ValueError(f"Noise operators should be square tensors of size {correct_shape}") + + if len(noise_operators) > 0: + if not all( + [ + len(set(op.qubit_support + self.qubit_support) - set(self.qubit_support)) == 0 + for op in noise_operators + ] + ): + raise ValueError( + "Noise operators should be defined" + " over the same or a subset of the qubit support" + ) + if True in [op.is_parametric for op in noise_operators]: + raise ValueError("Parametric operators are not supported") self.noise_operators = noise_operators @classmethod diff --git a/tests/backends/pyq/test_time_dependent_generator.py b/tests/backends/pyq/test_time_dependent_generator.py index 00341255..71de242b 100644 --- a/tests/backends/pyq/test_time_dependent_generator.py +++ b/tests/backends/pyq/test_time_dependent_generator.py @@ -9,7 +9,16 @@ from metrics import MIDDLE_ACCEPTANCE from pyqtorch.utils import SolverType -from qadence import AbstractBlock, HamEvo, QuantumCircuit, QuantumModel, Register, run +from qadence import ( + AbstractBlock, + HamEvo, + QuantumCircuit, + QuantumModel, + Register, + block_to_tensor, + run, +) +from qadence.operations import RZ, I, X @pytest.mark.parametrize("duration", [0.5, 1.0]) @@ -56,6 +65,7 @@ def test_time_dependent_generator( @pytest.mark.parametrize("duration", [0.5, 1.0]) +@pytest.mark.parametrize("noise_op", [I(0) * I(1), X(0)]) def test_noisy_time_dependent_generator( qadence_generator: AbstractBlock, qutip_generator: Callable, @@ -63,15 +73,14 @@ def test_noisy_time_dependent_generator( feature_param_x: float, feature_param_y: float, duration: float, + noise_op: AbstractBlock, ) -> None: n_steps = 500 ode_solver = SolverType.DP5_ME n_qubits = 2 - n_qubits_pow2 = 2**n_qubits # Define jump operators - # Note that we squeeze to remove the batch dimension - list_ops = [torch.eye(n_qubits_pow2, dtype=torch.complex128)] + list_ops = [noise_op] # simulate with qadence HamEvo using QuantumModel hamevo = HamEvo(qadence_generator, time_param, noise_operators=list_ops) @@ -94,9 +103,24 @@ def test_noisy_time_dependent_generator( # simulate with qutip t_points = np.linspace(0, duration, n_steps) - list_ops_qutip = [qutip.qeye(n_qubits_pow2)] - result = qutip.mesolve(qutip_generator, qutip.basis(n_qubits_pow2, 0), t_points, list_ops_qutip) + noise_tensor = ( + block_to_tensor(noise_op, qubit_support=tuple(range(n_qubits)), use_full_support=True) + .squeeze(0) + .numpy() + ) + list_ops_qutip = [qutip.Qobj(noise_tensor)] + result = qutip.mesolve(qutip_generator, qutip.basis(2**n_qubits, 0), t_points, list_ops_qutip) state_qutip = torch.tensor(result.states[-1].full()).unsqueeze(0) assert torch.allclose(state_qadence0, state_qutip, atol=MIDDLE_ACCEPTANCE) assert torch.allclose(state_qadence1, state_qutip, atol=MIDDLE_ACCEPTANCE) + + +@pytest.mark.parametrize("noise_op", [I(0) * I(1) * I(3), X(3), RZ(0, "theta")]) +def test_error_noise_operators_hamevo( + qadence_generator: AbstractBlock, + time_param: str, + noise_op: AbstractBlock, +) -> None: + with pytest.raises(ValueError): + hamevo = HamEvo(qadence_generator, time_param, noise_operators=[noise_op])