From 5722de30de3b14a25f1b276703985f773cd4237c Mon Sep 17 00:00:00 2001 From: seitzdom Date: Mon, 15 Jul 2024 16:43:21 +0200 Subject: [PATCH 1/5] [Feature] Time-dependence Port --- tests/test_analog.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/test_analog.py b/tests/test_analog.py index 4d702574..bfe3f605 100644 --- a/tests/test_analog.py +++ b/tests/test_analog.py @@ -348,3 +348,42 @@ def apply_hamevo_and_compare_expected(psi, values): apply_hamevo_and_compare_expected(psi, values) assert len(hamevo._cache_hamiltonian_evo) == 2 assert values_cache_key in previous_cache_keys + + +@pytest.mark.xfail # Hello vytautas +@pytest.mark.parametrize("n_qubits", [2, 4, 6]) +def test_timedependent( + n_qubits: int, +) -> None: + + tparam = "theta" + vparam = "rtheta" + parametric = True + ops = [pyq.RX, pyq.RY] * 2 + + qubit_targets = [0, 1] + generator = pyq.QuantumCircuit( + n_qubits, + [ + pyq.Add([op(q, vparam) for op, q in zip(ops, qubit_targets)]), + *[op(q, vparam) for op, q in zip(ops, qubit_targets)], + ], + ) + generator = generator.tensor( + n_qubits=n_qubits, values={vparam: torch.tensor([0.5])} + ) + generator = generator + torch.conj(torch.transpose(generator, 0, 1)) + hamevo = pyq.HamiltonianEvolution( + generator, tparam, tuple(range(n_qubits)), parametric, is_time_dependent=True + ) + # assert hamevo.generator_type == GeneratorType.TENSOR + vals = {tparam: torch.tensor([0.5])} + psi = random_state(n_qubits) + from pyqtorch.embed import Embedding + + embedding = Embedding(vparam_names=[vparam], tparam_name=tparam) + psi_star = hamevo(psi, vals, embedding) + # TODO vytautas FIX + psi_expected = _calc_mat_vec_wavefunction(hamevo, n_qubits, psi, vals) + + assert torch.allclose(psi_star, psi_expected, rtol=RTOL, atol=ATOL) From aa434b001540c8e844c1588010e375ba2b0f0d32 Mon Sep 17 00:00:00 2001 From: Vytautas Abramavicius Date: Fri, 30 Aug 2024 14:47:21 +0300 Subject: [PATCH 2/5] Fixed time-dependent generator for HamiltonianEvolution. --- pyqtorch/hamiltonians/evolution.py | 60 ++++++++++++++++++--- pyqtorch/utils.py | 18 ++++++- tests/conftest.py | 37 +++++++++++++ tests/test_analog.py | 85 +++++++++++++++--------------- tests/test_solvers.py | 1 + tests/test_tensor.py | 2 +- 6 files changed, 150 insertions(+), 53 deletions(-) diff --git a/pyqtorch/hamiltonians/evolution.py b/pyqtorch/hamiltonians/evolution.py index 3fbac026..46da7e39 100644 --- a/pyqtorch/hamiltonians/evolution.py +++ b/pyqtorch/hamiltonians/evolution.py @@ -15,15 +15,18 @@ from pyqtorch.embed import Embedding from pyqtorch.primitives import Primitive from pyqtorch.quantum_operation import QuantumOperation +from pyqtorch.time_dependent.sesolve import sesolve from pyqtorch.utils import ( ATOL, Operator, + SolverType, State, StrEnum, _round_operator, expand_operator, finitediff, is_diag, + is_parametric, ) BATCH_DIM = 2 @@ -141,8 +144,9 @@ def __init__( generator: TGenerator, time: Tensor | str, qubit_support: Tuple[int, ...] | None = None, - generator_parametric: bool = False, cache_length: int = 1, + steps: int = 100, + solver=SolverType.DP5_SE, ): """Initializes the HamiltonianEvolution. Depending on the generator argument, set the type and set the right generator getter. @@ -154,6 +158,9 @@ def __init__( generator_parametric: Whether the generator is parametric or not. """ + self.solver_type = solver + self.steps = steps + if isinstance(generator, Tensor): if qubit_support is None: raise ValueError( @@ -178,7 +185,7 @@ def __init__( "Taking support from generator and ignoring qubit_support input." ) qubit_support = generator.qubit_support - if generator_parametric: + if is_parametric(generator): generator = [generator] self.generator_type = GeneratorType.PARAMETRIC_OPERATION else: @@ -331,13 +338,50 @@ def forward( Returns: The transformed state. """ + if embedding is not None and getattr(embedding, "tparam_name", None): + n_qubits = len(state.shape) - 1 + batch_size = state.shape[-1] + t_grid = torch.linspace(0, float(self.time), self.steps) + + values.update({embedding.tparam_name: torch.tensor(0.0)}) # type: ignore [dict-item] + embedded_params = embedding(values) + + def Ht(t: torch.Tensor) -> torch.Tensor: + """Accepts a value 't' for time and returns + a (2**n_qubits, 2**n_qubits) Hamiltonian evaluated at time 't'. + """ + # We use the origial embedded params and return a new dict + # where we reembedded all parameters depending on time with value 't' + reembedded_time_values = embedding.reembed_tparam( + embedded_params, torch.as_tensor(t) + ) + return ( + self.generator[0] + .tensor(reembedded_time_values, embedding) + .squeeze(2) + ) # squeeze the batch dim and return shape (2**n_qubits, 2**n_qubits) + + sol = sesolve( + Ht, # returns a tensor of shape (2**n_qubits, 2**n_qubits) + torch.flatten( + state, start_dim=0, end_dim=-2 + ), # reshape to (2**n_qubits, batch_size) + t_grid, + self.solver_type, + ) - evolved_op = self.tensor(values, embedding) - return apply_operator( - state=state, - operator=evolved_op, - qubit_support=self.qubit_support, - ) + # Retrieve the last state of shape (2**n_qubits, batch_size) + state = sol.states[-1] + + return state.reshape( + [2] * n_qubits + [batch_size] + ) # reshape to [2] * n_qubits + [batch_size] + + else: + evolved_op = self.tensor(values, embedding) + return apply_operator( + state=state, operator=evolved_op, qubit_support=self.qubit_support + ) def tensor( self, diff --git a/pyqtorch/utils.py b/pyqtorch/utils.py index 0aa2a073..db53b9c0 100644 --- a/pyqtorch/utils.py +++ b/pyqtorch/utils.py @@ -15,11 +15,13 @@ from numpy import arange, argsort, array, delete, log2 from numpy import ndarray as NDArray from torch import Tensor, moveaxis +from typing_extensions import TypeAlias +import pyqtorch as pyq from pyqtorch.matrices import DEFAULT_MATRIX_DTYPE, DEFAULT_REAL_DTYPE, IMAT -State = Tensor -Operator = Tensor +State: TypeAlias = Tensor +Operator: TypeAlias = Tensor ATOL = 1e-06 ATOL_embedding = 1e-03 @@ -741,3 +743,15 @@ class SolverType(StrEnum): KRYLOV_SE = "krylov_se" """Uses Krylov Schrodinger equation solver""" + + +def is_parametric(operation: pyq.Sequence) -> bool: + params = [] + for m in operation.modules(): + if isinstance(m, pyq.Scale): + params.append(m.param_name) + + res = False + if any([isinstance(p, str) for p in params]): + res = True + return res diff --git a/tests/conftest.py b/tests/conftest.py index fcd0b7e3..9c54c4e3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,8 @@ from pytest import FixtureRequest from torch import Tensor +from pyqtorch.composite import Add, Scale, Sequence +from pyqtorch.embed import ConcretizedCallable from pyqtorch.noise import ( AmplitudeDamping, BitFlip, @@ -483,3 +485,38 @@ def hamiltonian_t(t: float, args: Any) -> qutip.Qobj: ) return hamiltonian_t + + +@pytest.fixture +def tparam() -> str: + return "t" + + +@pytest.fixture +def sin(tparam: str) -> tuple[str, ConcretizedCallable]: + sin_t, sin_fn = "sin_x", ConcretizedCallable("sin", [tparam]) + return sin_t, sin_fn + + +@pytest.fixture +def sq(tparam: str) -> tuple[str, ConcretizedCallable]: + t_sq, t_sq_fn = "t_sq", ConcretizedCallable("mul", [tparam, tparam]) + return t_sq, t_sq_fn + + +@pytest.fixture +def hamevo_generator( + omega: float, param_x: float, param_y: float, sin: tuple, sq: tuple +) -> Sequence: + sin_t, _ = sin + t_sq, _ = sq + generator = Scale( + Add( + [ + Scale(Scale(X(0), sin_t), "y"), + Scale(Scale(Y(1), t_sq), param_x), + ] + ), + omega, + ) + return generator diff --git a/tests/test_analog.py b/tests/test_analog.py index bfe3f605..8f8dfbdf 100644 --- a/tests/test_analog.py +++ b/tests/test_analog.py @@ -8,6 +8,7 @@ from helpers import calc_mat_vec_wavefunction, random_pauli_hamiltonian import pyqtorch as pyq +from pyqtorch.composite import Sequence from pyqtorch.hamiltonians import GeneratorType from pyqtorch.matrices import ( DEFAULT_MATRIX_DTYPE, @@ -19,6 +20,7 @@ from pyqtorch.utils import ( ATOL, RTOL, + SolverType, is_normalized, operator_kron, overlap, @@ -289,6 +291,46 @@ def test_hamevo_endianness_cnot() -> None: assert torch.allclose(wf_cnot, wf_hamevo, rtol=RTOL, atol=ATOL) +@pytest.mark.parametrize("ode_solver", [SolverType.DP5_SE, SolverType.KRYLOV_SE]) +def test_timedependent( + tparam: str, + param_y: float, + duration: float, + n_steps: int, + torch_hamiltonian: Callable, + hamevo_generator: Sequence, + sin: tuple, + sq: tuple, + ode_solver: SolverType, +) -> None: + + psi_start = random_state(2) + + # simulate with time-dependent solver + t_points = torch.linspace(0, duration, n_steps) + psi_solver = pyq.sesolve( + torch_hamiltonian, psi_start.reshape(-1, 1), t_points, ode_solver + ).states[-1] + + # simulate with HamiltonianEvolution + embedding = pyq.Embedding( + tparam_name=tparam, + var_to_call={sin[0]: sin[1], sq[0]: sq[1]}, + ) + hamiltonian_evolution = pyq.HamiltonianEvolution( + generator=hamevo_generator, + time=torch.as_tensor(duration), + steps=n_steps, + solver=ode_solver, + ) + values = {"y": param_y} + psi_hamevo = hamiltonian_evolution( + state=psi_start, values=values, embedding=embedding + ).reshape(-1, 1) + + assert torch.allclose(psi_solver, psi_hamevo, rtol=RTOL, atol=ATOL) + + @pytest.mark.parametrize("n_qubits", [2, 4, 6]) @pytest.mark.parametrize("batch_size", [1, 2]) def test_hamevo_parametric_gen(n_qubits: int, batch_size: int) -> None: @@ -302,9 +344,7 @@ def test_hamevo_parametric_gen(n_qubits: int, batch_size: int) -> None: param_list.append("t") - hamevo = pyq.HamiltonianEvolution( - generator, tparam, generator_parametric=True, cache_length=2 - ) + hamevo = pyq.HamiltonianEvolution(generator, tparam, cache_length=2) assert hamevo.generator_type == GeneratorType.PARAMETRIC_OPERATION assert len(hamevo._cache_hamiltonian_evo) == 0 @@ -348,42 +388,3 @@ def apply_hamevo_and_compare_expected(psi, values): apply_hamevo_and_compare_expected(psi, values) assert len(hamevo._cache_hamiltonian_evo) == 2 assert values_cache_key in previous_cache_keys - - -@pytest.mark.xfail # Hello vytautas -@pytest.mark.parametrize("n_qubits", [2, 4, 6]) -def test_timedependent( - n_qubits: int, -) -> None: - - tparam = "theta" - vparam = "rtheta" - parametric = True - ops = [pyq.RX, pyq.RY] * 2 - - qubit_targets = [0, 1] - generator = pyq.QuantumCircuit( - n_qubits, - [ - pyq.Add([op(q, vparam) for op, q in zip(ops, qubit_targets)]), - *[op(q, vparam) for op, q in zip(ops, qubit_targets)], - ], - ) - generator = generator.tensor( - n_qubits=n_qubits, values={vparam: torch.tensor([0.5])} - ) - generator = generator + torch.conj(torch.transpose(generator, 0, 1)) - hamevo = pyq.HamiltonianEvolution( - generator, tparam, tuple(range(n_qubits)), parametric, is_time_dependent=True - ) - # assert hamevo.generator_type == GeneratorType.TENSOR - vals = {tparam: torch.tensor([0.5])} - psi = random_state(n_qubits) - from pyqtorch.embed import Embedding - - embedding = Embedding(vparam_names=[vparam], tparam_name=tparam) - psi_star = hamevo(psi, vals, embedding) - # TODO vytautas FIX - psi_expected = _calc_mat_vec_wavefunction(hamevo, n_qubits, psi, vals) - - assert torch.allclose(psi_star, psi_expected, rtol=RTOL, atol=ATOL) diff --git a/tests/test_solvers.py b/tests/test_solvers.py index 35463bda..f6bc51ef 100644 --- a/tests/test_solvers.py +++ b/tests/test_solvers.py @@ -25,6 +25,7 @@ def test_sesolve( qutip_hamiltonian: Callable, ode_solver: SolverType, ) -> None: + psi0_qutip = qutip.basis(4, 0) # simulate with torch-based solver diff --git a/tests/test_tensor.py b/tests/test_tensor.py index fee44fbb..a8f6a856 100644 --- a/tests/test_tensor.py +++ b/tests/test_tensor.py @@ -243,7 +243,7 @@ def test_hevo_pauli_tensor( assert torch.allclose(psi_star, psi_expected, rtol=RTOL, atol=ATOL) # Test the hamiltonian evolution tparam = "t" - operator = HamiltonianEvolution(generator, tparam, generator_parametric=make_param) + operator = HamiltonianEvolution(generator, tparam) if make_param: assert operator.generator_type == GeneratorType.PARAMETRIC_OPERATION else: From cfc91271aea72f742a3ad122c0dee68417c898a5 Mon Sep 17 00:00:00 2001 From: Vytautas Abramavicius Date: Mon, 2 Sep 2024 13:34:26 +0300 Subject: [PATCH 3/5] forward() method refactoring --- pyqtorch/hamiltonians/evolution.py | 93 +++++++++++++++++------------- 1 file changed, 52 insertions(+), 41 deletions(-) diff --git a/pyqtorch/hamiltonians/evolution.py b/pyqtorch/hamiltonians/evolution.py index 46da7e39..94724b31 100644 --- a/pyqtorch/hamiltonians/evolution.py +++ b/pyqtorch/hamiltonians/evolution.py @@ -322,6 +322,55 @@ def spectral_gap(self) -> Tensor: spectral_gap = torch.unique(torch.abs(torch.tril(diffs))) return spectral_gap[spectral_gap.nonzero()] + def _forward( + self, + state: Tensor, + values: dict[str, Tensor] | ParameterDict = dict(), + embedding: Embedding | None = None, + ) -> State: + evolved_op = self.tensor(values, embedding) + return apply_operator( + state=state, operator=evolved_op, qubit_support=self.qubit_support + ) + + def _forward_time( + self, + state: Tensor, + values: dict[str, Tensor] | ParameterDict = dict(), + embedding: Embedding = Embedding(), + ) -> State: + n_qubits = len(state.shape) - 1 + batch_size = state.shape[-1] + t_grid = torch.linspace(0, float(self.time), self.steps) + + values.update({embedding.tparam_name: torch.tensor(0.0)}) # type: ignore [dict-item] + embedded_params = embedding(values) + + def Ht(t: torch.Tensor) -> torch.Tensor: + """Accepts a value 't' for time and returns + a (2**n_qubits, 2**n_qubits) Hamiltonian evaluated at time 't'. + """ + # We use the origial embedded params and return a new dict + # where we reembedded all parameters depending on time with value 't' + reembedded_time_values = embedding.reembed_tparam( + embedded_params, torch.as_tensor(t) + ) + return ( + self.generator[0].tensor(reembedded_time_values, embedding).squeeze(2) + ) + + sol = sesolve( + Ht, + torch.flatten(state, start_dim=0, end_dim=-2), + t_grid, + self.solver_type, + ) + + # Retrieve the last state of shape (2**n_qubits, batch_size) + state = sol.states[-1] + + return state.reshape([2] * n_qubits + [batch_size]) + def forward( self, state: Tensor, @@ -334,54 +383,16 @@ def forward( Arguments: state: Input state. values: Values of parameters. + embedding: Embedding of parameters. Returns: The transformed state. """ if embedding is not None and getattr(embedding, "tparam_name", None): - n_qubits = len(state.shape) - 1 - batch_size = state.shape[-1] - t_grid = torch.linspace(0, float(self.time), self.steps) - - values.update({embedding.tparam_name: torch.tensor(0.0)}) # type: ignore [dict-item] - embedded_params = embedding(values) - - def Ht(t: torch.Tensor) -> torch.Tensor: - """Accepts a value 't' for time and returns - a (2**n_qubits, 2**n_qubits) Hamiltonian evaluated at time 't'. - """ - # We use the origial embedded params and return a new dict - # where we reembedded all parameters depending on time with value 't' - reembedded_time_values = embedding.reembed_tparam( - embedded_params, torch.as_tensor(t) - ) - return ( - self.generator[0] - .tensor(reembedded_time_values, embedding) - .squeeze(2) - ) # squeeze the batch dim and return shape (2**n_qubits, 2**n_qubits) - - sol = sesolve( - Ht, # returns a tensor of shape (2**n_qubits, 2**n_qubits) - torch.flatten( - state, start_dim=0, end_dim=-2 - ), # reshape to (2**n_qubits, batch_size) - t_grid, - self.solver_type, - ) - - # Retrieve the last state of shape (2**n_qubits, batch_size) - state = sol.states[-1] - - return state.reshape( - [2] * n_qubits + [batch_size] - ) # reshape to [2] * n_qubits + [batch_size] + return self._forward_time(state, values, embedding) else: - evolved_op = self.tensor(values, embedding) - return apply_operator( - state=state, operator=evolved_op, qubit_support=self.qubit_support - ) + return self._forward(state, values, embedding) def tensor( self, From a4f1d4a024144fe97f50ea9a7777d871a74b3855 Mon Sep 17 00:00:00 2001 From: Vytautas Abramavicius Date: Mon, 2 Sep 2024 13:59:28 +0300 Subject: [PATCH 4/5] generalized is_parametric() function code --- pyqtorch/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyqtorch/utils.py b/pyqtorch/utils.py index db53b9c0..bb6bb3a6 100644 --- a/pyqtorch/utils.py +++ b/pyqtorch/utils.py @@ -746,9 +746,11 @@ class SolverType(StrEnum): def is_parametric(operation: pyq.Sequence) -> bool: + from pyqtorch.primitives import Parametric + params = [] for m in operation.modules(): - if isinstance(m, pyq.Scale): + if isinstance(m, (pyq.Scale, Parametric)): params.append(m.param_name) res = False From 1890d2765bfa4ce7d0b1d26ff3f048c7be334943 Mon Sep 17 00:00:00 2001 From: Vytautas Abramavicius <145791635+vytautas-a@users.noreply.github.com> Date: Mon, 2 Sep 2024 14:13:51 +0300 Subject: [PATCH 5/5] Update pyqtorch/utils.py Co-authored-by: Roland-djee <9250798+Roland-djee@users.noreply.github.com> --- pyqtorch/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtorch/utils.py b/pyqtorch/utils.py index bb6bb3a6..4d3e26be 100644 --- a/pyqtorch/utils.py +++ b/pyqtorch/utils.py @@ -754,6 +754,6 @@ def is_parametric(operation: pyq.Sequence) -> bool: params.append(m.param_name) res = False - if any([isinstance(p, str) for p in params]): + if any(isinstance(p, str) for p in params): res = True return res