Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Allow time-dependent duration to be passed at runtime #605

Merged
merged 6 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 28 additions & 15 deletions docs/content/time_dependent.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ dependencies = [
"jsonschema",
"nevergrad",
"scipy",
"pyqtorch==1.5.1",
"pyqtorch==1.5.2",
"pyyaml",
"matplotlib",
"Arpeggio==2.0.2",
Expand Down
4 changes: 3 additions & 1 deletion qadence/backends/pyqtorch/convert_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -234,7 +236,7 @@ def convert_block(
generator=generator,
time=time_param,
cache_length=0,
duration=block.duration, # type: ignore [attr-defined]
duration=duration,
)
]

Expand Down
91 changes: 65 additions & 26 deletions qadence/operations/ham_evo.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,29 +33,53 @@

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-independent Hamiltonians the solution is exact:

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.
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)})
```
"""

Expand All @@ -67,21 +91,30 @@ 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:
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."
)
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
Expand All @@ -94,16 +127,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
Expand Down
20 changes: 10 additions & 10 deletions tests/backends/pyq/test_time_dependent_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand Down