qadence 1.10.2__py3-none-any.whl → 1.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qadence/backends/horqrux/convert_ops.py +1 -1
- qadence/blocks/block_to_tensor.py +29 -32
- qadence/blocks/matrix.py +4 -0
- qadence/constructors/__init__.py +7 -1
- qadence/constructors/hamiltonians.py +96 -9
- qadence/mitigations/analog_zne.py +6 -2
- qadence/ml_tools/__init__.py +3 -2
- qadence/ml_tools/callbacks/callback.py +80 -50
- qadence/ml_tools/callbacks/callbackmanager.py +3 -2
- qadence/ml_tools/callbacks/writer_registry.py +3 -2
- qadence/ml_tools/config.py +66 -5
- qadence/ml_tools/constructors.py +9 -62
- qadence/ml_tools/data.py +4 -0
- qadence/ml_tools/information/__init__.py +3 -0
- qadence/ml_tools/information/information_content.py +339 -0
- qadence/ml_tools/models.py +69 -4
- qadence/ml_tools/optimize_step.py +1 -2
- qadence/ml_tools/train_utils/__init__.py +3 -1
- qadence/ml_tools/train_utils/accelerator.py +480 -0
- qadence/ml_tools/train_utils/config_manager.py +7 -7
- qadence/ml_tools/train_utils/distribution.py +209 -0
- qadence/ml_tools/train_utils/execution.py +421 -0
- qadence/ml_tools/trainer.py +291 -98
- qadence/operations/primitive.py +0 -4
- qadence/types.py +7 -11
- qadence/utils.py +45 -0
- {qadence-1.10.2.dist-info → qadence-1.11.0.dist-info}/METADATA +16 -13
- {qadence-1.10.2.dist-info → qadence-1.11.0.dist-info}/RECORD +30 -25
- {qadence-1.10.2.dist-info → qadence-1.11.0.dist-info}/WHEEL +0 -0
- {qadence-1.10.2.dist-info → qadence-1.11.0.dist-info}/licenses/LICENSE +0 -0
qadence/ml_tools/config.py
CHANGED
@@ -20,6 +20,7 @@ from qadence.types import (
|
|
20
20
|
ReuploadScaling,
|
21
21
|
Strategy,
|
22
22
|
)
|
23
|
+
from torch import dtype
|
23
24
|
|
24
25
|
logger = getLogger(__file__)
|
25
26
|
|
@@ -116,10 +117,9 @@ class TrainConfig:
|
|
116
117
|
"""The log folder for saving checkpoints and tensorboard logs.
|
117
118
|
|
118
119
|
This stores the path where all logs and checkpoints are being saved
|
119
|
-
for this training session. `log_folder` takes precedence over `root_folder
|
120
|
-
|
121
|
-
|
122
|
-
will not be used.
|
120
|
+
for this training session. `log_folder` takes precedence over `root_folder`,
|
121
|
+
but it is ignored if `create_subfolders_per_run=True` (in which case, subfolders
|
122
|
+
will be spawned in the root folder).
|
123
123
|
"""
|
124
124
|
|
125
125
|
checkpoint_best_only: bool = False
|
@@ -195,7 +195,7 @@ class TrainConfig:
|
|
195
195
|
plots that are logged or saved at specified intervals.
|
196
196
|
"""
|
197
197
|
|
198
|
-
_subfolders: list = field(default_factory=list)
|
198
|
+
_subfolders: list[str] = field(default_factory=list)
|
199
199
|
"""List of subfolders used for logging different runs using the same config inside the.
|
200
200
|
|
201
201
|
root folder.
|
@@ -203,6 +203,67 @@ class TrainConfig:
|
|
203
203
|
Each subfolder is of structure `<id>_<timestamp>_<PID>`.
|
204
204
|
"""
|
205
205
|
|
206
|
+
nprocs: int = 1
|
207
|
+
"""
|
208
|
+
The number of processes to use for training when spawning subprocesses.
|
209
|
+
|
210
|
+
For effective parallel processing, set this to a value greater than 1.
|
211
|
+
- In case of Multi-GPU or Multi-Node-Multi-GPU setups, nprocs should be equal to
|
212
|
+
the total number of GPUs across all nodes (world size), or total number of GPU to be used.
|
213
|
+
|
214
|
+
If nprocs > 1, multiple processes will be spawned for training. The training framework will launch
|
215
|
+
additional processes (e.g., for distributed or parallel training).
|
216
|
+
- For CPU setup, this will launch a true parallel processes
|
217
|
+
- For GPU setup, this will launch a distributed training routine.
|
218
|
+
This uses the DistributedDataParallel framework from PyTorch.
|
219
|
+
"""
|
220
|
+
|
221
|
+
compute_setup: str = "cpu"
|
222
|
+
"""
|
223
|
+
Compute device setup; options are "auto", "gpu", or "cpu".
|
224
|
+
|
225
|
+
- "auto": Automatically uses GPU if available; otherwise, falls back to CPU.
|
226
|
+
- "gpu": Forces GPU usage, raising an error if no CUDA device is available.
|
227
|
+
- "cpu": Forces the use of CPU regardless of GPU availability.
|
228
|
+
"""
|
229
|
+
|
230
|
+
backend: str = "gloo"
|
231
|
+
"""
|
232
|
+
Backend used for distributed training communication.
|
233
|
+
|
234
|
+
The default is "gloo". Other options may include "nccl" - which is optimized for GPU-based training or "mpi",
|
235
|
+
depending on your system and requirements.
|
236
|
+
It should be one of the backends supported by `torch.distributed`. For further details, please look at
|
237
|
+
[torch backends](https://pytorch.org/docs/stable/distributed.html#torch.distributed.Backend)
|
238
|
+
"""
|
239
|
+
|
240
|
+
log_setup: str = "cpu"
|
241
|
+
"""
|
242
|
+
Logging device setup; options are "auto" or "cpu".
|
243
|
+
|
244
|
+
- "auto": Uses the same device for logging as for computation.
|
245
|
+
- "cpu": Forces logging to occur on the CPU. This can be useful to avoid potential conflicts with GPU processes.
|
246
|
+
"""
|
247
|
+
|
248
|
+
dtype: dtype | None = None
|
249
|
+
"""
|
250
|
+
Data type (precision) for computations.
|
251
|
+
|
252
|
+
Both model parameters, and dataset will be of the provided precision.
|
253
|
+
|
254
|
+
If not specified or None, the default torch precision (usually torch.float32) is used.
|
255
|
+
If provided dtype is torch.complex128, model parameters will be torch.complex128, and data parameters will be torch.float64
|
256
|
+
"""
|
257
|
+
|
258
|
+
all_reduce_metrics: bool = False
|
259
|
+
"""
|
260
|
+
Whether to aggregate metrics (e.g., loss, accuracy) across processes.
|
261
|
+
|
262
|
+
When True, metrics from different training processes are averaged to provide a consolidated metrics.
|
263
|
+
Note: Since aggregation requires synchronization/all_reduce operation, this can increase the
|
264
|
+
computation time significantly.
|
265
|
+
"""
|
266
|
+
|
206
267
|
|
207
268
|
@dataclass
|
208
269
|
class FeatureMapConfig:
|
qadence/ml_tools/constructors.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
from typing import Callable
|
3
4
|
import numpy as np
|
4
5
|
from sympy import Basic
|
5
6
|
|
@@ -24,7 +25,6 @@ from qadence.constructors.iia import iia
|
|
24
25
|
from qadence.measurements import Measurements
|
25
26
|
from qadence.noise import NoiseHandler
|
26
27
|
from qadence.operations import CNOT, RX, RY, I, N, Z
|
27
|
-
from qadence.parameters import Parameter
|
28
28
|
from qadence.register import Register
|
29
29
|
from qadence.types import (
|
30
30
|
AnsatzType,
|
@@ -33,10 +33,10 @@ from qadence.types import (
|
|
33
33
|
InputDiffMode,
|
34
34
|
Interaction,
|
35
35
|
MultivariateStrategy,
|
36
|
-
ObservableTransform,
|
37
36
|
ReuploadScaling,
|
38
37
|
Strategy,
|
39
38
|
TParameter,
|
39
|
+
TArray,
|
40
40
|
)
|
41
41
|
|
42
42
|
from .config import AnsatzConfig, FeatureMapConfig
|
@@ -706,35 +706,6 @@ def _interleave_ansatz_in_fm(
|
|
706
706
|
return chain(*full_fm)
|
707
707
|
|
708
708
|
|
709
|
-
def load_observable_transformations(config: ObservableConfig) -> tuple[Parameter, Parameter]:
|
710
|
-
"""
|
711
|
-
Get the observable shifting and scaling factors.
|
712
|
-
|
713
|
-
Args:
|
714
|
-
config (ObservableConfig): Observable configuration.
|
715
|
-
|
716
|
-
Returns:
|
717
|
-
tuple[Parameter, Parameter]: The observable shifting and scaling factors.
|
718
|
-
"""
|
719
|
-
shift = config.shift
|
720
|
-
scale = config.scale
|
721
|
-
if config.trainable_transform is not None:
|
722
|
-
shift = Parameter(name=shift, trainable=config.trainable_transform)
|
723
|
-
scale = Parameter(name=scale, trainable=config.trainable_transform)
|
724
|
-
else:
|
725
|
-
shift = Parameter(shift)
|
726
|
-
scale = Parameter(scale)
|
727
|
-
return scale, shift
|
728
|
-
|
729
|
-
|
730
|
-
ObservableTransformMap = {
|
731
|
-
ObservableTransform.RANGE: lambda detuning, scale, shift: (
|
732
|
-
(shift, shift - scale) if detuning is N else (0.5 * (shift - scale), 0.5 * (scale + shift))
|
733
|
-
),
|
734
|
-
ObservableTransform.SCALE: lambda _, scale, shift: (scale, shift),
|
735
|
-
}
|
736
|
-
|
737
|
-
|
738
709
|
def _global_identity(register: int | Register) -> KronBlock:
|
739
710
|
"""Create a global identity block."""
|
740
711
|
return kron(
|
@@ -742,7 +713,7 @@ def _global_identity(register: int | Register) -> KronBlock:
|
|
742
713
|
)
|
743
714
|
|
744
715
|
|
745
|
-
def
|
716
|
+
def create_observable(
|
746
717
|
register: int | Register,
|
747
718
|
config: ObservableConfig,
|
748
719
|
) -> AbstractBlock:
|
@@ -756,35 +727,11 @@ def observable_from_config(
|
|
756
727
|
Returns:
|
757
728
|
AbstractBlock: The observable block.
|
758
729
|
"""
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
def create_observable(
|
764
|
-
register: int | Register,
|
765
|
-
detuning: TDetuning = Z,
|
766
|
-
scale: TParameter | None = None,
|
767
|
-
shift: TParameter | None = None,
|
768
|
-
transformation_type: ObservableTransform = ObservableTransform.NONE, # type: ignore[assignment]
|
769
|
-
) -> AbstractBlock:
|
770
|
-
"""
|
771
|
-
Create an observable block.
|
772
|
-
|
773
|
-
Args:
|
774
|
-
register (int | Register): Number of qubits or a register object.
|
775
|
-
detuning: The type of detuning.
|
776
|
-
scale: A parameter for the scale.
|
777
|
-
shift: A parameter for the shift.
|
778
|
-
|
779
|
-
Returns:
|
780
|
-
AbstractBlock: The observable block.
|
781
|
-
"""
|
782
|
-
if transformation_type == ObservableTransform.RANGE:
|
783
|
-
scale, shift = ObservableTransformMap[transformation_type](detuning, scale, shift) # type: ignore[index]
|
784
|
-
shifting_term: AbstractBlock = shift * _global_identity(register) # type: ignore[operator]
|
785
|
-
detuning_hamiltonian: AbstractBlock = scale * hamiltonian_factory( # type: ignore[operator]
|
730
|
+
shifting_term: AbstractBlock = config.shift * _global_identity(register) # type: ignore[operator]
|
731
|
+
detuning_hamiltonian: AbstractBlock = config.scale * hamiltonian_factory( # type: ignore[operator]
|
786
732
|
register=register,
|
787
|
-
|
733
|
+
interaction=config.interaction,
|
734
|
+
detuning=config.detuning,
|
788
735
|
)
|
789
736
|
return add(shifting_term, detuning_hamiltonian)
|
790
737
|
|
@@ -844,9 +791,9 @@ def build_qnn_from_configs(
|
|
844
791
|
circ = QuantumCircuit(register, *blocks)
|
845
792
|
|
846
793
|
observable: AbstractBlock | list[AbstractBlock] = (
|
847
|
-
[
|
794
|
+
[create_observable(register=register, config=cfg) for cfg in observable_config]
|
848
795
|
if isinstance(observable_config, list)
|
849
|
-
else
|
796
|
+
else create_observable(register=register, config=observable_config)
|
850
797
|
)
|
851
798
|
|
852
799
|
ufa = QNN(
|
qadence/ml_tools/data.py
CHANGED
@@ -34,6 +34,10 @@ class OptimizeResult:
|
|
34
34
|
"""Metrics that can be saved during training."""
|
35
35
|
extra: dict = field(default_factory=lambda: dict())
|
36
36
|
"""Extra dict for saving anything else to be used in callbacks."""
|
37
|
+
rank: int = 0
|
38
|
+
"""Rank of the process for which this result was generated."""
|
39
|
+
device: str | None = "cpu"
|
40
|
+
"""Device on which this result for calculated."""
|
37
41
|
|
38
42
|
|
39
43
|
@dataclass
|
@@ -0,0 +1,339 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import functools
|
4
|
+
from logging import getLogger
|
5
|
+
from math import log, sqrt
|
6
|
+
from statistics import NormalDist
|
7
|
+
from typing import Any, Callable
|
8
|
+
|
9
|
+
import torch
|
10
|
+
from torch import nn
|
11
|
+
from torch.func import functional_call # type: ignore
|
12
|
+
|
13
|
+
logger = getLogger("ml_tools")
|
14
|
+
|
15
|
+
|
16
|
+
class InformationContent:
|
17
|
+
def __init__(
|
18
|
+
self,
|
19
|
+
model: nn.Module,
|
20
|
+
loss_fn: Callable,
|
21
|
+
xs: Any,
|
22
|
+
epsilons: torch.Tensor,
|
23
|
+
variation_multiple: int = 20,
|
24
|
+
) -> None:
|
25
|
+
"""Information Landscape class.
|
26
|
+
|
27
|
+
This class handles the study of loss landscape from information theoretic
|
28
|
+
perspective and provides methods to get bounds on the norm of the
|
29
|
+
gradient from the Information Content of the loss landscape.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
model: The quantum or classical model to analyze.
|
33
|
+
loss_fn: Loss function that takes model output and calculates loss
|
34
|
+
xs: Input data to evaluate the model on
|
35
|
+
epsilons: The thresholds to use for discretization of the finite derivatives
|
36
|
+
variation_multiple: The number of sets of variational parameters to generate per each
|
37
|
+
variational parameter. The number of variational parameters required for the
|
38
|
+
statistical analysis scales linearly with the amount of them present in the
|
39
|
+
model. This is that linear factor.
|
40
|
+
|
41
|
+
Notes:
|
42
|
+
This class provides flexibility in terms of what the model, the loss function,
|
43
|
+
and the xs are. The only requirement is that the loss_fn takes the model and xs as
|
44
|
+
arguments and returns the loss, and another dictionary of other metrics.
|
45
|
+
|
46
|
+
Thus, assumed structure:
|
47
|
+
loss_fn(model, xs) -> (loss, metrics, ...)
|
48
|
+
|
49
|
+
Example: A Classifier
|
50
|
+
```python
|
51
|
+
model = nn.Linear(10, 1)
|
52
|
+
|
53
|
+
def loss_fn(
|
54
|
+
model: nn.Module,
|
55
|
+
xs: tuple[torch.Tensor, torch.Tensor]
|
56
|
+
) -> tuple[torch.Tensor, dict[str, float]:
|
57
|
+
criterion = nn.MSELoss()
|
58
|
+
inputs, labels = xs
|
59
|
+
outputs = model(inputs)
|
60
|
+
loss = criterion(outputs, labels)
|
61
|
+
metrics = {"loss": loss.item()}
|
62
|
+
return loss, metrics
|
63
|
+
|
64
|
+
xs = (torch.randn(10, 10), torch.randn(10, 1))
|
65
|
+
|
66
|
+
info_landscape = InfoLandscape(model, loss_fn, xs)
|
67
|
+
```
|
68
|
+
In this example, the model is a linear classifier, and the `xs` include both the
|
69
|
+
inputs and the target labels. The logic for calculation of the loss from this lies
|
70
|
+
entirely within the `loss_fn` function. This can then further be used to obtain the
|
71
|
+
bounds on the average norm of the gradient of the loss function.
|
72
|
+
|
73
|
+
Example: A Physics Informed Neural Network
|
74
|
+
```python
|
75
|
+
class PhysicsInformedNN(nn.Module):
|
76
|
+
// <Initialization Logic>
|
77
|
+
|
78
|
+
def forward(self, xs: dict[str, torch.Tensor]):
|
79
|
+
return {
|
80
|
+
"pde_residual": pde_residual(xs["pde"]),
|
81
|
+
"boundary_condition": bc_term(xs["bc"]),
|
82
|
+
}
|
83
|
+
|
84
|
+
def loss_fn(
|
85
|
+
model: PhysicsInformedNN,
|
86
|
+
xs: dict[str, torch.Tensor]
|
87
|
+
) -> tuple[torch.Tensor, dict[str, float]:
|
88
|
+
pde_residual, bc_term = model(xs)
|
89
|
+
loss = torch.mean(torch.sum(pde_residual**2, dim=1), dim=0)
|
90
|
+
+ torch.mean(torch.sum(bc_term**2, dim=1), dim=0)
|
91
|
+
|
92
|
+
return loss, {"pde_residual": pde_residual, "bc_term": bc_term}
|
93
|
+
|
94
|
+
xs = {
|
95
|
+
"pde": torch.linspace(0, 1, 10),
|
96
|
+
"bc": torch.tensor([0.0]),
|
97
|
+
}
|
98
|
+
|
99
|
+
info_landscape = InfoLandscape(model, loss_fn, xs)
|
100
|
+
```
|
101
|
+
|
102
|
+
In this example, the model is a Physics Informed Neural Network, and the `xs`
|
103
|
+
are the inputs to the different residual components of the model. The logic
|
104
|
+
for calculation of the residuals lies within the PhysicsInformedNN class, and
|
105
|
+
the loss function is defined to calculate the loss that is to be optimized
|
106
|
+
from these residuals. This can then further be used to obtain the
|
107
|
+
bounds on the average norm of the gradient of the loss function.
|
108
|
+
|
109
|
+
The first value that the `loss_fn` returns is the loss value that is being optimized.
|
110
|
+
The function is also expected to return other value(s), often the metrics that are
|
111
|
+
used to calculate the loss. These values are ignored for the purpose of this class.
|
112
|
+
"""
|
113
|
+
self.model = model
|
114
|
+
self.loss_fn = loss_fn
|
115
|
+
self.xs = xs
|
116
|
+
self.epsilons = epsilons
|
117
|
+
self.device = next(model.parameters()).device
|
118
|
+
|
119
|
+
self.param_shapes = {}
|
120
|
+
self.total_params = 0
|
121
|
+
|
122
|
+
for name, param in model.named_parameters():
|
123
|
+
self.param_shapes[name] = param.shape
|
124
|
+
self.total_params += param.numel()
|
125
|
+
self.n_variations = variation_multiple * self.total_params
|
126
|
+
self.all_variations = torch.empty(
|
127
|
+
(self.n_variations, self.total_params), device=self.device
|
128
|
+
).uniform_(0, 2 * torch.pi)
|
129
|
+
|
130
|
+
def reshape_param_variations(self) -> dict[str, torch.Tensor]:
|
131
|
+
"""Reshape variations of the model's variational parameters.
|
132
|
+
|
133
|
+
Returns:
|
134
|
+
Dictionary of parameter tensors, each with shape [n_variations, *param_shape]
|
135
|
+
"""
|
136
|
+
param_variations = {}
|
137
|
+
start_idx = 0
|
138
|
+
|
139
|
+
for name, shape in self.param_shapes.items():
|
140
|
+
param_size = torch.prod(torch.tensor(shape)).item()
|
141
|
+
param_variations[name] = self.all_variations[
|
142
|
+
:, start_idx : start_idx + param_size
|
143
|
+
].view(self.n_variations, *shape)
|
144
|
+
start_idx += param_size
|
145
|
+
|
146
|
+
return param_variations
|
147
|
+
|
148
|
+
def batched_loss(self) -> torch.Tensor:
|
149
|
+
"""Calculate loss for all parameter variations in a batched manner.
|
150
|
+
|
151
|
+
Returns: Tensor of loss values for each parameter variation
|
152
|
+
"""
|
153
|
+
param_variations = self.reshape_param_variations()
|
154
|
+
losses = torch.zeros(self.n_variations, device=self.device)
|
155
|
+
|
156
|
+
for i in range(self.n_variations):
|
157
|
+
params = {name: param[i] for name, param in param_variations.items()}
|
158
|
+
current_model = lambda x: functional_call(self.model, params, (x,))
|
159
|
+
losses[i] = self.loss_fn(current_model, self.xs)[0]
|
160
|
+
|
161
|
+
return losses
|
162
|
+
|
163
|
+
def randomized_finite_der(self) -> torch.Tensor:
|
164
|
+
"""
|
165
|
+
Calculate normalized finite difference of loss on doing random walk in the parameter space.
|
166
|
+
|
167
|
+
This serves as a proxy for the derivative of the loss with respect to parameters.
|
168
|
+
|
169
|
+
Returns:
|
170
|
+
Tensor containing normalized finite differences (approximate directional derivatives)
|
171
|
+
between consecutive points in the random walk. Shape: [n_variations - 1]
|
172
|
+
"""
|
173
|
+
losses = self.batched_loss()
|
174
|
+
|
175
|
+
return (losses[1:] - losses[:-1]) / (
|
176
|
+
torch.norm(self.all_variations[1:] - self.all_variations[:-1], dim=1) + 1e-8
|
177
|
+
)
|
178
|
+
|
179
|
+
def discretize_derivatives(self) -> torch.Tensor:
|
180
|
+
"""
|
181
|
+
Convert finite derivatives into discrete values.
|
182
|
+
|
183
|
+
Returns:
|
184
|
+
Tensor containing discretized derivatives with shape [n_epsilons, n_variations-2]
|
185
|
+
Each row contains {-1, 0, 1} values for that epsilon
|
186
|
+
"""
|
187
|
+
derivatives = self.randomized_finite_der()
|
188
|
+
|
189
|
+
derivatives = derivatives.unsqueeze(0)
|
190
|
+
epsilons = self.epsilons.unsqueeze(1)
|
191
|
+
|
192
|
+
discretized = torch.zeros((len(epsilons), len(derivatives[0])), device=self.device)
|
193
|
+
discretized[derivatives > epsilons] = 1
|
194
|
+
discretized[derivatives < -epsilons] = -1
|
195
|
+
|
196
|
+
return discretized
|
197
|
+
|
198
|
+
def calculate_transition_probabilities_batch(self) -> torch.Tensor:
|
199
|
+
"""
|
200
|
+
Calculate transition probabilities for multiple epsilon values.
|
201
|
+
|
202
|
+
Returns:
|
203
|
+
Tensor of shape [n_epsilons, 6] containing probabilities for each transition type
|
204
|
+
Columns order: [+1to0, +1to-1, 0to+1, 0to-1, -1to0, -1to+1]
|
205
|
+
"""
|
206
|
+
discretized = self.discretize_derivatives()
|
207
|
+
|
208
|
+
current = discretized[:, :-1]
|
209
|
+
next_val = discretized[:, 1:]
|
210
|
+
|
211
|
+
transitions = torch.stack(
|
212
|
+
[
|
213
|
+
((current == 1) & (next_val == 0)).sum(dim=1),
|
214
|
+
((current == 1) & (next_val == -1)).sum(dim=1),
|
215
|
+
((current == 0) & (next_val == 1)).sum(dim=1),
|
216
|
+
((current == 0) & (next_val == -1)).sum(dim=1),
|
217
|
+
((current == -1) & (next_val == 0)).sum(dim=1),
|
218
|
+
((current == -1) & (next_val == 1)).sum(dim=1),
|
219
|
+
],
|
220
|
+
dim=1,
|
221
|
+
).float()
|
222
|
+
|
223
|
+
total_transitions = current.size(1)
|
224
|
+
probabilities = transitions / total_transitions
|
225
|
+
|
226
|
+
return probabilities
|
227
|
+
|
228
|
+
@functools.cached_property
|
229
|
+
def calculate_IC(self) -> torch.Tensor:
|
230
|
+
"""
|
231
|
+
Calculate Information Content for multiple epsilon values.
|
232
|
+
|
233
|
+
Returns: Tensor of IC values for each epsilon [n_epsilons]
|
234
|
+
"""
|
235
|
+
probs = self.calculate_transition_probabilities_batch()
|
236
|
+
|
237
|
+
mask = probs > 1e-4
|
238
|
+
|
239
|
+
ic_terms = torch.where(mask, -probs * torch.log(probs), torch.zeros_like(probs))
|
240
|
+
ic_values = ic_terms.sum(dim=1) / torch.log(torch.tensor(6.0))
|
241
|
+
|
242
|
+
return ic_values
|
243
|
+
|
244
|
+
def max_IC(self) -> tuple[float, float]:
|
245
|
+
"""
|
246
|
+
Get the maximum Information Content and its corresponding epsilon.
|
247
|
+
|
248
|
+
Returns: Tuple of (maximum IC value, optimal epsilon)
|
249
|
+
"""
|
250
|
+
max_ic, max_idx = torch.max(self.calculate_IC, dim=0)
|
251
|
+
max_epsilon = self.epsilons[max_idx]
|
252
|
+
return max_ic.item(), max_epsilon.item()
|
253
|
+
|
254
|
+
def sensitivity_IC(self, eta: float) -> float:
|
255
|
+
"""
|
256
|
+
Find the minimum value of epsilon such that the information content is less than eta.
|
257
|
+
|
258
|
+
Args:
|
259
|
+
eta: Threshold value, the sensitivity IC.
|
260
|
+
|
261
|
+
Returns: The epsilon value that gives IC that is less than the sensitivity IC.
|
262
|
+
"""
|
263
|
+
ic_values = self.calculate_IC
|
264
|
+
mask = ic_values < eta
|
265
|
+
epsilons = self.epsilons[mask]
|
266
|
+
return float(epsilons.min().item())
|
267
|
+
|
268
|
+
@staticmethod
|
269
|
+
@functools.lru_cache
|
270
|
+
def q_value(H_value: float) -> float:
|
271
|
+
"""
|
272
|
+
Compute the q value.
|
273
|
+
|
274
|
+
q is the solution to the equation:
|
275
|
+
H(x) = 4h(x) + 2h(1/2 - 2x)
|
276
|
+
|
277
|
+
It is the value of the probability of 4 of the 6 transitions such that
|
278
|
+
the IC is the same as the IC of our system.
|
279
|
+
|
280
|
+
This quantity is useful in calculating the bounds on the norms of the gradients.
|
281
|
+
|
282
|
+
Args:
|
283
|
+
H_value (float): The information content.
|
284
|
+
|
285
|
+
Returns:
|
286
|
+
float: The q value
|
287
|
+
"""
|
288
|
+
|
289
|
+
x = torch.linspace(0.001, 0.16667, 10000)
|
290
|
+
|
291
|
+
H = -4 * x * torch.log(x) / torch.log(torch.tensor(6)) - 2 * (0.5 - 2 * x) * torch.log(
|
292
|
+
0.5 - 2 * x
|
293
|
+
) / torch.log(torch.tensor(6))
|
294
|
+
err = torch.abs(H - H_value)
|
295
|
+
idx = torch.argmin(err)
|
296
|
+
return float(x[idx].item())
|
297
|
+
|
298
|
+
def get_grad_norm_bounds_max_IC(self) -> tuple[float, float]:
|
299
|
+
"""
|
300
|
+
Compute the bounds on the average norm of the gradient.
|
301
|
+
|
302
|
+
Returns:
|
303
|
+
tuple[Tensor, Tensor]: The lower and upper bounds.
|
304
|
+
"""
|
305
|
+
max_IC, epsilon_m = self.max_IC()
|
306
|
+
lower_bound = (
|
307
|
+
epsilon_m
|
308
|
+
* sqrt(self.total_params)
|
309
|
+
/ (NormalDist().inv_cdf(1 - 2 * self.q_value(max_IC)))
|
310
|
+
)
|
311
|
+
upper_bound = (
|
312
|
+
epsilon_m
|
313
|
+
* sqrt(self.total_params)
|
314
|
+
/ (NormalDist().inv_cdf(0.5 * (1 + 2 * self.q_value(max_IC))))
|
315
|
+
)
|
316
|
+
|
317
|
+
if max_IC < log(2, 6):
|
318
|
+
logger.warning(
|
319
|
+
"Warning: The maximum IC is less than the required value. The bounds may be"
|
320
|
+
+ " inaccurate."
|
321
|
+
)
|
322
|
+
|
323
|
+
return lower_bound, upper_bound
|
324
|
+
|
325
|
+
def get_grad_norm_bounds_sensitivity_IC(self, eta: float) -> float:
|
326
|
+
"""
|
327
|
+
Compute the bounds on the average norm of the gradient.
|
328
|
+
|
329
|
+
Args:
|
330
|
+
eta (float): The sensitivity IC.
|
331
|
+
|
332
|
+
Returns:
|
333
|
+
Tensor: The lower bound.
|
334
|
+
"""
|
335
|
+
epsilon_sensitivity = self.sensitivity_IC(eta)
|
336
|
+
upper_bound = (
|
337
|
+
epsilon_sensitivity * sqrt(self.total_params) / (NormalDist().inv_cdf(1 - 3 * eta / 2))
|
338
|
+
)
|
339
|
+
return upper_bound
|
qadence/ml_tools/models.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from collections import Counter
|
3
|
+
from collections import Counter, OrderedDict
|
4
4
|
from logging import getLogger
|
5
5
|
from typing import Any, Callable
|
6
6
|
|
@@ -19,6 +19,7 @@ from qadence.model import QuantumModel
|
|
19
19
|
from qadence.noise import NoiseHandler
|
20
20
|
from qadence.register import Register
|
21
21
|
from qadence.types import BackendName, DiffMode, Endianness, InputDiffMode, ParamDictType
|
22
|
+
from qadence.utils import block_to_mathematical_expression
|
22
23
|
|
23
24
|
logger = getLogger(__name__)
|
24
25
|
|
@@ -208,6 +209,8 @@ class QNN(QuantumModel):
|
|
208
209
|
else:
|
209
210
|
raise ValueError(f"Unkown forward diff mode: {self.input_diff_mode}")
|
210
211
|
|
212
|
+
self._model_configs: dict = dict()
|
213
|
+
|
211
214
|
@classmethod
|
212
215
|
def from_configs(
|
213
216
|
cls,
|
@@ -255,7 +258,7 @@ class QNN(QuantumModel):
|
|
255
258
|
from qadence.constructors import ObservableConfig
|
256
259
|
from qadence.operations import Z
|
257
260
|
from qadence.types import (
|
258
|
-
AnsatzType, BackendName, BasisSet,
|
261
|
+
AnsatzType, BackendName, BasisSet, ReuploadScaling, Strategy
|
259
262
|
)
|
260
263
|
|
261
264
|
register = 4
|
@@ -263,7 +266,6 @@ class QNN(QuantumModel):
|
|
263
266
|
detuning=Z,
|
264
267
|
scale=5.0,
|
265
268
|
shift=0.0,
|
266
|
-
transformation_type=ObservableTransform.SCALE,
|
267
269
|
trainable_transform=None,
|
268
270
|
)
|
269
271
|
fm_config = FeatureMapConfig(
|
@@ -293,7 +295,7 @@ class QNN(QuantumModel):
|
|
293
295
|
"""
|
294
296
|
from .constructors import build_qnn_from_configs
|
295
297
|
|
296
|
-
|
298
|
+
qnn = build_qnn_from_configs(
|
297
299
|
register=register,
|
298
300
|
observable_config=obs_config,
|
299
301
|
fm_config=fm_config,
|
@@ -305,6 +307,69 @@ class QNN(QuantumModel):
|
|
305
307
|
configuration=configuration,
|
306
308
|
input_diff_mode=input_diff_mode,
|
307
309
|
)
|
310
|
+
qnn._model_configs = {
|
311
|
+
"register": register,
|
312
|
+
"observable_config": obs_config,
|
313
|
+
"fm_config": fm_config,
|
314
|
+
"ansatz_config": ansatz_config,
|
315
|
+
}
|
316
|
+
return qnn
|
317
|
+
|
318
|
+
def __str__(self) -> str | Any:
|
319
|
+
"""Return a string representation of a QNN.
|
320
|
+
|
321
|
+
When creating a QNN from a set of configurations,
|
322
|
+
we print the configurations used. Otherwise, we use the default printing.
|
323
|
+
|
324
|
+
Returns:
|
325
|
+
str | Any: A string representation of a QNN.
|
326
|
+
|
327
|
+
Example:
|
328
|
+
```python exec="on" source="material-block" result="json"
|
329
|
+
from qadence import QNN
|
330
|
+
from qadence.constructors.hamiltonians import Interaction
|
331
|
+
from qadence.ml_tools.config import AnsatzConfig, FeatureMapConfig
|
332
|
+
from qadence.ml_tools.constructors import (
|
333
|
+
ObservableConfig,
|
334
|
+
)
|
335
|
+
from qadence.operations import Z
|
336
|
+
from qadence.types import BackendName
|
337
|
+
|
338
|
+
backend = BackendName.PYQTORCH
|
339
|
+
fm_config = FeatureMapConfig(num_features=1)
|
340
|
+
ansatz_config = AnsatzConfig()
|
341
|
+
observable_config = ObservableConfig(detuning=Z, interaction=Interaction.ZZ, scale=2)
|
342
|
+
|
343
|
+
qnn = QNN.from_configs(
|
344
|
+
register=2,
|
345
|
+
obs_config=observable_config,
|
346
|
+
fm_config=fm_config,
|
347
|
+
ansatz_config=ansatz_config,
|
348
|
+
backend=backend,
|
349
|
+
)
|
350
|
+
print(qnn) # markdown-exec: hide
|
351
|
+
```
|
352
|
+
"""
|
353
|
+
if bool(self._model_configs):
|
354
|
+
configs_str = "\n".join(
|
355
|
+
(
|
356
|
+
k + " = " + str(self._model_configs[k])
|
357
|
+
for k in sorted(self._model_configs.keys())
|
358
|
+
if k != "observable_config"
|
359
|
+
)
|
360
|
+
)
|
361
|
+
observable_str = ""
|
362
|
+
if self._observable:
|
363
|
+
observable_str = (
|
364
|
+
"observable_config = [\n"
|
365
|
+
+ "\n".join(
|
366
|
+
(block_to_mathematical_expression(obs.original) for obs in self._observable)
|
367
|
+
)
|
368
|
+
+ "\n]"
|
369
|
+
)
|
370
|
+
return f"{type(self).__name__}(\n{configs_str}\n{observable_str}\n)"
|
371
|
+
|
372
|
+
return super().__str__()
|
308
373
|
|
309
374
|
def forward(
|
310
375
|
self,
|