Source code for flamingpy.cv.gkp
# Copyright 2022 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Functions useful for binning and errors associated with GKP encodings."""
import numpy as np
from scipy.special import erf
[docs]def integer_fractional(x, alpha):
"""Obtain the integer and fractional part of x with respect to alpha.
Any real number x can be expressed as n * alpha + f, where n is an
integer, alpha is any real number, and f is a real number such that
\|f\| <= alpha / 2. This function returns n and f.
Args:
x (float or array): real numbers
alpha (float): alpha from above.
"""
int_frac = np.divmod(x, alpha)
large_frac = np.greater(int_frac[1], alpha / 2).astype(int)
f = int_frac[1] - alpha * large_frac
n = int_frac[0].astype(int) + large_frac
return n, f
[docs]def GKP_binner(outcomes, return_fraction=False):
"""Naively translate CV outcomes to bit values.
The function treats values in (-sqrt(pi)/2, sqrt(pi)/2) as 0
and those in (sqrt(pi)/2, 3*sqrt(pi)/2) as 1. Values on the
boundary are binned to 0. The rest of the bins
are defined periodically.
Args:
outcomes (array): the values of a p-homodyne measurement.
return_fraction (bool): also return the fractional part of the outcome,
if desired.
Returns:
array: the corresponding bit values.
"""
# Bin width
alpha = np.sqrt(np.pi)
# np.divmod implementation also possible
int_frac = integer_fractional(outcomes, alpha)
# CAUTION: % has weird behaviour that seems to only show up for
# large (n ~ 100) multiples of sqrt(pi). Check!
bit_values = int_frac[0] % 2
if return_fraction:
return bit_values, int_frac[1]
return bit_values
[docs]def Z_err(var, var_num=5):
"""Return the probability of Z errors for a list of variances.
Args:
var (array): array of lattice p variances
var_num (float): number of variances away from the origin we
include in the integral
Returns:
array: probability of Z (phase flip) errors for each variance.
"""
# Find largest bin number by finding largest variance, multiplying by
# var_num, then rounding up to nearest integer of form 4n+1, which are
# the left boundaries of the 0 bins mod sqrt(pi)
n_max = int(np.ceil(var_num * np.amax(var)) // 2 * 4 + 1)
# error = 1 - integral over the 0 bins
# Initiate a list with length same as var
error = np.ones(len(var))
# Integral over 0 bins that fell within var_num*var_max away from
# origin
for i in range(-n_max, n_max, 4):
error -= 0.5 * (
erf((i + 2) * np.sqrt(np.pi) / (2 * var)) - erf(i * np.sqrt(np.pi) / (2 * var))
)
return error
[docs]def Z_err_cond(var, hom_val, var_num=10, replace_undefined=0, use_hom_val=False):
"""Return phase error probabilities conditioned on homodyne outcomes.
Return the phase error probability for a list of variances var given
homodyne outcomes hom_val, with var_num used to determine the number of
terms to keep in the summation in the formula. By default, the fractional
part of hom_val is used in the summation; if use_hom_val is True, use the
entire homodyne value.
Args:
var (array): the variances of the p quadrature.
hom_val (array): the p-homodyne outcomes.
var_num (float): number of variances away from the origin we include in
the integral.
replace_undefined (float): how to handle 0 denominators.
If 'bin_location', return poor-man's probability that ranges from 0
in the centre of a bin to 0.5 halfway between bins. Otherwise, sets
it to the replace_undefined value (0 by default).
use_hom_val (bool): if True, use the entire homodyne value hom_val in
the expression; otherwise use the fractional part.
Returns:
array: probability of Z (phase flip) errors for each variance,
contioned on the homodyne outcomes.
"""
# TODO: Perhaps make n_max a more complicated function of var_num, or
# better automate summation range.
n_max = var_num
bit, frac = GKP_binner(hom_val, return_fraction=True)
factor = 1 - bit if use_hom_val else 1
val = hom_val if use_hom_val else frac
if np.isscalar(val) and np.isscalar(var):
def ex_val(n):
return np.exp(-((val - n * np.sqrt(np.pi)) ** 2) / var)
numerator = np.sum(ex_val(2 * np.arange(-n_max, n_max) + factor))
denominator = np.sum(ex_val(np.arange(-n_max, n_max)))
if denominator != 0:
return numerator / denominator
else:
return replace_undefined
else:
# Initiate a list with length same as var
error = np.zeros(np.shape(var))
def ex(z, n):
return np.exp(-((z - n * np.sqrt(np.pi)) ** 2) / var)
numerator = np.sum([ex(val, 2 * i + factor) for i in range(-n_max, n_max)], 0)
denominator = np.sum([ex(val, i) for i in range(-n_max, n_max)], 0)
# Dealing with 0 denonimators
where_0 = np.where(denominator == 0)[0]
the_rest = np.delete(np.arange(np.size(var)), where_0)
error = np.empty(np.size(var))
# For 0 denominator, populate error according to replace_undefined
if len(where_0):
if replace_undefined == "bin_location":
zero_dem_result = np.abs(val) / np.sqrt(np.pi)
else:
zero_dem_result = replace_undefined
error[where_0] = np.full(len(where_0), zero_dem_result)
if np.size(var) == 1:
numerator = np.array([numerator])
denominator = np.array([denominator])
if the_rest.size > 0:
error[the_rest] = numerator[the_rest] / denominator[the_rest]
if np.size(var) == 1:
error = error[0]
return error
_modules/flamingpy/cv/gkp
Download Python script
Download Notebook
View on GitHub