From 374ae7903c487a28007cea52cbb2bbf7d8169dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20P=2E=20Moutinho?= <56390829+jpmoutinho@users.noreply.github.com> Date: Fri, 8 Nov 2024 14:34:41 +0100 Subject: [PATCH 1/5] preliminary duration changes --- qadence/backends/pyqtorch/convert_ops.py | 4 ++- qadence/operations/ham_evo.py | 46 ++++++++++++++++-------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/qadence/backends/pyqtorch/convert_ops.py b/qadence/backends/pyqtorch/convert_ops.py index 3e228fba..9145eee2 100644 --- a/qadence/backends/pyqtorch/convert_ops.py +++ b/qadence/backends/pyqtorch/convert_ops.py @@ -208,8 +208,10 @@ def convert_block( return [pyq.Scale(pyq.Sequence(scaled_ops), param)] elif isinstance(block, TimeEvolutionBlock): + duration = block.duration # type: ignore [attr-defined] if getattr(block.generator, "is_time_dependent", False): config._use_gate_params = False + duration = config.get_param_name(block)[1] generator = convert_block(block.generator, config=config)[0] # type: ignore [arg-type] elif isinstance(block.generator, sympy.Basic): generator = config.get_param_name(block)[1] @@ -234,7 +236,7 @@ def convert_block( generator=generator, time=time_param, cache_length=0, - duration=block.duration, # type: ignore [attr-defined] + duration=duration, ) ] diff --git a/qadence/operations/ham_evo.py b/qadence/operations/ham_evo.py index b22515cc..6c2a6f32 100644 --- a/qadence/operations/ham_evo.py +++ b/qadence/operations/ham_evo.py @@ -33,11 +33,15 @@ class HamEvo(TimeEvolutionBlock): """ - A block implementing the Hamiltonian evolution operation H where: + The Hamiltonian evolution operator U(t). - H = exp(-iG, t) - where G represents a square generator and t represents the time parameter - which can be parametrized. + For time-dependent Hamiltonians + + U(t) = exp(-iGt) + + where G represents an Hermitian generator, or Hamiltonian and t represents the + time parameter. For time-dependent Hamiltonians, the solution is obtained by + numerical integration of the Schrodinger equation. Arguments: generator: Either a AbstractBlock, torch.Tensor or numpy.ndarray. @@ -67,21 +71,27 @@ def __init__( generator: Union[TGenerator, AbstractBlock], parameter: TParameter, qubit_support: tuple[int, ...] = None, - duration: float | None = None, + duration: TParameter | None = None, ): - gen_exprs = {} + params = {} if qubit_support is None and not isinstance(generator, AbstractBlock): raise ValueError("You have to supply a qubit support for non-block generators.") super().__init__(qubit_support if qubit_support else generator.qubit_support) if isinstance(generator, AbstractBlock): qubit_support = generator.qubit_support if generator.is_parametric: - gen_exprs = {str(e): e for e in expressions(generator)} - - if generator.is_time_dependent and duration is None: - raise ValueError("For time-dependent generators, a duration must be specified.") - + params = {str(e): e for e in expressions(generator)} + if generator.is_time_dependent and duration is None: + duration = Parameter("duration", trainable=False) + if not generator.is_time_dependent and duration is not None: + raise TypeError( + "Duration argument is only supported for time-dependent generators." + ) elif isinstance(generator, torch.Tensor): + if duration is not None: + raise TypeError( + "Duration argument is only supported for time-dependent generators." + ) msg = "Please provide a square generator." if len(generator.shape) == 2: assert generator.shape[0] == generator.shape[1], msg @@ -94,16 +104,22 @@ def __init__( In case of a 3D generator, the batch dim\ is expected to be at dim 0." ) - gen_exprs = {str(generator.__hash__()): generator} + params = {str(generator.__hash__()): generator} elif isinstance(generator, (sympy.Basic, sympy.Array)): - gen_exprs = {str(generator): generator} + if duration is not None: + raise TypeError( + "Duration argument is only supported for time-dependent generators." + ) + params = {str(generator): generator} else: raise TypeError( f"Generator of type {type(generator)} not supported.\ If you're using a numpy.ndarray, please cast it to a torch tensor." ) - ps = {"parameter": Parameter(parameter), **gen_exprs} - self.parameters = ParamMap(**ps) + if duration is not None: + params = {"duration": Parameter(duration), **params} + params = {"parameter": Parameter(parameter), **params} + self.parameters = ParamMap(**params) self.time_param = parameter self.generator = generator self.duration = duration From 6383dc39fc7fafb8d35f76bf79498721504a8655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20P=2E=20Moutinho?= <56390829+jpmoutinho@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:21:51 +0100 Subject: [PATCH 2/5] update docs --- docs/content/time_dependent.md | 43 ++++++++++++++++++++++------------ pyproject.toml | 2 +- qadence/operations/ham_evo.py | 42 ++++++++++++++++++++++++--------- 3 files changed, 60 insertions(+), 27 deletions(-) diff --git a/docs/content/time_dependent.md b/docs/content/time_dependent.md index 29c14849..9ea6cb33 100644 --- a/docs/content/time_dependent.md +++ b/docs/content/time_dependent.md @@ -1,32 +1,45 @@ For use cases when the Hamiltonian of the system is time-dependent, Qadence provides a special parameter `TimePrameter("t")` that denotes the explicit time dependence. Using this time parameter one can define a parameterized block acting as the generator passed to `HamEvo` that encapsulates the required time dependence function. -```python exec="on" source="material-block" session="getting_started" result="json" -from qadence import X, Y, HamEvo, TimeParameter, Parameter, run +```python exec="on" source="material-block" session="getting_started" +from qadence import X, Y, HamEvo, TimeParameter, FeatureParameter, run from pyqtorch.utils import SolverType import torch -# simulation parameters -duration = 1.0 # duration of time-dependent block simulation +# Simulation parameters ode_solver = SolverType.DP5_SE # time-dependent Schrodinger equation solver method n_steps_hevo = 500 # integration time steps used by solver -# define block parameters +# Define block parameters t = TimeParameter("t") -omega_param = Parameter("omega") +omega_param = FeatureParameter("omega") -# create time-dependent generator +# Arbitrarily compose a time-dependent generator generator_td = omega_param * (t * X(0) + t**2 * Y(1)) -# create parameterized HamEvo block -hamevo = HamEvo(generator_td, t, duration=duration) +# Create parameterized HamEvo block +hamevo = HamEvo(generator_td, t) +``` + +Note that when using `HamEvo` with a time-dependent generator, the actual time parameter that was used to construct the generator must be passed for the second argument `parameter`. + +By default, the code above will initialize an internal parameter `FeatureParameter("duration")` in the `HamEvo`. Alternatively, +the `duration` argument can be used to rename this parameter, or to pass a fixed value directly. If no fixed value is passed, +it must then be set in the `values` dictionary at runtime. + +!!! note "Future improvements" + Currently it is only possible to pass a single value for the duration, and the only result obtained will be the one + corresponding to the state at end of the integration. In the future we will change the interface to allow directly passing + some array of save values to obtain expectation values or statevectors at intermediate steps during the evolution. + +```python exec="on" source="material-block" session="getting_started" result="json" + +values = {"omega": torch.tensor(10.0), "duration": torch.tensor(1.0)} + +config = {"ode_solver": ode_solver, "n_steps_hevo": n_steps_hevo} -# run simulation -out_state = run(hamevo, - values={"omega": torch.tensor(10.0)}, - configuration={"ode_solver": ode_solver, - "n_steps_hevo": n_steps_hevo}) +out_state = run(hamevo, values = values, configuration = config) print(out_state) ``` -Note that when using `HamEvo` with a time-dependent generator, the actual time parameter that was used to construct the generator must be passed for the second argument `parameter`. In time-dependent case a value for `duration` argument to `HamEvo` must be passed in order to define the duration of the simulation. The unit of passed duration value $\tau$ must be aligned with the units of other parameters in the time-dependent generator so that the integral of generator $\overset{\tau}{\underset{0}{\int}}\mathcal{\hat{H}}(t){\rm d}t$ is dimensionless. +Note that Qadence makes no assumption on units. The unit of passed duration value $\tau$ must be aligned with the units of other parameters in the time-dependent generator so that the integral of generator $\overset{\tau}{\underset{0}{\int}}\mathcal{\hat{H}}(t){\rm d}t$ is dimensionless. diff --git a/pyproject.toml b/pyproject.toml index 126f1b97..74c612c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dependencies = [ "jsonschema", "nevergrad", "scipy", - "pyqtorch==1.5.1", + "pyqtorch==1.5.2", "pyyaml", "matplotlib", "Arpeggio==2.0.2", diff --git a/qadence/operations/ham_evo.py b/qadence/operations/ham_evo.py index 6c2a6f32..070e5ee4 100644 --- a/qadence/operations/ham_evo.py +++ b/qadence/operations/ham_evo.py @@ -44,22 +44,42 @@ class HamEvo(TimeEvolutionBlock): numerical integration of the Schrodinger equation. Arguments: - generator: Either a AbstractBlock, torch.Tensor or numpy.ndarray. - parameter: A scalar or vector of numeric or torch.Tensor type. - qubit_support: The qubits on which the evolution will be performed on. - duration: duration of evolution in case of time-dependent generator + generator: Hamiltonian generator, either symbolic as an AbstractBlock, + or as a torch.Tensor or numpy.ndarray. + parameter: The time parameter for evolution operator. For the time-independent + case, it represents the actual value for which the evolution will be + evaluated. For the time-dependent case, it should be an instance of + TimeParameter to signal the solver the variable that will be integrated over. + qubit_support: The qubits on which the evolution will be performed on. Only + required for generators that are not a composition of blocks. + duration: (optional) duration of the evolution in case of time-dependent + generator. By default, a FeatureParameter with tag "duration" will + be initialized, and the value will then be required in the values dict. Examples: ```python exec="on" source="material-block" result="json" - from qadence import RX, HamEvo, run, PI + from qadence import X, HamEvo, PI, add, run + from qadence import FeatureParameter, TimeParameter import torch - hevo = HamEvo(generator=RX(0, PI), parameter=torch.rand(2)) - print(run(hevo)) - # Now lets use a torch.Tensor as a generator, Now we have to pass the support - gen = torch.rand(2,2, dtype=torch.complex128) - hevo = HamEvo(generator=gen, parameter=torch.rand(2), qubit_support=(0,)) - print(run(hevo)) + + n_qubits = 3 + + # Hamiltonian as a block composition + hamiltonian = add(X(i) for i in range(n_qubits)) + hevo = HamEvo(hamiltonian, parameter=torch.rand(2)) + state = run(hevo) + + # Hamiltonian as a random matrix + hamiltonian = torch.rand(2, 2, dtype=torch.complex128) + hevo = HamEvo(hamiltonian, parameter=torch.rand(2), qubit_support=(0,)) + state = run(hevo) + + # Time-dependent Hamiltonian + t = TimeParameter("t") + hamiltonian = t * add(X(i) for i in range(n_qubits)) + hevo = HamEvo(hamiltonian, parameter=t) + state = run(hevo, values = {"duration": torch.tensor(1.0)}) ``` """ From 3af2c6239f05e4290bb7d4bbd33eb648eb8eb2cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20P=2E=20Moutinho?= <56390829+jpmoutinho@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:36:02 +0100 Subject: [PATCH 3/5] change test --- .../pyq/test_time_dependent_generator.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/backends/pyq/test_time_dependent_generator.py b/tests/backends/pyq/test_time_dependent_generator.py index 4b2da570..7564ceef 100644 --- a/tests/backends/pyq/test_time_dependent_generator.py +++ b/tests/backends/pyq/test_time_dependent_generator.py @@ -12,7 +12,7 @@ from qadence import AbstractBlock, HamEvo, QuantumCircuit, QuantumModel, Register, run -@pytest.mark.parametrize("duration", [0.5, 1.0, 2.0, 5.0]) +@pytest.mark.parametrize("duration", [0.5, 1.0, 2.0]) @pytest.mark.parametrize("ode_solver", [SolverType.DP5_SE, SolverType.KRYLOV_SE]) def test_time_dependent_generator( qadence_generator: AbstractBlock, @@ -26,17 +26,17 @@ def test_time_dependent_generator( n_steps = 500 # simulate with qadence HamEvo using QuantumModel - hamevo = HamEvo(qadence_generator, time_param, duration=duration) + hamevo = HamEvo(qadence_generator, time_param) reg = Register(2) circ = QuantumCircuit(reg, hamevo) - model = QuantumModel(circ, configuration={"ode_solver": ode_solver, "n_steps_hevo": n_steps}) - state_qadence0 = model.run(values={"x": torch.tensor(feature_param_x)}) - - state_qadence1 = run( - hamevo, - values={"x": torch.tensor(feature_param_x)}, - configuration={"ode_solver": ode_solver, "n_steps_hevo": n_steps}, - ) + config = {"ode_solver": ode_solver, "n_steps_hevo": n_steps} + values = values = {"x": torch.tensor(feature_param_x), "duration": torch.tensor(duration)} + + model = QuantumModel(circ, configuration=config) + state_qadence0 = model.run(values=values) + + # simulate with qadence.execution + state_qadence1 = run(hamevo, values=values, configuration=config) # simulate with qutip t_points = np.linspace(0, duration, n_steps) From 040f826c958be13b394b7a156f171c9b1e229d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20P=2E=20Moutinho?= <56390829+jpmoutinho@users.noreply.github.com> Date: Mon, 11 Nov 2024 10:25:29 +0100 Subject: [PATCH 4/5] fix docstring --- qadence/operations/ham_evo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qadence/operations/ham_evo.py b/qadence/operations/ham_evo.py index 070e5ee4..947abc50 100644 --- a/qadence/operations/ham_evo.py +++ b/qadence/operations/ham_evo.py @@ -35,7 +35,7 @@ class HamEvo(TimeEvolutionBlock): """ The Hamiltonian evolution operator U(t). - For time-dependent Hamiltonians + For time-independent Hamiltonians the solution is exact: U(t) = exp(-iGt) From ac5fea1685792eb95d8d47e4f9dfb72e2d1b75fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20P=2E=20Moutinho?= <56390829+jpmoutinho@users.noreply.github.com> Date: Mon, 11 Nov 2024 12:48:03 +0100 Subject: [PATCH 5/5] fix arg --- qadence/operations/ham_evo.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/qadence/operations/ham_evo.py b/qadence/operations/ham_evo.py index 947abc50..7af7dba1 100644 --- a/qadence/operations/ham_evo.py +++ b/qadence/operations/ham_evo.py @@ -101,8 +101,11 @@ def __init__( qubit_support = generator.qubit_support if generator.is_parametric: params = {str(e): e for e in expressions(generator)} - if generator.is_time_dependent and duration is None: - duration = Parameter("duration", trainable=False) + if generator.is_time_dependent: + if isinstance(duration, str): + duration = Parameter(duration, trainable=False) + elif duration is None: + duration = Parameter("duration", trainable=False) if not generator.is_time_dependent and duration is not None: raise TypeError( "Duration argument is only supported for time-dependent generators."