def param_shift(weights, s_param): unit_v = np.zeros_like(weights) gradient_ = np.zeros_like(weights) for ii in np.ndenumerate(unit_v): ii = ii[0] unit_v[ii] = 1 gradient_[ii] = (circuit(weights + s_param_g * unit_v) - circuit(weights - s_param_g * unit_v)) / ( 2 * np.sin(s_param_g)) unit_v[ii] = 0 return gradient_
def parameter_shift(weights): """Compute the gradient of the variational circuit given by the ansatz function using the parameter-shift rule. Write your code below between the # QHACK # markers—create a device with the correct number of qubits, create a QNode that applies the above ansatz, and compute the gradient of the provided ansatz using the parameter-shift rule. Args: weights (array): An array of floating-point numbers with size (2, 3). Returns: array: The gradient of the variational circuit. The shape should match the input weights array. """ dev = qml.device("default.qubit", wires=3) @qml.qnode(dev) def circuit(weights): for i in range(len(weights)): qml.RX(weights[i, 0], wires=0) qml.RY(weights[i, 1], wires=1) qml.RZ(weights[i, 2], wires=2) qml.CNOT(wires=[0, 1]) qml.CNOT(wires=[1, 2]) qml.CNOT(wires=[2, 0]) return qml.expval(qml.PauliY(0) @ qml.PauliZ(2)) gradient = np.zeros_like(weights) # QHACK # # def parameter_shift_term(qnode, params, i, j): s = 0.1 shifted = params.copy() shifted_back = params.copy() shifted[i][j] += s forward = qnode(shifted) shifted_back[i][j] -= s backward = qnode(shifted_back) return (forward - backward) / (2 * np.sin(s)) def parameter_shift_all(qnode, params, gradient): for i in range(len(params[0]) - 1): for j in range(len(params[1])): gradient[i][j] = parameter_shift_term(qnode, params, i, j) return gradient gradient = parameter_shift_all(circuit, weights, gradient) # QHACK # return gradient
def parameter_shift(weights): """Compute the gradient of the variational circuit given by the ansatz function using the parameter-shift rule. Write your code below between the # QHACK # markers—create a device with the correct number of qubits, create a QNode that applies the above ansatz, and compute the gradient of the provided ansatz using the parameter-shift rule. Args: weights (array): An array of floating-point numbers with size (2, 3). Returns: array: The gradient of the variational circuit. The shape should match the input weights array. """ dev = qml.device("default.qubit", wires=3) @qml.qnode(dev) def circuit(weights): for i in range(len(weights)): qml.RX(weights[i, 0], wires=0) qml.RY(weights[i, 1], wires=1) qml.RZ(weights[i, 2], wires=2) qml.CNOT(wires=[0, 1]) qml.CNOT(wires=[1, 2]) qml.CNOT(wires=[2, 0]) return qml.expval(qml.PauliY(0) @ qml.PauliZ(2)) gradient = np.zeros_like(weights) # QHACK # gradient = qml.grad(circuit) # QHACK # return gradient(weights)
def parameter_shift(qnode, params): gradients = np.zeros_like(weights) for i in range(params.shape[0]): for j in range(params.shape[1]): gradients[i][j] = parameter_shift_term(qnode, params, i, j) return gradients
def parameter_shift(weights): """Compute the gradient of the variational circuit given by the ansatz function using the parameter-shift rule. Write your code below between the # QHACK # markers—create a device with the correct number of qubits, create a QNode that applies the above ansatz, and compute the gradient of the provided ansatz using the parameter-shift rule. Args: weights (array): An array of floating-point numbers with size (2, 3). Returns: array: The gradient of the variational circuit. The shape should match the input weights array. """ dev = qml.device("default.qubit", wires=3) @qml.qnode(dev) def circuit(weights): for i in range(len(weights)): qml.RX(weights[i, 0], wires=0) qml.RY(weights[i, 1], wires=1) qml.RZ(weights[i, 2], wires=2) qml.CNOT(wires=[0, 1]) qml.CNOT(wires=[1, 2]) qml.CNOT(wires=[2, 0]) return qml.expval(qml.PauliY(0) @ qml.PauliZ(2)) gradient = np.zeros_like(weights) # QHACK # # grad_func = qml.grad(circuit) # gradient = grad_func(weights)[0] def parameter_shift_term(qnode, params, i, shift): shifted = params.copy() shifted[np.unravel_index(i, shifted.shape)] += shift #np.pi / 2 forward = qnode(shifted) # forward evaluation shifted[np.unravel_index(i, shifted.shape)] -= 2 * shift #np.pi backward = qnode(shifted) # backward evaluation return 0.5 * (forward - backward) / np.sin(shift) def parameter_shift(qnode, params, shift): gradients = np.zeros_like(params) for i in range(len(gradients.flatten())): gradients[np.unravel_index( i, gradients.shape)] = parameter_shift_term( qnode, params, i, shift) return gradients gradient = parameter_shift(circuit, weights, shift=0.5 * np.pi) # QHACK # return gradient
def parameter_shift(qnode, weights): gradients = np.zeros_like(weights) print(len(weights)) for i in range(weights.shape[0]): for j in range(weights.shape[1]): gradients[i, j] = parameter_shift_term(qnode, weights, i, j) return gradients
def get_grads(qnode, params): gradients = np.zeros_like(params) for i in range(30): gradients[i] = parameter_shift_term(qnode, params, i) return gradients
def compute_grad( self, objective_fn, args, kwargs ): # pylint: disable=signature-differs,arguments-differ r"""Compute gradient of the objective function, as well as the variance of the gradient, at the given point. Args: objective_fn (function): the objective function for optimization args: arguments to the objective function kwargs: keyword arguments to the objective function Returns: tuple[array[float], array[float]]: a tuple of NumPy arrays containing the gradient :math:`\nabla f(x^{(t)})` and the variance of the gradient """ if isinstance(objective_fn, qml.ExpvalCost): grads = self._single_shot_expval_gradients(objective_fn, args, kwargs) elif isinstance(objective_fn, qml.QNode) or hasattr(objective_fn, "device"): grads = self._single_shot_qnode_gradients(objective_fn, args, kwargs) else: raise ValueError( "The objective function must either be encoded as a single QNode or " "an ExpvalCost object for the Shot adaptive optimizer. " ) # grads will have dimension [max(self.s), *params.shape] # For each parameter, we want to truncate the number of shots to self.s[idx], # and take the mean and the variance. gradients = [] gradient_variances = [] for i, grad in enumerate(grads): p_ind = np.ndindex(*grad.shape[1:]) g = np.zeros_like(grad[0]) s = np.zeros_like(grad[0]) for idx in p_ind: grad_slice = grad[(slice(0, self.s[i][idx]),) + idx] g[idx] = np.mean(grad_slice) s[idx] = np.var(grad_slice, ddof=1) gradients.append(g) gradient_variances.append(s) return gradients, gradient_variances
def parameter_shift(qnode, params, shift): gradients = np.zeros_like(params) for i in range(len(gradients.flatten())): gradients[np.unravel_index( i, gradients.shape)] = parameter_shift_term( qnode, params, i, shift) return gradients
def parameter_shiftz(weight): gradient = np.zeros_like(weight) for i in range(len(weight)): gradient[i][0] = parameter_shift_grad(weight, i, 0) gradient[i][1] = parameter_shift_grad(weight, i, 1) gradient[i][2] = parameter_shift_grad(weight, i, 2) return gradient
def get_grads(qnode, params): gradients = np.zeros_like(params) for i in range(2): for j in range(3): gradients[i][j] = parameter_shift_term(qnode, params, i, j) return gradients
def test_Rot_gradient(self, mocker, theta, shift, tol): """Tests that the automatic gradient of an arbitrary Euler-angle-parameterized gate is correct.""" spy = mocker.spy(qml.gradients.parameter_shift, "_get_operation_recipe") dev = qml.device("default.qubit", wires=1) params = np.array([theta, theta**3, np.sqrt(2) * theta]) with qml.tape.JacobianTape() as tape: qml.QubitStateVector(np.array([1.0, -1.0], requires_grad=False) / np.sqrt(2), wires=0) qml.Rot(*params, wires=[0]) qml.expval(qml.PauliZ(0)) tape.trainable_params = {1, 2, 3} tapes, fn = qml.gradients.param_shift(tape, shift=shift) assert len(tapes) == 2 * len(tape.trainable_params) autograd_val = fn(dev.batch_execute(tapes)) manualgrad_val = np.zeros_like(autograd_val) for idx in list(np.ndindex(*params.shape)): s = np.zeros_like(params) s[idx] += np.pi / 2 forward = tape.execute(dev, params=params + s) backward = tape.execute(dev, params=params - s) manualgrad_val[0, idx] = (forward - backward) / 2 assert np.allclose(autograd_val, manualgrad_val, atol=tol, rtol=0) assert spy.call_args[1]["shift"] == shift # compare to finite differences tapes, fn = qml.gradients.finite_diff(tape) numeric_val = fn(dev.batch_execute(tapes)) assert np.allclose(autograd_val, numeric_val, atol=tol, rtol=0)
def parameter_shift(weights): """Compute the gradient of the variational circuit given by the ansatz function using the parameter-shift rule. Write your code below between the # QHACK # markers—create a device with the correct number of qubits, create a QNode that applies the above ansatz, and compute the gradient of the provided ansatz using the parameter-shift rule. Args: weights (array): An array of floating-point numbers with size (2, 3). Returns: array: The gradient of the variational circuit. The shape should match the input weights array. """ dev = qml.device("default.qubit", wires=3) def parameter_shift_term(qnode, params, t,i): shifted = params.copy() shifted[t,i] += np.pi/2 forward = qnode(shifted) # forward evaluation shifted[t,i] -= np.pi backward = qnode(shifted) # backward evaluation return 0.5 * (forward - backward) @qml.qnode(dev) def circuit(weights): for i in range(len(weights)): qml.RX(weights[i, 0], wires=0) qml.RY(weights[i, 1], wires=1) qml.RZ(weights[i, 2], wires=2) qml.CNOT(wires=[0, 1]) qml.CNOT(wires=[1, 2]) qml.CNOT(wires=[2, 0]) return qml.expval(qml.PauliY(0) @ qml.PauliZ(2)) c_test, c_input = weights.shape gradient = np.zeros_like(weights) for t in range(c_test): for a in range(c_input): gradient[t,a] = parameter_shift_term(circuit, weights, t,a) # QHACK # # # QHACK # return gradient
def plot_decision_boundaries(classifier, ax, N_gridpoints=14): _xx, _yy = np.meshgrid(np.linspace(-1, 1, N_gridpoints), np.linspace(-1, 1, N_gridpoints)) _zz = np.zeros_like(_xx) for idx in np.ndindex(*_xx.shape): _zz[idx] = classifier.predict( np.array([_xx[idx], _yy[idx]])[np.newaxis, :]) plot_data = {'_xx': _xx, '_yy': _yy, '_zz': _zz} ax.contourf(_xx, _yy, _zz, cmap=mpl.colors.ListedColormap(['#FF0000', '#0000FF']), alpha=.2, levels=[-1, 0, 1]) dataset.plot(ax) return plot_data
def term(params, sign_term, sign_i, i, sign_j, j): qml.BasisState(np.array([0, 0, 0, 0, 0, 0]), wires=range(6)) variational_circuit_wire_list(params) shift = np.zeros_like(params) shift[i] = sign_i * one shift[j] = sign_j * one variational_circuit_wire_list(params + shift, wire_list=[3, 4, 5]) # return qml.DiagonalQubitUnitary([1] * 6, wires=[0, 1, 2]), qml.DiagonalQubitUnitary([1] * 6, wires=[3, 4, 5]) # return [ # [qml.expval(qml.PauliX(wire)) for wire in [0, 1, 2]], # [qml.expval(qml.PauliX(wire)) for wire in [3, 4, 5]], # ] return sign_term * np.abs( qml.dot( [qml.expval(qml.PauliX(wire)) for wire in [0, 1, 2]], [qml.expval(qml.PauliX(wire)) for wire in [3, 4, 5]], ))**2
def CFIM(weights, phi, gamma): p = experiment(weights, phi, gamma=gamma) dp = [] for idx in range(3): # We use the parameter-shift rule explicitly # to compute the derivatives shift = np.zeros_like(phi) shift[idx] = np.pi / 2 plus = experiment(weights, phi + shift, gamma=gamma) minus = experiment(weights, phi - shift, gamma=gamma) dp.append(0.5 * (plus - minus)) matrix = [0] * 9 for i in range(3): for j in range(3): matrix[3 * i + j] = np.sum(dp[i] * dp[j] / p) return np.array(matrix).reshape((3, 3))
def plot_decision_boundaries(classifier, ax, N_gridpoints=14): _xx, _yy = np.meshgrid(np.linspace(-1, 1, N_gridpoints), np.linspace(-1, 1, N_gridpoints)) _zz = np.zeros_like(_xx) for idx in np.ndindex(*_xx.shape): _zz[idx] = classifier.predict( np.array([_xx[idx], _yy[idx]])[np.newaxis, :]) plot_data = {"_xx": _xx, "_yy": _yy, "_zz": _zz} ax.contourf( _xx, _yy, _zz, cmap=mpl.colors.ListedColormap(["#FF0000", "#0000FF"]), alpha=0.2, levels=[-1, 0, 1], ) plot_double_cake_data(X, Y, ax) return plot_data
for i in range(20): weights, cost_ = opt.step_and_cost(opt_cost, weights) if (i + 1) % 5 == 0: print("Iteration {:>4}: Cost = {:6.4f}".format(i + 1, cost_)) ############################################################################## # Comparison with the standard protocol # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # Now we want to see how our protocol compares to the standard Ramsey interferometry protocol. # The probe state in this case is a tensor product of three separate :math:`|+\rangle` states # while the encoded state is measured in the :math:`|+\rangle / |-\rangle` basis. # We can recreate the standard schemes with specific weights for our setup. Ramsey_weights = np.zeros_like(weights) Ramsey_weights[1:6:2] = np.pi / 2 Ramsey_weights[15:20:2] = np.pi / 2 print("Cost for standard Ramsey sensing = {:6.4f}".format( opt_cost(Ramsey_weights))) ############################################################################## # We can now make a plot to compare the noise scaling of the above probes. gammas = np.linspace(0, 0.75, 21) comparison_costs = { "optimized": [], "standard": [], } for gamma in gammas: comparison_costs["optimized"].append(cost(weights, phi, gamma, J, W))
C1 = [circuit(np.array([theta, .5])) for theta in theta_func] C2 = [circuit(np.array([3.3, theta])) for theta in theta_func] # Show the sweeps. fig, ax = plt.subplots(1, 1, figsize=(4, 3)) ax.plot(theta_func, C1, label="$E(\\theta, 0.5)$", color="r") ax.plot(theta_func, C2, label="$E(3.3, \\theta)$", color="orange") ax.set_xlabel("$\\theta$") ax.set_ylabel("$E$") ax.legend() plt.tight_layout() # Create a 2D grid and evaluate the energy on the grid points. # We cut out a part of the landscape to increase clarity. X, Y = np.meshgrid(theta_func, theta_func) Z = np.zeros_like(X) for i, t1 in enumerate(theta_func): for j, t2 in enumerate(theta_func): # Cut out the viewer-facing corner if (2 * np.pi - t2)**2 + t1**2 > 4: Z[i, j] = circuit([t1, t2]) else: X[i, j] = np.nan Y[i, j] = np.nan Z[i, j] = np.nan # Show the energy landscape on the grid. fig, ax = plt.subplots(1, 1, subplot_kw={"projection": "3d"}, figsize=(4, 4)) surf = ax.plot_surface(X, Y, Z,
def Words_Entangler_circuit(params_, wires): """Apply many controled-rotation between words 'params_' is a matrix of different angles of size (num_words/2) x qbits_per_word """ mask = np.zeros_like(params_) #EVEN number of qubits if num_words % 2 == 0: for bit in range(qbits_per_word): for i in range(int(np.ceil(num_words / 2))): if bit % 2 != 0: i_ = i * qbits_per_word * 2 + qbits_per_word if i_ + bit + qbits_per_word > len(wires) - 1: qml.CRY(params_[bit][i], wires=[wires[i_ + bit], wires[bit]]) mask[bit][i] = 1 else: qml.CRY(params_[bit][i], wires=[ wires[i_ + bit], wires[i_ + bit + qbits_per_word] ]) mask[bit][i] = 1 else: i_ = i * qbits_per_word * 2 qml.CRY(params_[bit][i], wires=[ wires[i_ + bit], wires[i_ + bit + qbits_per_word] ]) mask[bit][i] = 1 #ODD number of qubits elif num_words % 2 == 1: for bit in range(qbits_per_word): if bit % 2 == 0: #even bits: need to loop back the last CRot for i in range(int(np.ceil(num_words / 2))): i_ = i * qbits_per_word * 2 if i_ + bit + qbits_per_word > len(wires) - 1: if bit == qbits_per_word - 1: #last layer qml.CRY(params_[bit][i], wires=[wires[i_ + bit], wires[bit]]) mask[bit][i] = 1 else: qml.CRY(params_[bit][i], wires=[wires[i_ + bit], wires[bit + 1]]) mask[bit][i] = 1 else: qml.CRY(params_[bit][i], wires=[ wires[i_ + bit], wires[i_ + bit + qbits_per_word] ]) mask[bit][i] = 1 else: #odd bits for i in range(int(np.floor(num_words / 2))): i_ = i * qbits_per_word * 2 + qbits_per_word qml.CRY(params_[bit][i], wires=[ wires[i_ + bit], wires[i_ + bit + qbits_per_word] ]) mask[bit][i] = 1
def clear(self) -> None: """Resets the mask to not mask anything.""" self.mask = np.zeros_like(self.mask, dtype=self.mask.dtype, requires_grad=False)
def natural_gradient(params): """Calculate the natural gradient of the qnode() cost function. The code you write for this challenge should be completely contained within this function between the # QHACK # comment markers. You should evaluate the metric tensor and the gradient of the QNode, and then combine these together using the natural gradient definition. The natural gradient should be returned as a NumPy array. The metric tensor should be evaluated using the equation provided in the problem text. Hint: you will need to define a new QNode that returns the quantum state before measurement. Args: params (np.ndarray): Input parameters, of dimension 6 Returns: np.ndarray: The natural gradient evaluated at the input parameters, of dimension 6 """ natural_grad = np.zeros(6) # QHACK # def variational_circuit_wire_list(params, wire_list=None): """A layered variational circuit composed of two parametrized layers of single qubit rotations interleaved with non-parameterized layers of fixed quantum gates specified by ``non_parametrized_layer``. The first parametrized layer uses the first three parameters of ``params``, while the second layer uses the final three parameters. """ if wire_list is None: wire_list = [0, 1, 2] non_parametrized_layer() qml.RX(params[0], wires=wire_list[0]) qml.RY(params[1], wires=wire_list[1]) qml.RZ(params[2], wires=wire_list[2]) non_parametrized_layer() qml.RX(params[3], wires=wire_list[0]) qml.RY(params[4], wires=wire_list[1]) qml.RZ(params[5], wires=wire_list[2]) # FS metric TODO F = np.zeros([6, 6], dtype=np.float64) one = np.pi / 2 dev_F = qml.device("default.qubit", wires=2 * n_wires) @qml.qnode(dev_F) def term(params, sign_term, sign_i, i, sign_j, j): qml.BasisState(np.array([0, 0, 0, 0, 0, 0]), wires=range(6)) variational_circuit_wire_list(params) shift = np.zeros_like(params) shift[i] = sign_i * one shift[j] = sign_j * one variational_circuit_wire_list(params + shift, wire_list=[3, 4, 5]) # return qml.DiagonalQubitUnitary([1] * 6, wires=[0, 1, 2]), qml.DiagonalQubitUnitary([1] * 6, wires=[3, 4, 5]) # return [ # [qml.expval(qml.PauliX(wire)) for wire in [0, 1, 2]], # [qml.expval(qml.PauliX(wire)) for wire in [3, 4, 5]], # ] return sign_term * np.abs( qml.dot( [qml.expval(qml.PauliX(wire)) for wire in [0, 1, 2]], [qml.expval(qml.PauliX(wire)) for wire in [3, 4, 5]], ))**2 for i in range(len(params)): for j in range(len(params)): print(term(params, -1, 1, i, 1, j)) # F[i, j] += term(params, -1, 1, i, 1, j) # F[i, j] += term(params, 1, 1, i, -1, j) # F[i, j] += term(params, 1, -1, i, 1, j) # F[i, j] += term(params, -1, -1, i, -1, j) F /= 8 F_inverse = np.linalg.inv(F) # gradient gradient = np.zeros_like(params, dtype=np.float64) shift_mask = np.zeros_like(params) s = 1 denominator = 2 * np.sin(s) for i in range(len(params)): shift_mask[i] = s gradient[i] = (qnode(params + shift_mask) - qnode(params - shift_mask)) / denominator shift_mask[i] = 0 # from https://pennylane.ai/qml/demos/tutorial_quantum_natural_gradient.html # print(np.round(qml.metric_tensor(circuit)(params), 8)) np.set_printoptions(formatter={'float': lambda x: "{0:0.5f}".format(x)}) print(f"gradient: {gradient}") natural_grad = F_inverse * gradient # QHACK # return natural_grad
def parameter_shift(weights): """Compute the gradient of the variational circuit given by the ansatz function using the parameter-shift rule. Write your code below between the # QHACK # markers—create a device with the correct number of qubits, create a QNode that applies the above ansatz, and compute the gradient of the provided ansatz using the parameter-shift rule. Args: weights (array): An array of floating-point numbers with size (2, 3). Returns: array: The gradient of the variational circuit. The shape should match the input weights array. """ dev = qml.device("default.qubit", wires=3) @qml.qnode(dev, diff_method="parameter-shift") def circuit(weights): # for i in range(len(weights)): # qml.RX(weights[i, 0], wires=0) # qml.RY(weights[i, 1], wires=1) # qml.RZ(weights[i, 2], wires=2) # # qml.CNOT(wires=[0, 1]) # qml.CNOT(wires=[1, 2]) # qml.CNOT(wires=[2, 0]) qml.RX(weights[0], wires=0) qml.RY(weights[1], wires=1) qml.RZ(weights[2], wires=2) qml.CNOT(wires=[0, 1]) qml.CNOT(wires=[1, 2]) qml.CNOT(wires=[2, 0]) qml.RX(weights[3], wires=0) qml.RY(weights[4], wires=1) qml.RZ(weights[5], wires=2) qml.CNOT(wires=[0, 1]) qml.CNOT(wires=[1, 2]) qml.CNOT(wires=[2, 0]) return qml.expval(qml.PauliY(0) @ qml.PauliZ(2)) gradient = np.zeros_like(weights) # QHACK # def apply_parameter_shift(qnode, params, i): shifted = params.copy() shifted[i] += np.pi / 2 forward = qnode(shifted) # forward evaluation shifted[i] -= np.pi backward = qnode(shifted) # backward evaluation return 0.5 * (forward - backward) params = [] for weight_arr in weights: for val in weight_arr: params.append(float(val)) gradient = [ apply_parameter_shift(circuit, params, i) for i in range(len(params)) ] # QHACK # return gradient
def step(self, objective_fn, *args, **kwargs): """Update trainable arguments with one step of the optimizer. Args: objective_fn (function): the objective function for optimization *args: variable length argument list for objective function **kwargs: variable length of keyword arguments for the objective function Returns: list[array]: The new variable values :math:`x^{(t+1)}`. If single arg is provided, list[array] is replaced by array. """ self.trainable_args = set() for index, arg in enumerate(args): if getattr(arg, "requires_grad", True): self.trainable_args |= {index} if self.s is None: # Number of shots per parameter self.s = [ np.zeros_like(a, dtype=np.int64) + self.min_shots for i, a in enumerate(args) if i in self.trainable_args ] # keep track of the number of shots run s = np.concatenate([i.flatten() for i in self.s]) self.max_shots = max(s) self.shots_used = int(2 * np.sum(s)) self.total_shots_used += self.shots_used # compute the gradient, as well as the variance in the gradient, # using the number of shots determined by the array s. grads, grad_variances = self.compute_grad(objective_fn, args, kwargs) new_args = self.apply_grad(grads, args) if self.xi is None: self.chi = [np.zeros_like(g, dtype=np.float64) for g in grads] self.xi = [np.zeros_like(g, dtype=np.float64) for g in grads] # running average of the gradient self.chi = [self.mu * c + (1 - self.mu) * g for c, g in zip(self.chi, grads)] # running average of the gradient variance self.xi = [self.mu * x + (1 - self.mu) * v for x, v in zip(self.xi, grad_variances)] for idx, (c, x) in enumerate(zip(self.chi, self.xi)): xi = x / (1 - self.mu ** (self.k + 1)) chi = c / (1 - self.mu ** (self.k + 1)) # determine the new optimum shots distribution for the next # iteration of the optimizer s = np.ceil( (2 * self.lipschitz * self.stepsize * xi) / ((2 - self.lipschitz * self.stepsize) * (chi ** 2 + self.b * (self.mu ** self.k))) ) # apply an upper and lower bound on the new shot distributions, # to avoid the number of shots reducing below min(2, min_shots), # or growing too significantly. gamma = ( (self.stepsize - self.lipschitz * self.stepsize ** 2 / 2) * chi ** 2 - xi * self.lipschitz * self.stepsize ** 2 / (2 * s) ) / s argmax_gamma = np.unravel_index(np.argmax(gamma), gamma.shape) smax = max(s[argmax_gamma], 2) self.s[idx] = np.squeeze(np.int64(np.clip(s, min(2, self.min_shots), smax))) self.k += 1 # unwrap from list if one argument, cleaner return if len(new_args) == 1: return new_args[0] return new_args