1"""Gradient and partial derivative calculation methods for parameterized quantum circuits."""
2from typing import Literal
3
4import numpy as np
5
6from qiskit import QuantumCircuit
7from qiskit.circuit import Parameter
8from qiskit_machine_learning.gradients import (
9 BaseSamplerGradient,
10 LinCombSamplerGradient,
11 SPSASamplerGradient,
12 ParamShiftSamplerGradient,
13)
14
15from quantum_launcher.base.base import Backend
16from quantum_launcher.routines.qiskit_routines import QiskitBackend
17
18
19def _param_grads_to_jacobian(grads, num_possible_values) -> np.ndarray:
20 grads_list = [[g.get(i, 0) for i in range(num_possible_values)] for g in grads]
21 return np.array(grads_list)
22
23
[docs]
24def calculate_jacobian(
25 circuit: QuantumCircuit,
26 set_params: dict[Parameter, int | float],
27 backend: Backend,
28 method: Literal['param_shift', 'spsa', 'lin_comb'] = 'param_shift',
29 shots: int = 1024
30) -> np.ndarray:
31 """
32 For each parameter calculate partial derivatives w.r.t to each output value (possible measurement).
33
34 Args:
35 circuit (QuantumCircuit): Circuit to sample.
36 set_params (dict[Parameter, int | float]): Parameters for which to calculate derivatives and their current values.
37 backend (Backend): Backend to use.
38 method (Literal['param_shift', 'spsa', 'lin_comb'], optional):
39 Gradient algorithm to use. Defaults to 'param_shift'.
40 shots (int): How many shots to use for Sampler.
41
42 Raises:
43 ValueError: For unsupported method or backend.
44
45 Returns:
46 np.ndarray: `len(set_params) x 2^measured_qubits` matrix of partial derivatives.
47 """
48 if not isinstance(backend, QiskitBackend):
49 raise ValueError("Only qiskit backends are supported.")
50
51 num_possible_values = 2**circuit.num_clbits
52
53 set_params_amp = {k: v for k, v in set_params.items() if 'Amp_Encoder' in k.name}
54 set_params_compatible = {k: v for k, v in set_params.items() if 'Amp_Encoder' not in k.name}
55
56 grads_amp = calculate_gradients_incompatible(circuit.assign_parameters(set_params_compatible), set_params_amp, backend, shots, 0.01)
57 grads_compat = calculate_gradients_compatible(circuit.assign_parameters(set_params_amp), set_params_compatible, backend, method, shots)
58
59 grads_amp = dict(zip(set_params_amp.keys(), grads_amp))
60 grads_compat = dict(zip(set_params_compatible.keys(), grads_compat))
61
62 grads = grads_amp | grads_compat
63
64 return _param_grads_to_jacobian([grads[k] for k in set_params.keys()], num_possible_values)
65
66
[docs]
67def calculate_gradients_compatible(
68 circuit: QuantumCircuit,
69 set_params: dict[Parameter, int | float],
70 backend: QiskitBackend,
71 method: Literal['param_shift', 'spsa', 'lin_comb'] = 'param_shift',
72 shots: int = 1024
73):
74 """
75 Calculate gradients for parameters in compatible gates.
76
77 Args:
78 circuit (QuantumCircuit): Circuit to sample.
79 set_params (dict[Parameter, int | float]): Parameters for which to calculate derivatives and their current values.
80 backend (QiskitBackend): Backend to use.
81 method (Literal['param_shift', 'spsa', 'lin_comb'], optional):
82 Gradient algorithm to use. Defaults to 'param_shift'.
83 shots (int): How many shots to use for Sampler.
84
85 Raises:
86 ValueError: For unsupported method or backend.
87
88 Returns:
89 np.ndarray: `len(set_params) x 2^measured_qubits` matrix of partial derivatives.
90 """
91 gradient_type = {
92 'param_shift': ParamShiftSamplerGradient,
93 'spsa': lambda sampler: SPSASamplerGradient(sampler, epsilon=0.001, batch_size=10),
94 'lin_comb': LinCombSamplerGradient,
95 }.get(method, None)
96
97 if gradient_type is None:
98 raise ValueError(f"Unsupported method {method}")
99
100 gradient_calc: BaseSamplerGradient = gradient_type(backend.samplerV1)
101 if len(set_params) == 0:
102 return []
103 params, values = zip(*list(set_params.items()))
104
105 grads = gradient_calc.run([circuit], [values], [params], shots=shots).result().gradients[0]
106 return grads
107
108
[docs]
109def calculate_gradients_incompatible(
110 circuit: QuantumCircuit,
111 set_params: dict[Parameter, int | float],
112 backend: QiskitBackend,
113 shots: int = 1024,
114 epsilon: float = 0.01
115):
116 """
117 Calculate gradients for parameters in incompatible gates, e.g. amplitude encoding parameters.
118 Calculation is done using the standard derivative formula (f(x+h) - f(x))/h
119
120 Args:
121 circuit (QuantumCircuit): Circuit to sample.
122 set_params (dict[Parameter, int | float]): Parameters for which to calculate derivatives and their current values.
123 backend (QiskitBackend): Backend to use.
124 shots (int): How many shots to use for Sampler.
125 epsilon (float): How much to shift the parameter.
126
127 Raises:
128 ValueError: For unsupported method or backend.
129
130 Returns:
131 np.ndarray: `len(set_params) x 2^measured_qubits` matrix of partial derivatives.
132 """
133 initial_distribution = backend.samplerV1.run(circuit.assign_parameters(set_params), shots=shots).result().quasi_dists[0]
134 shifted_results = []
135 for param in set_params:
136 distribution = backend.samplerV1.run(
137 circuit.assign_parameters(
138 set_params | {param: set_params[param] + epsilon}
139 ),
140 shots=shots).result().quasi_dists[0]
141
142 for k in initial_distribution:
143 distribution[k] = distribution.get(k, 0) - initial_distribution[k]
144
145 for k in distribution:
146 distribution[k] /= epsilon
147
148 shifted_results.append(distribution)
149 return shifted_results