Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into cm/qmodel_doc
Browse files Browse the repository at this point in the history
  • Loading branch information
Charles MOUSSA committed Jul 3, 2024
2 parents 7a8acc8 + 6626726 commit 1932627
Show file tree
Hide file tree
Showing 21 changed files with 290 additions and 30 deletions.
32 changes: 32 additions & 0 deletions docs/content/time_dependent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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
from pyqtorch.utils import SolverType
import torch

# simulation parameters
duration = 1.0 # duration of time-dependent block simulation
ode_solver = SolverType.DP5_SE # time-dependent Schrodinger equation solver method
n_steps_hevo = 500 # integration time steps used by solver

# define block parameters
t = TimeParameter("t")
omega_param = Parameter("omega")

# create time-dependent generator
generator_td = omega_param * (t * X(0) + t**2 * Y(1))

# create parameterized HamEvo block
hamevo = HamEvo(generator_td, 0.0, duration=duration)

# run simulation
out_state = run(hamevo,
values={"omega": torch.tensor(10.0)},
configuration={"ode_solver": ode_solver,
"n_steps_hevo": n_steps_hevo})

print(out_state)
```

Note that when using `HamEvo` with a time-dependent generator, its second argument `parameter` is not used and an arbitrary value can be passed to it. However, in case of time-dependent generator 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.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ nav:
- Contents:
- Block system: content/block_system.md
- Parametric programs: content/parameters.md
- Time-dependent generators: content/time_dependent.md
- Quantum models: content/quantummodels.md
- Quantum registers: content/register.md
- State initialization: content/state_init.md
Expand Down
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ authors = [
{ name = "Gergana Velikova", email = "[email protected]" },
{ name = "Eduardo Maschio", email = "[email protected]" },
{ name = "Smit Chaudhary", email = "[email protected]" },
{ name = "Charles Moussa", email = "[email protected]" },
]
requires-python = ">=3.9"
license = { text = "Apache 2.0" }
Expand All @@ -45,7 +46,7 @@ dependencies = [
"jsonschema",
"nevergrad",
"scipy",
"pyqtorch==1.2.4",
"pyqtorch==1.2.5",
"pyyaml",
"matplotlib",
"Arpeggio==2.0.2",
Expand All @@ -57,8 +58,8 @@ allow-ambiguous-features = true

[project.optional-dependencies]
pulser = [
"pulser-core==0.18.1",
"pulser-simulation==0.18.1",
"pulser-core==0.19.0",
"pulser-simulation==0.19.0",
"pasqal-cloud==0.11.0",
]
braket = ["amazon-braket-sdk<1.71.2"]
Expand Down
1 change: 1 addition & 0 deletions qadence/backends/gpsr.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ def multi_gap_psr(

# get number of observables from expectation value tensor
if f_plus.numel() > 1:
batch_size = F[0].shape[0]
n_obs = F[0].shape[1]

# reshape F vector
Expand Down
1 change: 0 additions & 1 deletion qadence/backends/pyqtorch/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,6 @@ def _looped_expectation(
"Looping expectation does not make sense with batched initial state. "
"Define your initial state with `batch_size=1`"
)

list_expvals = []
observables = observable if isinstance(observable, list) else [observable]
for vals in to_list_of_dicts(param_values):
Expand Down
5 changes: 5 additions & 0 deletions qadence/backends/pyqtorch/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from logging import getLogger
from typing import Callable

from pyqtorch.utils import SolverType

from qadence.analog import add_background_hamiltonian
from qadence.backend import BackendConfiguration
from qadence.transpile import (
Expand Down Expand Up @@ -41,6 +43,9 @@ class Configuration(BackendConfiguration):
algo_hevo: AlgoHEvo = AlgoHEvo.EXP
"""Determine which kind of Hamiltonian evolution algorithm to use."""

ode_solver: SolverType = SolverType.DP5_SE
"""Determine which ODE solver to use for time-dependent blocks."""

n_steps_hevo: int = 100
"""Default number of steps for the Hamiltonian evolution."""

Expand Down
87 changes: 79 additions & 8 deletions qadence/backends/pyqtorch/convert_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

import pyqtorch as pyq
import sympy
import torch
from pyqtorch.apply import apply_operator
from pyqtorch.matrices import _dagger
from pyqtorch.time_dependent.sesolve import sesolve
from pyqtorch.utils import is_diag
from torch import (
Tensor,
Expand All @@ -26,6 +28,8 @@

from qadence.backends.utils import (
finitediff,
pyqify,
unpyqify,
)
from qadence.blocks import (
AbstractBlock,
Expand All @@ -38,8 +42,12 @@
ScaleBlock,
TimeEvolutionBlock,
)
from qadence.blocks.block_to_tensor import _block_to_tensor_embedded, block_to_tensor
from qadence.blocks.block_to_tensor import (
_block_to_tensor_embedded,
block_to_tensor,
)
from qadence.blocks.primitive import ProjectorBlock
from qadence.blocks.utils import parameters
from qadence.operations import (
U,
multi_qubit_gateset,
Expand Down Expand Up @@ -177,6 +185,7 @@ def __init__(
self.param_names = config.get_param_name(block)
self.block = block
self.hmat: Tensor
self.config = config

if isinstance(block.generator, AbstractBlock) and not block.generator.is_parametric:
hmat = block_to_tensor(
Expand Down Expand Up @@ -289,18 +298,80 @@ def dagger(self, values: dict[str, Tensor]) -> Tensor:
"""Dagger of the evolved operator given the current parameter values."""
return _dagger(self.unitary(values))

def _get_time_parameter(self) -> str:
# get unique time parameters
unique_time_params = set()
for p in parameters(self.block.generator): # type: ignore [arg-type]
if getattr(p, "is_time", False):
unique_time_params.add(str(p))

if len(unique_time_params) > 1:
raise Exception("Only a single time parameter is supported.")

return unique_time_params.pop()

def forward(
self,
state: Tensor,
values: dict[str, Tensor],
) -> Tensor:
return apply_operator(
state,
self.unitary(values),
self.qubit_support,
self.n_qubits,
self.batch_size,
)
if getattr(self.block.generator, "is_time_dependent", False): # type: ignore [union-attr]

def Ht(t: Tensor | float) -> Tensor:
# values dict has to change with new value of t
# initial value of a feature parameter inside generator block
# has to be inferred here
new_vals = dict()
for str_expr, val in values.items():
expr = sympy.sympify(str_expr)
t_symb = sympy.Symbol(self._get_time_parameter())
free_symbols = expr.free_symbols
if t_symb in free_symbols:
# create substitution list for time and feature params
subs_list = [(t_symb, t)]

if len(free_symbols) > 1:
# get feature param symbols
feat_symbols = free_symbols.difference(set([t_symb]))

# get feature param values
feat_vals = values["orig_param_values"]

# update substitution list with feature param values
for fs in feat_symbols:
subs_list.append((fs, feat_vals[str(fs)]))

# evaluate expression with new time param value
new_vals[str_expr] = torch.tensor(float(expr.subs(subs_list)))
else:
# expression doesn't contain time parameter - copy it as is
new_vals[str_expr] = val

# get matrix form of generator
hmat = _block_to_tensor_embedded(
self.block.generator, # type: ignore[arg-type]
values=new_vals,
qubit_support=self.qubit_support,
use_full_support=False,
device=self.device,
).squeeze(0)

return hmat

tsave = torch.linspace(0, self.block.duration, self.config.n_steps_hevo) # type: ignore [attr-defined]
result = pyqify(
sesolve(Ht, unpyqify(state).T[:, 0:1], tsave, self.config.ode_solver).states[-1].T
)
else:
result = apply_operator(
state,
self.unitary(values),
self.qubit_support,
self.n_qubits,
self.batch_size,
)

return result

@property
def device(self) -> torch_device:
Expand Down
3 changes: 2 additions & 1 deletion qadence/backends/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,11 @@ def to_list_of_dicts(param_values: ParamDictType) -> list[ParamDictType]:
if not param_values:
return [param_values]

max_batch_size = max(p.size()[0] for p in param_values.values())
max_batch_size = max(p.size()[0] for p in param_values.values() if isinstance(p, Tensor))
batched_values = {
k: (v if v.size()[0] == max_batch_size else v.repeat(max_batch_size, 1))
for k, v in param_values.items()
if isinstance(v, Tensor)
}

return [{k: v[i] for k, v in batched_values.items()} for i in range(max_batch_size)]
Expand Down
7 changes: 7 additions & 0 deletions qadence/blocks/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,13 @@ def is_parametric(self) -> bool:
params: list[sympy.Basic] = parameters(self)
return any(isinstance(p, Parameter) for p in params)

@property
def is_time_dependent(self) -> bool:
from qadence.blocks.utils import parameters

params: list[sympy.Basic] = parameters(self)
return any(getattr(p, "is_time", False) for p in params)

def tensor(self, values: dict[str, TNumber | torch.Tensor] = {}) -> torch.Tensor:
from .block_to_tensor import block_to_tensor

Expand Down
29 changes: 17 additions & 12 deletions qadence/blocks/embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,18 +111,21 @@ def embedding_fn(params: ParamDictType, inputs: ParamDictType) -> ParamDictType:
angle: ArrayLike
values = {}
for symbol in expr.free_symbols:
if symbol.name in inputs:
value = inputs[symbol.name]
elif symbol.name in params:
value = params[symbol.name]
if not symbol.is_time:
if symbol.name in inputs:
value = inputs[symbol.name]
elif symbol.name in params:
value = params[symbol.name]
else:
msg_trainable = "Trainable" if symbol.trainable else "Non-trainable"
raise KeyError(
f"{msg_trainable} parameter '{symbol.name}' not found in the "
f"inputs list: {list(inputs.keys())} nor the "
f"params list: {list(params.keys())}."
)
values[symbol.name] = value
else:
msg_trainable = "Trainable" if symbol.trainable else "Non-trainable"
raise KeyError(
f"{msg_trainable} parameter '{symbol.name}' not found in the "
f"inputs list: {list(inputs.keys())} nor the "
f"params list: {list(params.keys())}."
)
values[symbol.name] = value
values[symbol.name] = tensor(1.0)
angle = fn(**values)
# do not reshape parameters which are multi-dimensional
# tensors, such as for example generator matrices
Expand All @@ -139,7 +142,9 @@ def embedding_fn(params: ParamDictType, inputs: ParamDictType) -> ParamDictType:
gate_lvl_params[uuid] = embedded_params[e]
return gate_lvl_params
else:
return {stringify(k): v for k, v in embedded_params.items()}
out = {stringify(k): v for k, v in embedded_params.items()}
out.update({"orig_param_values": inputs})
return out

params: ParamDictType
params = {
Expand Down
14 changes: 11 additions & 3 deletions qadence/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ def _(
endianness: Endianness = Endianness.BIG,
configuration: Union[BackendConfiguration, dict, None] = None,
) -> Tensor:
bknd = backend_factory(backend, configuration=configuration)
diff_mode = None
if backend == BackendName.PYQTORCH:
diff_mode = DiffMode.AD
bknd = backend_factory(backend, diff_mode=diff_mode, configuration=configuration)
conv = bknd.convert(circuit)
with no_grad():
return bknd.run(
Expand Down Expand Up @@ -147,7 +150,10 @@ def _(
endianness: Endianness = Endianness.BIG,
configuration: Union[BackendConfiguration, dict, None] = None,
) -> list[Counter]:
bknd = backend_factory(backend, configuration=configuration)
diff_mode = None
if backend == BackendName.PYQTORCH:
diff_mode = DiffMode.AD
bknd = backend_factory(backend, diff_mode=diff_mode, configuration=configuration)
conv = bknd.convert(circuit)
return bknd.sample(
circuit=conv.circuit,
Expand Down Expand Up @@ -242,7 +248,9 @@ def _(
configuration: Union[BackendConfiguration, dict, None] = None,
) -> Tensor:
observable = observable if isinstance(observable, list) else [observable]
bknd = backend_factory(backend, configuration=configuration, diff_mode=diff_mode)
if backend == BackendName.PYQTORCH:
diff_mode = DiffMode.AD
bknd = backend_factory(backend, diff_mode=diff_mode, configuration=configuration)
conv = bknd.convert(circuit, observable)

def _expectation() -> Tensor:
Expand Down
2 changes: 2 additions & 0 deletions qadence/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,9 @@ def run(
"""
if values is None:
values = {}

params = self.embedding_fn(self._params, values)

return self.backend.run(self._circuit, params, state=state, endianness=endianness)

def sample(
Expand Down
10 changes: 10 additions & 0 deletions qadence/operations/ham_evo.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class HamEvo(TimeEvolutionBlock):
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
Examples:
Expand All @@ -66,6 +67,7 @@ def __init__(
generator: Union[TGenerator, AbstractBlock],
parameter: TParameter,
qubit_support: tuple[int, ...] = None,
duration: float | None = None,
):
gen_exprs = {}
if qubit_support is None and not isinstance(generator, AbstractBlock):
Expand All @@ -75,6 +77,10 @@ def __init__(
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.")

elif isinstance(generator, torch.Tensor):
msg = "Please provide a square generator."
if len(generator.shape) == 2:
Expand All @@ -99,6 +105,7 @@ def __init__(
ps = {"parameter": Parameter(parameter), **gen_exprs}
self.parameters = ParamMap(**ps)
self.generator = generator
self.duration = duration

@classmethod
def num_parameters(cls) -> int:
Expand Down Expand Up @@ -197,3 +204,6 @@ def digital_decomposition(self, approximation: LTSOrder = LTSOrder.ST4) -> Abstr
raise NotImplementedError(
"The current digital decomposition can be applied only to Pauli Hamiltonians."
)

def __matmul__(self, other: AbstractBlock) -> AbstractBlock:
return super().__matmul__(other)
Loading

0 comments on commit 1932627

Please sign in to comment.