Source code for qrisp.alg_primitives.arithmetic.matrix_multiplication
"""\********************************************************************************* Copyright (c) 2025 the Qrisp authors** This program and the accompanying materials are made available under the* terms of the Eclipse Public License 2.0 which is available at* http://www.eclipse.org/legal/epl-2.0.** This Source Code may also be made available under the following Secondary* Licenses when the conditions for such availability set forth in the Eclipse* Public License, v. 2.0 are satisfied: GNU General Public License, version 2* with the GNU Classpath Exception which is* available at https://www.gnu.org/software/classpath/license.html.** SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0********************************************************************************/"""importnumpyasnpimportsympyasspfromqrisp.core.gate_application_functionsimportp,zfromqrisp.coreimportQuantumArrayfromqrisp.alg_primitives.arithmetic.SBP_arithmeticimporthybrid_mult,polynomial_encoder
[docs]defq_matmul(q_array_0,q_array_1,output_array=None,res_bit_shape="eq",phase_tolerant=False):""" Matrix multiplication for QuantumArrays. Parameters ---------- q_array_0 : QuantumArray The first factor of the matrix multiplication. q_array_1 : QuantumArray The second factor of the matrix multiplication. output_array : QuantumArray, optional The QuantumArray to store the results in. By default, a new QuanumArray is created. res_bit_shape : str or QuantumFloat, optional Specification of the dimension of the output bitshape of the output QuantumArray. Possible are "eq", which will take the bitshape equal two the first factor, "safe" which automatically determines the bitshape such that there can be no overflow, or a QuantumFloat which has the desired bitshape. The default is "eq". phase_tolerant : bool, optional If set to True, the required gate count is reduced but each constellation of computational basis states of the inputs will introduce a different phase. This is helpful when it's clear that this function will be at some point uncomputed, resulting in the cancelation of these phases. The default is False. Raises ------ Exception Tried to perform matrix multiplication with differing contraction index size. Returns ------- res : QuantumArray The result of the matrix multiplication. Examples -------- We multiply a QuantumArray with a multiply of the identity matrix (np.eye): >>> import numpy as np >>> from qrisp import QuantumFloat, QuantumArray, q_matmul >>> qf = QuantumFloat(4,0, signed = False) >>> q_arr_0 = QuantumArray(qf, shape = (2,2)) >>> q_arr_1 = QuantumArray(qf, shape = (2,2)) >>> q_arr_0[:] = [[1,2],[3,4]] >>> q_arr_1[:] = 2*np.eye(2) >>> res = q_matmul(q_arr_0, q_arr_1) >>> print(res) {OutcomeArray([[2, 4], [6, 8]]): 1.0} """ifq_array_0.shape[1]!=q_array_1.shape[0]:raiseException("Tried to perform matrix multiplication""with differing contraction index size")L=q_array_0.shape[0]K=q_array_0.shape[1]J=q_array_1.shape[1]ifoutput_arrayisnotNone:res=output_arrayelse:fromqrispimportQuantumFloatifisinstance(res_bit_shape,QuantumFloat):qtype=res_bit_shape.duplicate()ifres_bit_shape=="safe":fromsympyimportSymbolq_array_0_symbols=[]q_array_1_symbols=[]poly=0foriinrange(K):q_array_0_symbols.append(Symbol(q_array_0[0,i].name))q_array_1_symbols.append(Symbol(q_array_1[i,0].name))poly+=Symbol(q_array_0[0,i].name)*Symbol(q_array_1[i,0].name)# poly += q_array_0_symbols[-1]*q_array_1_symbols[-1]fromqrisp.alg_primitives.arithmeticimportcreate_output_qfqtype=create_output_qf(list(q_array_0.flatten())+list(q_array_1.flatten()),poly)ifres_bit_shape=="eq":qtype=q_array_0[0,0]res=QuantumArray(shape=(L,J),qtype=qtype,qs=q_array_0.qs)foriinrange(L):forjinrange(J):forkinrange(K):ifK==1:ifoutput_arrayisnotNone:hybrid_mult(q_array_0[i,k],q_array_1[k,j],output_qf=res[i,j],init_op="qft",terminal_op="qft",phase_tolerant=phase_tolerant,)else:hybrid_mult(q_array_0[i,k],q_array_1[k,j],output_qf=res[i,j],init_op="h",terminal_op="qft",phase_tolerant=phase_tolerant,)continue# If the res_bit_shape is safe,we can use the strategy# of correcting phases on the result as displayed in hybrid multifres_bit_shape=="safe"andnotphase_tolerant:ifk==0:hybrid_mult(q_array_0[i,k],q_array_1[k,j],output_qf=res[i,j],init_op="h",terminal_op=None,phase_tolerant=True,)elifk==K-1:hybrid_mult(q_array_0[i,k],q_array_1[k,j],output_qf=res[i,j],init_op=None,terminal_op="qft",phase_tolerant=True,)elifk!=q_array_0.shape[1]-1:hybrid_mult(q_array_0[i,k],q_array_1[k,j],output_qf=res[i,j],init_op=None,terminal_op=None,phase_tolerant=True,)forminrange(res[i,j].size):p(-(2*np.pi)*2**l/2**(res[i,j].size+1),res[i,j][m],)ifres[i,j].signedandK%2:z(res[i,j][-1])else:ifk==0:ifoutput_arrayisnotNone:hybrid_mult(q_array_0[i,k],q_array_1[k,j],output_qf=res[i,j],init_op="qft",terminal_op=None,phase_tolerant=phase_tolerant,)else:hybrid_mult(q_array_0[i,k],q_array_1[k,j],output_qf=res[i,j],init_op="h",terminal_op=None,phase_tolerant=phase_tolerant,)elifk==K-1:hybrid_mult(q_array_0[i,k],q_array_1[k,j],output_qf=res[i,j],init_op=None,terminal_op="qft",phase_tolerant=phase_tolerant,)else:hybrid_mult(q_array_0[i,k],q_array_1[k,j],output_qf=res[i,j],init_op=None,terminal_op=None,phase_tolerant=phase_tolerant,)returnres
[docs]defsemi_classic_matmul(q_matrix,cl_matrix,output_array=None,res_bit_shape="eq"):""" Performs matrix multiplication between a classical numpy array and a QuantumArray Parameters ---------- q_matrix : QuantumArray The QuantumArray to multiply. cl_matrix : numpy.ndarray The numpy array to multiply. output_array : QuantumArray, optional The QuantumArray to store the result in. The default is None. res_bit_shape : str or QuantumFloat, optional Specification of the dimension of the output bitshape of the output QuantumArray. Possible are "eq", which will take the bitshape equal two the first factor, "safe" which automatically determines the bitshape such that there can be no overflow, or a QuantumFloat which has the desired bitshape. The default is "eq". Raises ------ Exception Tried to apply matrix multiplication with unfitting dimensions. Returns ------- output_array : QuantumArray The QuantumArray containing the result. Examples -------- We multiply a QuantumArray with a scalar multiple of the identity matrix (np.eye): >>> import numpy as np >>> from qrisp import QuantumFloat, QuantumArray, semi_classic_matmul >>> qf = QuantumFloat(4,0, signed = False) >>> q_arr_0 = QuantumArray(qf, shape = (2,2)) >>> q_arr_1 = QuantumArray(qf, shape = (2,2)) >>> q_arr_0[:] = [[1,2],[3,4]] >>> res = semi_classic_matmul(q_arr_0, 2*np.eye(2)) >>> print(res) {OutcomeArray([[2, 4], [6, 8]]): 1.0} """ifq_matrix.shape[1]!=cl_matrix.shape[0]:raiseException("Tried to apply matrix multiplication with unfitting dimensions")L=q_matrix.shape[0]K=q_matrix.shape[1]J=cl_matrix.shape[1]fromsympyimportSymbolfromqrispimportQuantumFloatifoutput_arrayisNone:ifisinstance(res_bit_shape,QuantumFloat):qtype=res_bit_shape.duplicate()ifres_bit_shape=="safe":q_array_0_symbols=[]poly=0foriinrange(K):q_array_0_symbols.append(Symbol(q_matrix[0,i].name))poly+=Symbol(q_matrix[0,i].name)fromqrisp.alg_primitives.arithmeticimportcreate_output_qfqtype=create_output_qf(list(q_matrix.flatten()),poly)ifres_bit_shape=="eq":qtype=q_matrix[0,0]output_array=QuantumArray(shape=(L,J),qtype=qtype,qs=q_matrix.qs)ifoutput_array.shape!=(L,J):raiseException("Tried to to encode matrix multiplication""into Quantum Array of unfitting size")fromsympy.matricesimportzerossymb_matrix=zeros(q_matrix.shape[0],q_matrix.shape[1])symbol_dic={}foriinrange(L):forkinrange(K):temp_symb=Symbol("x_"+str(i)+"_"+str(k))symb_matrix[i,k]=temp_symbsymbol_dic[q_matrix[i,k].name]=temp_symbres_symb_matrix=symb_matrix@cl_matrixqv_list=list(q_matrix.flatten())foriinrange(L):forjinrange(J):ifres_symb_matrix[i,j]!=0:polynomial_encoder(qv_list,output_array[i,j],res_symb_matrix[i,j],encoding_dic=symbol_dic,)returnoutput_array
defegcd(a,b):ifa==0:return(b,0,1)else:g,y,x=egcd(b%a,a)return(g,x-(b//a)*y,y)defmodinv(a,m):g,x,y=egcd(a,m)ifg!=1:raiseException("modular inverse does not exist")else:returnx%m# Function for inplace multiplication with a quantum array# [qv1, qv2,.. qvn] with a n x n matrix A# ie. the array contains [a11*qv1 +a12*qv2+ ... , a21*qv1..] after
[docs]definplace_matrix_app(vector,matrix):r""" Performs inplace matrix application to a vector-valued QuantumArray. Note that due to reversibility reasons, the matrix can only contain integers and has to be invertible over $\text{GL}(2^n)$. This is equal to the condition that the determinant is odd. Parameters ---------- vector : QuantumArray The QuantumArray to apply the matrix to. matrix : numpy.ndarray The matrix to apply. Raises ------ Exception Tried to multiply matrix with Quantum Array with unfitting shape. Examples -------- We perform an inplace matrix multiplication between a randomly chosen matrix and a randomly chosen vector >>> from qrisp import QuantumFloat, QuantumArray, inplace_matrix_app >>> qtype = QuantumFloat(4) >>> q_vector = QuantumArray(qtype, 3) >>> q_vector[:] = [1,0,2] >>> matrix = np.array([[2., 2., 1.], [2., 3., 1.], [3., 1., 2.]]) >>> inplace_matrix_app(q_vector, matrix) >>> print(q_vector) {OutcomeArray([4, 4, 7]): 1.0} """iflen(vector.shape)!=1:raiseException("Tried to multiply matrix with Quantum Array with unfitting shape")n=vector.shape[0]ifn!=matrix.shape[0]orn!=matrix.shape[1]:raiseException("Tried to multiply matrix with Quantum Array with unfitting shape")bit=vector[0].size# The general idea is to calculate a row of the matrix into the variable x_j which# can be reconstructed from the result of the calculation and thus discarded.# Calling the result of this evaluation x_j_new we have# x_j_new = a*x_j + b*x_0 ... c*x_n# For this equation to be invertible a has to be invertible. We then have# x_j = 1/a*(x_j_new - b*x_0 + .. c*x_n)# we then replace x_j in the remaining evaluation equations with this expression# We will perform this process in two steps:# 1. Prepare a list of equations that contains what should be evaluated# 2. Evaluate these equations in the quantum session# Step 1:# Prepare symbolsancilla_symbol=sp.Symbol("a")x=[sp.Symbol("x"+str(i))foriinrange(n)]# This list contains the equations that need to be calculatedtarget_values=matrix*sp.Matrix(x)# This list contains the variables that have been eliminated in favor# of the completed equationseliminated_variables=[]# This list contains the equations# that will be evaluated later by the circuit generatoreval_eq=[]fromqrisp.miscimportis_invforiinrange(n):# Save evaluationeval_eq.append(target_values[i])# Find a column with invertible coeffiecientj=0whileTrue:# Determine that coefficient(can be done by differentiating the evaluating# eq. after the column variable)coeff=int(sp.diff(target_values[i],x[j]))# Check if the coefficient is invertible and has not been eliminated yetifis_inv(coeff,bit)and(jnotineliminated_variables):breakj+=1ifj>=n:raiseException("Could not find invertible element")# Generate inverse equationeval_inverse=modinv(coeff,2**bit)*(-target_values[i].subs({x[j]:0})+ancilla_symbol)fromqrisp.qtypes.quantum_floatimporttrunc_poly# Truncate coefficients in order to stay inside Z/ 2^n Zeval_inverse=trunc_poly(eval_inverse,(0,bit))# Rewrite the remaining evaluation equations in terms of the new variablesubs_dic={x[j]:eval_inverse}forkinrange(i+1,n):# Substitue in the following equationstarget_values[k]=(target_values[k].subs(subs_dic).subs({ancilla_symbol:x[j]}))# Truncate coefficientstarget_values[k]=trunc_poly(target_values[k],(0,bit))eliminated_variables.append(j)# Step 2:# Create symbol dic in order for the polynomial encoder# to know which symbol to use as which quantum floatsymbol_dic={vector[j].name:x[j]forjinrange(vector.shape[0])}foriinrange(n):# Evaluate the value of the entry in eval_eq into the x_j variableelim_var=eliminated_variables[i]# Create list of quantum variables which are not being written onnon_elim_var_list=list(vector)non_elim_var_list.pop(elim_var)# Perform evaluation. The first step is to determine the coefficient for the# inplace multiplication (ie. in the language of the initial description "a")inplace_mult_coeff=int(sp.diff(eval_eq[i],x[elim_var]))# Now determine the remaining polynomialeval_poly=eval_eq[i].subs({x[elim_var]:0})print(inplace_mult_coeff)polynomial_encoder(non_elim_var_list,vector[elim_var],eval_poly,encoding_dic=symbol_dic,inplace_mult=inplace_mult_coeff,)# Reorder quantum arrayqv_reordering_array=np.zeros(n,dtype="object")foriinrange(n):qv_reordering_array[i]=vector[eliminated_variables[i]]# In order to enter the reorder values in the QuantumArray, we need to use the# setitem method of ndarray because setitem for QuantumArray# is overloaded with initializationnp.ndarray.__setitem__(vector,slice(None,None,None),qv_reordering_array)
defauto_matmul_wrapper(a,b,out=None):ifisinstance(a,QuantumArray)andisinstance(b,QuantumArray):returnq_matmul(a,b,out)elifisinstance(a,QuantumArray):returnsemi_classic_matmul(a,b,out)elifisinstance(b,QuantumArray):returnsemi_classic_matmul(b.transpose(),a.transpose(),out).transpose()else:raiseException("Could not proccess input constellation "+str(type(a))+" and "+str(type(b)))
[docs]defdot(a,b,out=None):""" Port of the popular `numpy function <https://numpy.org/doc/stable/reference/generated/numpy.dot.html>`_ with similar semantics. Parameters ---------- a : QuantumArray or QuantumFloat or numpy.ndarray The first operand. b : QuantumArray or QuantumFloat or numpy.ndarray The second operand. out : QuantumArray, optional The QuantumArray to store the output in. The default is None. Returns ------- QuantumArray The result as described in the numpy documentation. Examples -------- We create two QuantumArrays and apply dot as a function performing matrix-vector multiplication. >>> import numpy as np >>> from qrisp import QuantumFloat, QuantumArray, dot >>> qf = QuantumFloat(5,0, signed = False) >>> q_arr_0 = QuantumArray(qf) >>> q_arr_1 = QuantumArray(qf) >>> q_arr_0[:] = [2,3] >>> q_arr_1[:] = 2*np.eye(2) >>> res = dot(q_arr_0, q_arr_1) >>> print(res) {OutcomeArray([[4, 6]]): 1.0} Scalar-product: >>> q_arr_0 = QuantumArray(qf) >>> q_arr_1 = QuantumArray(qf) >>> q_arr_0[:] = [3,4,5] >>> q_arr_1[:] = [1,1,1] >>> res = dot(q_arr_0, q_arr_1) >>> print(res) {12: 1.0} Matrix-matrix multiplication >>> qf = QuantumFloat(3,0, signed = True) >>> q_arr_0 = QuantumArray(qf) >>> q_arr_1 = QuantumArray(qf) >>> q_arr_0[:] = [[0,1],[1,0]] >>> q_arr_1[:] = [[1,0],[0,-1]] >>> res = dot(q_arr_0, q_arr_1) >>> print(res) {OutcomeArray([[ 0, -1], [ 1, 0]]): 1.0} """fromqrispimportQuantumFloatifisinstance(a,QuantumFloat)orisinstance(b,QuantumFloat):returnnp.dot(a,b,out)else:iflen(a.shape)==1andlen(b.shape)==1:temp_0=a.reshape((1,a.shape[0]))temp_1=b.reshape((a.shape[0],1))res=(auto_matmul_wrapper(temp_0,temp_1,out))[0,0]returnreseliflen(a.shape)==2andlen(b.shape)==2:returnauto_matmul_wrapper(a,b,out)eliflen(b.shape)==1:temp_0=a.reshape((a.size//b.shape[-1],b.shape[-1]))temp_1=b.reshape((b.shape[0],1))new_shape=list(b.shape)new_shape[-1]=b.shape[0]res=(auto_matmul_wrapper(temp_0,temp_1,out)).reshape(new_shape)returnreselse:temp_0=a.reshape((a.size//b.shape[-1],b.shape[-1]))temp_1=np.swapaxes(b,0,-2)temp_1=np.reshape(temp_1,(temp_1.shape[0],temp_1.size//temp_1.shape[0]))res=auto_matmul_wrapper(temp_0,temp_1,out)res_shape=list(temp_1.shape)res_shape.pop(-2)res_shape=list(temp_0.shape)[:-1]+res_shapereturnnp.reshape(res,res_shape)
[docs]deftensordot(a,b,axes):r""" Port of `numpy tensordot <https://numpy.org/doc/stable/reference/generated/numpy.tensordot.html>`_ with similar semantics. Parameters ---------- a : QuantumArray The first operand. b : QuantumArray The second operand. axes : tuple The axes to contract. Returns ------- QuantumArray The QuantumArray containing the result of tensordot. Examples -------- Using ``tensordot`` we can perform the arithmetic for simulating a quantum computer *on a quantum computer*. >>> import numpy as np >>> from qrisp import QuantumFloat, QuantumArray, tensordot Initiate the QuantumArray holding the statevector. We initate the state of uniform superposition .. math:: \ket{+} = \frac{1}{\sqrt{2^n}} \sum_{i = 0}^{2^n - 1} \ket{i} >>> qfloat_type = QuantumFloat(3, -2, signed = True) >>> num_qubits = 4 >>> statevector = QuantumArray(shape = 2**num_qubits, qtype = qfloat_type) >>> statevector[:] = [1/(2**num_qubits)**0.5]*2**num_qubits >>> print(statevector) {OutcomeArray([0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25]): 1.0} Initiate the QuantumArray holding the unitary of a Z-gate >>> z_gate = QuantumArray(shape = (2,2), qtype = qfloat_type) >>> z_gate[:] = [[1,0], [0,-1]] >>> print(z_gate) {OutcomeArray([[ 1., 0.], [ 0., -1.]]): 1.0} Perform the contraction >>> statevector = statevector.reshape(num_qubits*[2]) >>> target_qubit = 3 >>> new_statevector = tensordot(z_gate, statevector, (1, target_qubit)) >>> new_statevector = new_statevector.reshape(2**num_qubits) >>> print(new_statevector) {OutcomeArray([ 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25, -0.25]): 1.0} We perform a similar contraction using numpy arrays >>> from numpy import tensordot >>> statevector = 0.25*np.ones(2**num_qubits) >>> print(statevector) [0.25 0.25 0.25 0.25 0.25 0.25 0.25 0.25 0.25 0.25 0.25 0.25 0.25 0.25 0.25 0.25] >>> statevector = statevector.reshape(num_qubits*[2]) >>> z_gate = np.zeros((2,2)) >>> z_gate[:] = [[1,0], [0,-1]] >>> new_statevector = tensordot(z_gate, statevector, (1, target_qubit)) >>> print(new_statevector.reshape(2**num_qubits)) [ 0.25 0.25 0.25 0.25 0.25 0.25 0.25 0.25 -0.25 -0.25 -0.25 -0.25 -0.25 -0.25 -0.25 -0.25] """try:iter(axes)exceptException:axes_a=list(range(-axes,0))axes_b=list(range(0,axes))else:axes_a,axes_b=axestry:na=len(axes_a)axes_a=list(axes_a)exceptTypeError:axes_a=[axes_a]na=1try:nb=len(axes_b)axes_b=list(axes_b)exceptTypeError:axes_b=[axes_b]nb=1a,b=np.asanyarray(a),np.asanyarray(b)as_=a.shapenda=a.ndimbs=b.shapendb=b.ndimequal=Trueifna!=nb:equal=Falseelse:forkinrange(na):ifas_[axes_a[k]]!=bs[axes_b[k]]:equal=Falsebreakifaxes_a[k]<0:axes_a[k]+=ndaifaxes_b[k]<0:axes_b[k]+=ndbifnotequal:raiseValueError("shape-mismatch for sum")# Move the axes to sum over to the end of "a"# and to the front of "b"notin=[kforkinrange(nda)ifknotinaxes_a]newaxes_a=notin+axes_aN2=1foraxisinaxes_a:N2*=as_[axis]newshape_a=(int(np.multiply.reduce([as_[ax]foraxinnotin])),N2)olda=[as_[axis]foraxisinnotin]notin=[kforkinrange(ndb)ifknotinaxes_b]newaxes_b=axes_b+notinN2=1foraxisinaxes_b:N2*=bs[axis]newshape_b=(N2,int(np.multiply.reduce([bs[ax]foraxinnotin])))oldb=[bs[axis]foraxisinnotin]at=a.transpose(newaxes_a).reshape(newshape_a)bt=b.transpose(newaxes_b).reshape(newshape_b)res=dot(at,bt)returnres.reshape(olda+oldb)
Get in touch!
If you are interested in Qrisp or high-level quantum algorithm research in general connect with us on our
Slack workspace.