qsppack 0.1.0__tar.gz

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.
qsppack-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: qsppack
3
+ Version: 0.1.0
4
+ Summary: Quantum Signal Processing optimization and analysis
5
+ Author: James Larsen
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/beevus77/qsp
8
+ Project-URL: Bug Tracker, https://github.com/beevus77/qsp/issues
9
+ Project-URL: Documentation, https://qsp.readthedocs.io/
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Topic :: Scientific/Engineering :: Physics
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/x-rst
18
+
19
+ qsp - Quantum Signal Processing Optimization
20
+ ============================================
21
+
22
+ A Python package for Quantum Signal Processing optimization and analysis.
23
+
24
+ Installation
25
+ ------------
26
+
27
+ You can install the package using pip:
28
+
29
+ .. code-block:: bash
30
+
31
+ pip install qsppack
32
+
33
+ Or install from source:
34
+
35
+ .. code-block:: bash
36
+
37
+ git clone https://github.com/beevus77/qsp.git
38
+ cd qsp
39
+ pip install -e .
40
+
41
+ Documentation
42
+ -------------
43
+
44
+ Full documentation is available at: https://qsp.readthedocs.io/
45
+
46
+ Features
47
+ --------
48
+
49
+ * Quantum Signal Processing optimization
50
+ * Support for various optimization methods (L-BFGS, FPI, Newton)
51
+ * Utility functions for Chebyshev polynomials and phase factor manipulation
52
+
53
+ License
54
+ -------
55
+
56
+ This project is licensed under the MIT License - see the LICENSE file for details.
@@ -0,0 +1,38 @@
1
+ qsp - Quantum Signal Processing Optimization
2
+ ============================================
3
+
4
+ A Python package for Quantum Signal Processing optimization and analysis.
5
+
6
+ Installation
7
+ ------------
8
+
9
+ You can install the package using pip:
10
+
11
+ .. code-block:: bash
12
+
13
+ pip install qsppack
14
+
15
+ Or install from source:
16
+
17
+ .. code-block:: bash
18
+
19
+ git clone https://github.com/beevus77/qsp.git
20
+ cd qsp
21
+ pip install -e .
22
+
23
+ Documentation
24
+ -------------
25
+
26
+ Full documentation is available at: https://qsp.readthedocs.io/
27
+
28
+ Features
29
+ --------
30
+
31
+ * Quantum Signal Processing optimization
32
+ * Support for various optimization methods (L-BFGS, FPI, Newton)
33
+ * Utility functions for Chebyshev polynomials and phase factor manipulation
34
+
35
+ License
36
+ -------
37
+
38
+ This project is licensed under the MIT License - see the LICENSE file for details.
@@ -0,0 +1,121 @@
1
+ """Main solver interface for Quantum Signal Processing optimization.
2
+
3
+ This module provides the main interface for solving QSP optimization problems,
4
+ coordinating the various optimization methods and utility functions.
5
+ """
6
+
7
+ import numpy as np
8
+ from time import time
9
+ from .utils import chebyshev_to_func
10
+ from .objective import obj_sym, grad_sym, grad_sym_real
11
+ from .optimizers import lbfgs, coordinate_minimization, newton
12
+ from .core import reduced_to_full
13
+
14
+ def solve(coef, parity, opts):
15
+ """Given coefficients of a polynomial P, yield corresponding phase factors.
16
+
17
+ The reference chose the first half of the phase factors as the
18
+ optimization variables, while in the code we used the second half of the
19
+ phase factors. These two formulations are equivalent.
20
+
21
+ To simplify the representation, a constant pi/4 is added to both sides of
22
+ the phase factors when evaluating the objective and the gradient. In the
23
+ output, the FULL phase factors with pi/4 are given.
24
+
25
+ Parameters
26
+ ----------
27
+ coef : array_like
28
+ Coefficients of polynomial P under Chebyshev basis. P should be even/odd,
29
+ only provide non-zero coefficients. Coefficients should be ranked from
30
+ low order term to high order term.
31
+ parity : int
32
+ Parity of polynomial P (0 -- even, 1 -- odd)
33
+ opts : dict, optional
34
+ Options dictionary with fields:
35
+
36
+ - criteria : float
37
+ Stop criteria
38
+ - useReal : bool
39
+ Use only real arithmetics if true
40
+ - targetPre : bool
41
+ Want Pre to be target function if true
42
+ - method : {'LBFGS', 'FPI', 'Newton'}
43
+ Optimization method to use
44
+ - typePhi : {'full', 'reduced'}
45
+ Type of phase factors to return
46
+
47
+ Returns
48
+ -------
49
+ phi_proc : ndarray
50
+ Solution of optimization problem, FULL phase factors
51
+ out : dict
52
+ Information of solving process containing:
53
+
54
+ - iter : int
55
+ Number of iterations
56
+ - time : float
57
+ Runtime in seconds
58
+ - value : float
59
+ Final error value
60
+ - parity : int
61
+ Input parity value
62
+ - targetPre : bool
63
+ Whether Pre was target function
64
+ - typePhi : str
65
+ Type of phase factors returned
66
+ """
67
+ # Setup options for L-BFGS solver
68
+ opts.setdefault('maxiter', 5e4)
69
+ opts.setdefault('criteria', 1e-12)
70
+ opts.setdefault('useReal', True)
71
+ opts.setdefault('targetPre', True)
72
+ opts.setdefault('method', 'FPI')
73
+ opts.setdefault('typePhi', 'full')
74
+
75
+ if opts['method'] == 'LBFGS':
76
+ # Initial preparation
77
+ tot_len = len(coef)
78
+ delta = np.cos((np.arange(1, 2 * tot_len, 2) * (np.pi / (2 * tot_len))))
79
+ if not opts['targetPre']:
80
+ opts['target'] = lambda x: -chebyshev_to_func(x, coef, parity, True)
81
+ else:
82
+ opts['target'] = lambda x: chebyshev_to_func(x, coef, parity, True)
83
+ opts['parity'] = parity
84
+ obj = obj_sym
85
+ grad = grad_sym_real if opts['useReal'] else grad_sym
86
+
87
+ # Solve by L-BFGS with selected initial point
88
+ start_time = time()
89
+ phi, err, iter = lbfgs(obj, grad, delta, np.zeros(tot_len), opts)
90
+ # Convert phi to reduced phase factors
91
+ if parity == 0:
92
+ phi[0] = phi[0] / 2
93
+ runtime = time() - start_time
94
+
95
+ elif opts['method'] == 'FPI':
96
+ phi, err, iter, runtime = coordinate_minimization(coef, parity, opts)
97
+
98
+ elif opts['method'] == 'Newton':
99
+ phi, err, iter, runtime = newton(coef, parity, opts)
100
+
101
+ else:
102
+ print("Assigned method doesn't exist. Please choose method from 'LBFGS', 'FPI' or 'Newton'.")
103
+ return None, None
104
+
105
+ # Output information
106
+ out = {
107
+ 'iter': iter,
108
+ 'time': runtime,
109
+ 'value': err,
110
+ 'parity': parity,
111
+ 'targetPre': opts['targetPre']
112
+ }
113
+
114
+ if opts['typePhi'] == 'full':
115
+ phi_proc = reduced_to_full(phi, parity, opts['targetPre'])
116
+ out['typePhi'] = 'full'
117
+ else:
118
+ phi_proc = phi
119
+ out['typePhi'] = 'reduced'
120
+
121
+ return phi_proc, out
@@ -0,0 +1,25 @@
1
+ """Quantum Signal Processing Optimization Package.
2
+
3
+ This package provides tools for optimizing phase factors in Quantum Signal
4
+ Processing problems.
5
+ """
6
+
7
+ from .QSP_solver import *
8
+ from .core import *
9
+ from .utils import *
10
+ from .objective import *
11
+ from .optimizers import *
12
+
13
+ __all__ = [
14
+ # QSP_solver
15
+ 'solve',
16
+ # core
17
+ 'get_unitary',
18
+ 'get_unitary_sym',
19
+ 'get_entry',
20
+ 'reduced_to_full',
21
+ # utils
22
+ 'chebyshev_to_func',
23
+ 'cvx_poly_coef',
24
+ # Add all other public functions here
25
+ ]
@@ -0,0 +1,265 @@
1
+ """Core Quantum Signal Processing functionality.
2
+
3
+ This module provides the fundamental operations needed for Quantum Signal Processing,
4
+ including unitary matrix construction and manipulation of phase factors.
5
+ """
6
+
7
+ import numpy as np
8
+ from .utils import get_unitary_sym, get_pim_sym, get_pim_sym_real, get_pim_deri_sym, get_pim_deri_sym_real
9
+
10
+ def get_unitary(phase, x):
11
+ """Compute QSP unitary matrix for given phase factors.
12
+
13
+ This function constructs the full QSP unitary matrix for a given set of phase
14
+ factors at a specific point. The unitary is built by alternating W(x) gates
15
+ and phase rotations.
16
+
17
+ Parameters
18
+ ----------
19
+ phase : array_like
20
+ Phase factors for the QSP circuit. These are used to construct the
21
+ phase rotation gates in the circuit.
22
+ x : float
23
+ Point at which to evaluate the unitary, must be in [-1, 1].
24
+
25
+ Returns
26
+ -------
27
+ float
28
+ Real part of the (1,1) element of the QSP unitary matrix, which
29
+ represents the QSP approximation of the target function.
30
+
31
+ Notes
32
+ -----
33
+ The unitary is constructed as:
34
+ U = R(phi_0) * W(x) * R(phi_1) * W(x) * ... * R(phi_n)
35
+ where R(phi) is a phase rotation gate and W(x) is the signal processing gate.
36
+ """
37
+ Wx = np.array([[x, 1j * np.sqrt(1 - x**2)], [1j * np.sqrt(1 - x**2), x]])
38
+ expphi = np.exp(1j * phase)
39
+
40
+ ret = np.array([[expphi[0], 0], [0, np.conj(expphi[0])]])
41
+
42
+ for k in range(1, len(expphi)):
43
+ temp = np.array([[expphi[k], 0], [0, np.conj(expphi[k])]])
44
+ ret = np.dot(np.dot(ret, Wx), temp)
45
+
46
+ targ = np.real(ret[0, 0])
47
+
48
+ return targ
49
+
50
+ def get_entry(xlist, phase, opts):
51
+ """Compute QSP unitary matrix entries for multiple points.
52
+
53
+ This function evaluates the QSP unitary matrix at multiple points, handling
54
+ both full and reduced phase factors. It can compute either the real or
55
+ imaginary part of the (1,1) element based on the options.
56
+
57
+ Parameters
58
+ ----------
59
+ xlist : array_like
60
+ Points at which to evaluate the QSP unitary, must be in [-1, 1].
61
+ phase : array_like
62
+ Phase factors for the QSP circuit. Can be either full or reduced
63
+ phase factors depending on opts['typePhi'].
64
+ opts : dict
65
+ Options dictionary containing:
66
+
67
+ - targetPre : bool
68
+ If True, compute real part (Pre), otherwise compute imaginary part (Pim)
69
+ - parity : int
70
+ Parity of the polynomial (0 for even, 1 for odd)
71
+ - typePhi : {'full', 'reduced'}
72
+ Type of phase factors provided:
73
+
74
+ - 'full' : complete set of phase factors
75
+ - 'reduced' : reduced set of phase factors (will be expanded)
76
+
77
+ Returns
78
+ -------
79
+ ndarray
80
+ QSP approximation values at each point in xlist. For targetPre=True,
81
+ returns real part of (1,1) element; for targetPre=False, returns
82
+ imaginary part.
83
+
84
+ Notes
85
+ -----
86
+ When typePhi='reduced', the function expands the reduced phase factors to
87
+ full phase factors using symmetry. For targetPre=False, it also adjusts
88
+ the first and last phase factors by -π/4 to compute the imaginary part.
89
+ """
90
+ typePhi = opts['typePhi']
91
+ targetPre = opts['targetPre']
92
+ parity = opts['parity']
93
+
94
+ d = len(xlist)
95
+ ret = np.zeros(d)
96
+
97
+ if typePhi == 'reduced':
98
+ dd = 2 * len(phase) - 1 + parity
99
+ phi = np.zeros(dd)
100
+ phi[(dd - len(phase)):] = phase
101
+ phi[:len(phase)] += phase[::-1]
102
+ else:
103
+ phi = phase
104
+
105
+ if not targetPre:
106
+ phi[0] -= np.pi / 4
107
+ phi[-1] -= np.pi / 4
108
+
109
+ for i in range(d):
110
+ x = xlist[i]
111
+ ret[i] = get_unitary(phi, x)
112
+
113
+ return ret
114
+
115
+ def reduced_to_full(phi_cm, parity, targetPre):
116
+ """Convert reduced phase factors to full phase factors for QSP.
117
+
118
+ This function constructs the full set of phase factors required for the
119
+ Quantum Signal Processing (QSP) unitary matrix from a reduced set. The
120
+ conversion uses symmetry properties of the phase factors and handles
121
+ both even and odd parity cases.
122
+
123
+ Parameters
124
+ ----------
125
+ phi_cm : array_like
126
+ Reduced phase factors. For even parity, these represent half the
127
+ total phase factors; for odd parity, they represent the unique
128
+ phase factors.
129
+ parity : int
130
+ Parity of the phase factors:
131
+
132
+ - 0 : even parity (full length = 2*len(phi_cm) - 1)
133
+ - 1 : odd parity (full length = 2*len(phi_cm))
134
+ targetPre : bool
135
+ Whether to adjust for target preparation:
136
+
137
+ - True : add π/4 to the last phase factor
138
+ - False : use phase factors as is
139
+
140
+ Returns
141
+ -------
142
+ ndarray
143
+ Full phase factors constructed by mirroring the reduced factors.
144
+ The length depends on parity:
145
+
146
+ - For even parity: 2*len(phi_cm) - 1
147
+ - For odd parity: 2*len(phi_cm)
148
+
149
+ Notes
150
+ -----
151
+ The full phase factors are constructed by:
152
+ 1. Copying the reduced factors to the right half
153
+ 2. Mirroring them to the left half
154
+ 3. Adjusting the last factor if targetPre is True
155
+ """
156
+ phi_right = phi_cm.copy()
157
+ if targetPre:
158
+ phi_right[-1] += np.pi / 4
159
+
160
+ dd = 2 * len(phi_right)
161
+ if parity == 0:
162
+ dd -= 1
163
+
164
+ phi_full = np.zeros(dd)
165
+ phi_full[(dd - len(phi_right)):] = phi_right
166
+ phi_full[:len(phi_right)] += phi_right[::-1]
167
+
168
+ return phi_full
169
+
170
+ def F(phi, parity, opts):
171
+ """Compute the Chebyshev coefficients of P_im.
172
+
173
+ P_im is the imaginary part of the (1,1) element of the QSP unitary matrix.
174
+
175
+ Parameters
176
+ ----------
177
+ phi : array_like
178
+ Reduced phase factors
179
+ parity : int
180
+ Parity of phi (0 for even, 1 for odd)
181
+ opts : dict
182
+ Options dictionary with fields:
183
+ - useReal : bool
184
+ Whether to use real matrix multiplication
185
+
186
+ Returns
187
+ -------
188
+ ndarray
189
+ Chebyshev coefficients of P_im w.r.t.
190
+ T_(2k) for even parity or T_(2k-1) for odd parity
191
+ """
192
+ # Setup options for CM solver
193
+ opts.setdefault('useReal', True)
194
+
195
+ # Initial preparation
196
+ d = len(phi)
197
+ dd = 2 * d
198
+ theta = np.arange(d + 1) * np.pi / dd
199
+ M = np.zeros(2 * dd)
200
+
201
+ if opts['useReal']:
202
+ f = lambda x: [get_pim_sym_real(phi, xval, parity) for xval in x]
203
+ else:
204
+ f = lambda x: [get_pim_sym(phi, xval, parity) for xval in x]
205
+
206
+ # Start Chebyshev coefficients evaluation
207
+ M[:d+1] = f(np.cos(theta))
208
+ M[d+1:dd+1] = (-1)**parity * M[d-1::-1]
209
+ M[dd+1:] = M[dd-1:0:-1]
210
+ M = np.fft.fft(M) # FFT w.r.t. columns.
211
+ M = np.real(M)
212
+ M /= (2 * dd)
213
+ M[1:-1] *= 2
214
+ coe = M[parity:2*d:2]
215
+
216
+ return coe
217
+
218
+ def F_Jacobian(phi, parity, opts):
219
+ """Compute the Jacobian matrix of Chebyshev coefficients.
220
+
221
+ Parameters
222
+ ----------
223
+ phi : array_like
224
+ Reduced phase factors
225
+ parity : int
226
+ Parity of phi (0 for even, 1 for odd)
227
+ opts : dict
228
+ Options dictionary with fields:
229
+ - useReal : bool
230
+ Whether to use real matrix multiplication
231
+
232
+ Returns
233
+ -------
234
+ ndarray
235
+ Jacobian matrix of Chebyshev coefficients
236
+ """
237
+ # Setup options
238
+ opts.setdefault('useReal', True)
239
+
240
+ # Initial preparation
241
+ if opts['useReal']:
242
+ f = lambda x: get_pim_deri_sym_real(phi, x, parity)
243
+ else:
244
+ f = lambda x: get_pim_deri_sym(phi, x, parity)
245
+
246
+ d = len(phi)
247
+ dd = 2 * d
248
+ theta = np.arange(d + 1) * np.pi / dd
249
+ M = np.zeros((2 * dd, d + 1))
250
+
251
+ for n in range(d + 1):
252
+ M[n, :] = f(np.cos(theta[n]))
253
+
254
+ M[d + 1:dd + 1, :] = (-1) ** parity * M[d - 1::-1, :]
255
+ M[dd + 1:, :] = M[dd - 1:0:-1, :]
256
+
257
+ M = np.fft.fft(M, axis=0) # FFT w.r.t. columns.
258
+ M = np.real(M[:dd + 1, :])
259
+ M[1:-1, :] *= 2
260
+ M /= (2 * dd)
261
+
262
+ f = M[parity::2, -1][:d]
263
+ df = M[parity::2, :-1][:d]
264
+
265
+ return f, df
@@ -0,0 +1,140 @@
1
+ """Objective and gradient functions for QSP optimization.
2
+
3
+ This module provides functions for computing objective values and gradients
4
+ needed in QSP optimization problems.
5
+ """
6
+
7
+ import numpy as np
8
+ from .utils import get_unitary_sym, get_pim_sym, get_pim_sym_real, get_pim_deri_sym, get_pim_deri_sym_real
9
+ from .core import get_entry
10
+
11
+ def obj_sym(phi, delta, opts):
12
+ """Compute objective function value for QSP optimization.
13
+
14
+ Parameters
15
+ ----------
16
+ phi : array_like
17
+ Phase factors for QSP circuit
18
+ delta : array_like
19
+ Samples
20
+ opts : dict
21
+ Options dictionary containing target function and parameters
22
+
23
+ Returns
24
+ -------
25
+ float
26
+ Objective function value
27
+ """
28
+ m = len(delta)
29
+ obj = np.zeros(m)
30
+ for i in range(m):
31
+ qspmat = get_unitary_sym(phi, delta[i], opts['parity'])
32
+ obj[i] = 0.5 * (np.real(qspmat[0, 0]) - opts['target']([delta[i]]))**2
33
+
34
+ return obj
35
+
36
+ def grad_sym(phi, delta, opts):
37
+ """Compute gradient of objective function.
38
+
39
+ Parameters
40
+ ----------
41
+ phi : array_like
42
+ Phase factors for QSP circuit
43
+ delta : array_like
44
+ Samples
45
+ opts : dict
46
+ Options dictionary containing target function and parameters
47
+
48
+ Returns
49
+ -------
50
+ grad : ndarray
51
+ Gradient of objective function
52
+ obj : ndarray
53
+ Objective function value
54
+ """
55
+ # Initial computation
56
+ m = len(delta)
57
+ d = len(phi)
58
+ obj = np.zeros(m)
59
+ grad = np.zeros((m, d))
60
+ gate = np.array([[np.exp(1j * np.pi / 4), 0], [0, np.conj(np.exp(1j * np.pi / 4))]])
61
+ exptheta = np.exp(1j * phi)
62
+ targetx = opts['target']
63
+ parity = opts['parity']
64
+
65
+ # Start gradient evaluation
66
+ for i in range(m):
67
+ x = delta[i]
68
+ Wx = np.array([[x, 1j * np.sqrt(1 - x**2)], [1j * np.sqrt(1 - x**2), x]])
69
+ tmp_save1 = np.zeros((2, 2, d), dtype=complex)
70
+ tmp_save2 = np.zeros((2, 2, d), dtype=complex)
71
+ tmp_save1[:, :, 0] = np.eye(2)
72
+ tmp_save2[:, :, 0] = np.dot(np.array([[exptheta[d-1], 0], [0, np.conj(exptheta[d-1])]]), gate)
73
+ for j in range(1, d):
74
+ tmp_save1[:, :, j] = np.dot(tmp_save1[:, :, j-1], np.dot(np.diag([exptheta[j-1], np.conj(exptheta[j-1])]), Wx))
75
+ tmp_save2[:, :, j] = np.dot(np.dot(np.array([[exptheta[d-j-1], 0], [0, np.conj(exptheta[d-j-1])]]), Wx), tmp_save2[:, :, j-1])
76
+ if parity == 1:
77
+ qspmat = np.dot(np.dot(tmp_save2[:, :, d-1].T, Wx), tmp_save2[:, :, d-1])
78
+ gap = np.real(qspmat[0, 0]) - targetx(x)
79
+ leftmat = np.dot(tmp_save2[:, :, d-1].T, Wx)
80
+ for j in range(d):
81
+ grad_tmp = np.dot(np.dot(leftmat, tmp_save1[:, :, j]), np.array([[1j, -1j]]).T) * tmp_save2[:, :, d-j-1]
82
+ grad[i, j] = 2 * np.real(grad_tmp[0, 0]) * gap
83
+ obj[i] = 0.5 * (np.real(qspmat[0, 0]) - targetx(x))**2
84
+ else:
85
+ qspmat = np.dot(np.dot(tmp_save2[:, :, d-2].T, Wx), tmp_save2[:, :, d-1])
86
+ gap = np.real(qspmat[0, 0]) - targetx(x)
87
+ leftmat = np.dot(tmp_save2[:, :, d-2].T, Wx)
88
+ for j in range(d):
89
+ grad_tmp = np.dot(np.dot(leftmat, tmp_save1[:, :, j]), np.array([[1j, -1j]]).T) * tmp_save2[:, :, d-j-1]
90
+ grad[i, j] = 2 * np.real(grad_tmp[0, 0]) * gap
91
+ grad[i, 0] /= 2
92
+ obj[i] = 0.5 * (np.real(qspmat[0, 0]) - targetx(x))**2
93
+
94
+ return grad, obj
95
+
96
+ def grad_sym_real(phi, delta, opts):
97
+ """Compute gradient using real arithmetic.
98
+
99
+ Similar to grad_sym but uses only real arithmetic for efficiency.
100
+
101
+ Parameters
102
+ ----------
103
+ phi : array_like
104
+ Phase factors for QSP circuit
105
+ delta : array_like
106
+ Samples
107
+ opts : dict
108
+ Options dictionary containing target function and parameters
109
+
110
+ Returns
111
+ -------
112
+ grad : ndarray
113
+ Gradient of objective function
114
+ obj : ndarray
115
+ Objective function value
116
+ """
117
+ # Initial computation
118
+ m = len(delta)
119
+ d = len(phi)
120
+ obj = np.zeros(m)
121
+ grad = np.zeros((m, d))
122
+ targetx = opts['target']
123
+ parity = opts['parity']
124
+
125
+ # Convert the phase factor used in LBFGS solver to reduced phase factors
126
+ if parity == 0:
127
+ phi[0] = phi[0] / 2
128
+
129
+ # Start gradient evaluation
130
+ for i in range(m):
131
+ x = delta[i]
132
+ y = get_pim_deri_sym_real(phi, x, parity)
133
+ if parity == 0:
134
+ y[0] = y[0] / 2
135
+ y = -y # Flip the sign
136
+ gap = y[-1] - targetx([x])
137
+ obj[i] = 0.5 * gap**2
138
+ grad[i, :] = y[:-1] * gap
139
+
140
+ return grad, obj