def test_piecewise_linear_function(self, num_state_qubits, breakpoints, slopes, offsets): """Test the piecewise linear rotations.""" def pw_linear(x): for i, point in enumerate(reversed(breakpoints)): if x >= point: return offsets[-(i + 1)] + slopes[-(i + 1)] * (x - point) return 0 pw_linear_rotations = PiecewiseLinearPauliRotations(num_state_qubits, breakpoints, [2 * slope for slope in slopes], [2 * offset for offset in offsets]) self.assertFunctionIsCorrect(pw_linear_rotations, pw_linear)
def build(self, qc, q, q_ancillas=None, params=None): """Build the circuit.""" pwlr = PiecewiseLinearPauliRotations(num_state_qubits=self.num_state_qubits, breakpoints=self.breakpoints, slopes=self.slopes, offsets=self.offsets, basis=self.basis).to_instruction() qr = [q[i] for i in self.i_state] + [q[self.i_target]] if q_ancillas: # pylint:disable=unnecessary-comprehension qr += [qi for qi in q_ancillas[:self.required_ancillas()]] qc.append(pwlr, qr)
def test_piecewise_linear_rotations_mutability(self): """Test the mutability of the linear rotations circuit.""" pw_linear_rotations = PiecewiseLinearPauliRotations() with self.subTest(msg='missing number of state qubits'): with self.assertRaises(AttributeError): # no state qubits set print(pw_linear_rotations.draw()) with self.subTest( msg='default setup, just setting number of state qubits'): pw_linear_rotations.num_state_qubits = 2 self.assertFunctionIsCorrect(pw_linear_rotations, lambda x: x / 2) with self.subTest(msg='setting non-default values'): pw_linear_rotations.breakpoints = [0, 2] pw_linear_rotations.slopes = [-1 * 2, 1 * 2] pw_linear_rotations.offsets = [0, -1.2 * 2] self.assertFunctionIsCorrect( pw_linear_rotations, lambda x: -1.2 + (x - 2) if x >= 2 else -x) with self.subTest(msg='changing all values'): pw_linear_rotations.num_state_qubits = 4 pw_linear_rotations.breakpoints = [1, 3, 6] pw_linear_rotations.slopes = [-1 * 2, 1 * 2, -0.2 * 2] pw_linear_rotations.offsets = [0, -1.2 * 2, 2 * 2] def pw_linear(x): if x >= 6: return 2 - 0.2 * (x - 6) if x >= 3: return -1.2 + (x - 3) if x >= 1: return -(x - 1) return 0 self.assertFunctionIsCorrect(pw_linear_rotations, pw_linear)
def __init__(self, num_state_qubits: int, min_state_value: float, max_state_value: float, breakpoints: Union[List[float], np.ndarray], slopes: Union[List[float], np.ndarray], offsets: Union[List[float], np.ndarray], f_min: float, f_max: float, c_approx: float, i_state: Optional[int] = None, i_objective: Optional[int] = None) -> None: r""" Args: num_state_qubits: number of qubits to represent the state min_state_value : lower bound of values to be represented by state qubits max_state_value: upper bound of values to be represented by state qubits breakpoints: breakpoints of piecewise linear function slopes: slopes of linear segments offsets: offset of linear segments f_min: minimal value of resulting function (required for normalization of amplitude) f_max: maximal value of resulting function (required for normalization of amplitude) c_approx: approximating factor (linear segments are approximated by contracting rotation around pi/4, where sin\^2() is locally linear) i_state: indices of qubits that represent the state i_objective: index of target qubit to apply the rotation to """ super().__init__(num_state_qubits + 1) self.num_state_qubits = num_state_qubits self.min_state_value = min_state_value self.max_state_value = max_state_value # sort breakpoints i_sort = np.argsort(breakpoints) breakpoints = np.array(breakpoints)[i_sort] slopes = np.array(slopes)[i_sort] offsets = np.array(offsets)[i_sort] # drop breakpoints and corresponding values below min_state_value or above max_state_value for i in reversed(range(len(breakpoints))): if breakpoints[i] <= (self.min_state_value - 1e-6) or \ breakpoints[i] >= (self.max_state_value + 1e-6): breakpoints = np.delete(breakpoints, i) slopes = np.delete(slopes, i) offsets = np.delete(offsets, i) # make sure the minimal value is included in the breakpoints min_value_included = False for point in breakpoints: if np.isclose(point, min_state_value): min_value_included = True break if not min_value_included: breakpoints = np.append(min_state_value, breakpoints) slopes = np.append(0, slopes) offsets = np.append(0, offsets) # store parameters self._breakpoints = breakpoints self._slopes = slopes self._offsets = offsets self._f_min = f_min self._f_max = f_max self._c_approx = c_approx # get and store qubit indices self.i_state = None if i_state is not None: self.i_state = i_state else: self.i_state = list(range(num_state_qubits)) self.i_objective = None if i_objective is not None: self.i_objective = i_objective else: self.i_objective = num_state_qubits # map breakpoints, slopes, and offsets such that they fit {0, ..., 2^n-1} lower = min_state_value upper = max_state_value self._mapped_breakpoints = [] self._mapped_slopes = [] self._mapped_offsets = [] for i, point in enumerate(breakpoints): mapped_breakpoint = (point - lower) / (upper - lower) * (2**num_state_qubits - 1) if mapped_breakpoint <= 2**num_state_qubits - 1: self._mapped_breakpoints += [mapped_breakpoint] # factor (upper - lower) / (2^n - 1) is for the scaling of x to [l,u] # note that the +l for mapping to [l,u] is already included in # the offsets given as parameters self._mapped_slopes += [slopes[i] * (upper - lower) / (2**num_state_qubits - 1)] self._mapped_offsets += [offsets[i]] self._mapped_breakpoints = np.array(self._mapped_breakpoints) self._mapped_slopes = np.array(self._mapped_slopes) self._mapped_offsets = np.array(self._mapped_offsets) # approximate linear behavior by scaling and contracting around pi/4 if len(self._mapped_breakpoints): # pylint: disable=len-as-condition self._slope_angles = np.zeros(len(breakpoints)) self._offset_angles = np.pi / 4 * (1 - c_approx) * np.ones(len(breakpoints)) for i in range(len(breakpoints)): self._slope_angles[i] = \ np.pi * c_approx * self._mapped_slopes[i] / 2 / (f_max - f_min) self._offset_angles[i] += \ np.pi * c_approx * (self._mapped_offsets[i] - f_min) / 2 / (f_max - f_min) # multiply by 2 since Y-rotation uses theta/2 as angle self._slope_angles = 2 * self._slope_angles self._offset_angles = 2 * self._offset_angles # create piecewise linear Y rotation self._pwl_ry = PiecewiseLinearPauliRotations( num_state_qubits, self._mapped_breakpoints, self._slope_angles, self._offset_angles ) else: self.offset_angle = 0 self.slope_angle = 0 # create piecewise linear Y rotation self._pwl_ry = None
class UnivariatePiecewiseLinearObjective(CircuitFactory): r"""Univariate Piecewise Linear Objective Function. This objective function applies controlled Y-rotation to the target qubit, where the control qubits represent integer value, and rotation approximates a piecewise linear function of the amplitude f: .. math:: |x\rangle |0\rangle \mapsto |x\rangle (\sqrt(1 - f(x))|0\rangle + sqrt(f(x))|1\rangle ) """ def __init__(self, num_state_qubits: int, min_state_value: float, max_state_value: float, breakpoints: Union[List[float], np.ndarray], slopes: Union[List[float], np.ndarray], offsets: Union[List[float], np.ndarray], f_min: float, f_max: float, c_approx: float, i_state: Optional[int] = None, i_objective: Optional[int] = None) -> None: r""" Args: num_state_qubits: number of qubits to represent the state min_state_value : lower bound of values to be represented by state qubits max_state_value: upper bound of values to be represented by state qubits breakpoints: breakpoints of piecewise linear function slopes: slopes of linear segments offsets: offset of linear segments f_min: minimal value of resulting function (required for normalization of amplitude) f_max: maximal value of resulting function (required for normalization of amplitude) c_approx: approximating factor (linear segments are approximated by contracting rotation around pi/4, where sin\^2() is locally linear) i_state: indices of qubits that represent the state i_objective: index of target qubit to apply the rotation to """ super().__init__(num_state_qubits + 1) self.num_state_qubits = num_state_qubits self.min_state_value = min_state_value self.max_state_value = max_state_value # sort breakpoints i_sort = np.argsort(breakpoints) breakpoints = np.array(breakpoints)[i_sort] slopes = np.array(slopes)[i_sort] offsets = np.array(offsets)[i_sort] # drop breakpoints and corresponding values below min_state_value or above max_state_value for i in reversed(range(len(breakpoints))): if breakpoints[i] <= (self.min_state_value - 1e-6) or \ breakpoints[i] >= (self.max_state_value + 1e-6): breakpoints = np.delete(breakpoints, i) slopes = np.delete(slopes, i) offsets = np.delete(offsets, i) # make sure the minimal value is included in the breakpoints min_value_included = False for point in breakpoints: if np.isclose(point, min_state_value): min_value_included = True break if not min_value_included: breakpoints = np.append(min_state_value, breakpoints) slopes = np.append(0, slopes) offsets = np.append(0, offsets) # store parameters self._breakpoints = breakpoints self._slopes = slopes self._offsets = offsets self._f_min = f_min self._f_max = f_max self._c_approx = c_approx # get and store qubit indices self.i_state = None if i_state is not None: self.i_state = i_state else: self.i_state = list(range(num_state_qubits)) self.i_objective = None if i_objective is not None: self.i_objective = i_objective else: self.i_objective = num_state_qubits # map breakpoints, slopes, and offsets such that they fit {0, ..., 2^n-1} lower = min_state_value upper = max_state_value self._mapped_breakpoints = [] self._mapped_slopes = [] self._mapped_offsets = [] for i, point in enumerate(breakpoints): mapped_breakpoint = (point - lower) / (upper - lower) * (2**num_state_qubits - 1) if mapped_breakpoint <= 2**num_state_qubits - 1: self._mapped_breakpoints += [mapped_breakpoint] # factor (upper - lower) / (2^n - 1) is for the scaling of x to [l,u] # note that the +l for mapping to [l,u] is already included in # the offsets given as parameters self._mapped_slopes += [slopes[i] * (upper - lower) / (2**num_state_qubits - 1)] self._mapped_offsets += [offsets[i]] self._mapped_breakpoints = np.array(self._mapped_breakpoints) self._mapped_slopes = np.array(self._mapped_slopes) self._mapped_offsets = np.array(self._mapped_offsets) # approximate linear behavior by scaling and contracting around pi/4 if len(self._mapped_breakpoints): # pylint: disable=len-as-condition self._slope_angles = np.zeros(len(breakpoints)) self._offset_angles = np.pi / 4 * (1 - c_approx) * np.ones(len(breakpoints)) for i in range(len(breakpoints)): self._slope_angles[i] = \ np.pi * c_approx * self._mapped_slopes[i] / 2 / (f_max - f_min) self._offset_angles[i] += \ np.pi * c_approx * (self._mapped_offsets[i] - f_min) / 2 / (f_max - f_min) # multiply by 2 since Y-rotation uses theta/2 as angle self._slope_angles = 2 * self._slope_angles self._offset_angles = 2 * self._offset_angles # create piecewise linear Y rotation self._pwl_ry = PiecewiseLinearPauliRotations( num_state_qubits, self._mapped_breakpoints, self._slope_angles, self._offset_angles ) else: self.offset_angle = 0 self.slope_angle = 0 # create piecewise linear Y rotation self._pwl_ry = None def value_to_estimation(self, value): """ value to estimation """ if self._c_approx < 1: # map normalized value back to estimation estimator = value - 1 / 2 + np.pi / 4 * self._c_approx estimator *= 2 / np.pi / self._c_approx estimator *= self._f_max - self._f_min estimator += self._f_min return estimator else: return value def required_ancillas(self): """ requires ancillas """ return self._pwl_ry.num_ancilla_qubits def build(self, qc, q, q_ancillas=None, params=None): """ build """ q_state = [q[i] for i in self.i_state] q_objective = q[self.i_objective] # apply piecewise linear rotation qubits = q_state[:] + [q_objective] if q_ancillas: qubits += q_ancillas[:self.required_ancillas()] qc.append(self._pwl_ry.to_instruction(), qubits)