Package: pykronecker
Check out the full documentation and install instructionshere:)OverviewPyKronecker is a library for manipulating matrices which have aKronecker productstructure. Systems involving Kronecker products arise in many areas of applied mathematics and statistics. The aim of this library is to provide a clean interface for dealing with such systems, combining lazy evaluation and algebraic tricks to deliver large savings in terms of both memory and execution time.InstallationInstallation on Windows, OSX and Linux can be performed by runningpip3 install pykroneckerThis will install the vanilla version of the library, with support for NumPy arrays only. Linux users have the additional option of installing PyKronecker withJaxsupport. The benefit of this is significantly faster runtimes, even when working with NumPy arrays only, due to Jax's JIT complier. This can be installed by runningpip3 install "pykronecker[jax]"For Linux users with an Nvidia graphics card, PyKronecker is also compatible with the GPU and TPU version of Jax. However, since this relies on CUDA and cuDNN, it is recommended to follow the instructionshereto install Jax first.UsageThe concept of this library is to create instances of aKroneckerOperatorclass, which can be broadly treated as if it is a square numpy array. These objects are designed to be used with the@syntax for matrix multiplication.Basic operatorsKroneckerProductCreate aKroneckerProductfrom two or more square NumPy/Jax arrays. These can be real or complex valued.importnumpyasnpfrompykroneckerimportKroneckerProductA=np.random.normal(size=(5,5))B=np.random.normal(size=(6,6))KP=KroneckerProduct([A,B])This object can operate on both vectors of shape(5 * 6, )and tensors of shape(5, 6)using the@syntax for matrix multiplication. The returned array will be of the same shape.x=np.random.normal(size=5*6)X=x.reshape(5,6)assertnp.allclose(KP@x,(KP@X).ravel())KroneckerSumAKronekerSumcan be created and used in much the same way.importnumpyasnpfrompykroneckerimportKroneckerSumA=np.random.normal(size=(5,5))B=np.random.normal(size=(6,6))x=np.random.normal(size=5*6)KS=KroneckerSum([A,B])print(KS@x)KroneckerDiagKroneckerDiagprovides support for diagonal matrices, and can be created by passing a tensor of the appropriate size. This creates, in effect, a matrix with the vectorized tensor along the diagonal.importnumpyasnpfrompykroneckerimportKroneckerDiagD=np.random.normal(size=(5,6))x=np.random.normal(size=5*6)KD=KroneckerDiag(D)print(KD@x)KroneckerIdentityFinally,KroneckerIdentitycreates the identity matrix, which can be instantiated by passing another operator of the same size, or the shape of tensors the operator is expected to act on.importnumpyasnpfrompykroneckerimportKroneckerIdentity,KroneckerDiag# create another KroneckerDiag operatorD=np.random.normal(size=(5,6))KD=KroneckerDiag(D)# create a KroneckerIdentity by passing `like` parameterKI1=KroneckerIdentity(like=KD)# create KroneckerIdentity by passing `tensor_shape` parameterKI2=KroneckerIdentity(tensor_shape=(5,6))x=np.random.normal(size=5*6)assertnp.allclose(KI1@x,x)assertnp.allclose(KI2@x,x)Deriving new operatorsAll four of these objects can be added or multiplied together arbitrarily to create new composite operators. In this way, they can be treated similarly to literal NumPy arrays.importnumpyasnpfrompykroneckerimport*A=np.random.normal(size=(5,5))B=np.random.normal(size=(6,6))D=np.random.normal(size=(5,6))x=np.random.normal(size=5*6)KP=KroneckerProduct([A,B])KS=KroneckerSum([A,B])KD=KroneckerDiag(D)KI=KroneckerIdentity(like=KP)# create a new composite operator!new_operator1=KP@KD+KS-KIprint(new_operator1@x)Other possible operations include transposing with.T, and multiplying/dividing by a scalar.new_operator2=5*KP.T-KS/2print(new_operator2@x)Many basic operators can also be multipled element-wise just as with NumPy arrays.new_operator3=KS*KPprint(new_operator3@x)Some operators (notably, notKroneckerSums) can be raised to a power element-wisenew_operator4=KP**2print(new_operator4@x)Block operatorsBlock operators are composed of smaller operators which have been stacked into a set of blocks. In the example below, we create a new block operatorKBwhich is composed of four other block operators.importnumpyasnpfrompykroneckerimport*A=np.random.normal(size=(5,5))B=np.random.normal(size=(6,6))D=np.random.normal(size=(5,6))KP=KroneckerProduct([A,B])KS=KroneckerSum([A,B])KD=KroneckerDiag(D)KI=KroneckerIdentity(like=KP)# Create a block of pure KroneckerOperatorsKB1=KroneckerBlock([[KP,KD],[KI,KS]])x1=np.random.normal(size=5*6*2)print(KB1@x1)We can also create block operators that contain a mixture ofKroneckerOperators and NumPy arrays# Create a block with a mixture of KroneckerOperators and ndarraysM11=KPM12=np.ones((5*6,5))M21=np.random.normal(size=(5,5*6))M22=np.eye(5)KB2=KroneckerBlock([[M11,M12],[M21,M22]])x2=np.random.normal(size=5*6+5)print(KB2@x2)Block diagonal matrices can also be created in a similar wayfrompykroneckerimportKroneckerBlockDiagKBD=KroneckerBlockDiag([KP,KS])x3=np.random.normal(size=5*6*2)print(KBD@x3)Other featuresFor operators that are products ofKroneckerProducts,KroneckerDiags, orKroneckerIdentitys, we can find the inverse with.inv().importnumpyasnpfrompykroneckerimport*A=np.random.normal(size=(5,5))B=np.random.normal(size=(6,6))D=np.random.normal(size=(5,6))x=np.random.normal(size=5*6)KP=KroneckerProduct([A,B])KS=KroneckerSum([A,B])KD=KroneckerDiag(D)KI=KroneckerIdentity(like=KP)# find the inverseM=(KP@KD).inv()print(M@x)Basic indexing is supported for all operators. This will return literal numpy arrays.Advanced indexingis not supported.J=KP.T+KS@KDprint(J[0])print(J[2:5])print(J[2:8:3])print(J[:,2])print(J[:,2:5])print(J[:,2:8:3])print(J[2,5])print(J[2:5,2:8:3])print(J[:])print(J[:,:])Summing down an axis or over the whole matrix is supported for any opertor.print(J.sum(0))print(J.sum(1))print(J.sum())Any operator can also be converted to a literal array. This should only be used for small test purposes, as the arrays created can be very large.print(J.to_array())The matrix diagonal of most operators can be found with.diag(). This returns a one-dimensional array.print(J.diag())The conjugate transpose of any complex operator can be found with.HA_=np.random.normal(size=(5,5))+1j*np.random.normal(size=(5,5))B_=np.random.normal(size=(6,6))+1j*np.random.normal(size=(6,6))KP_=KroneckerProduct([A_,B_])print(KP_.H@x)Use with JAXOperators and tensors can also be created from Jax arrays for accelerated computation when thepykronecker[jax]extra has been installed. Note that this is only available on Linux and MacOS.importnumpyasnpimportjax.numpyasjnpfrompykroneckerimportKroneckerProductA=jnp.asarray(np.random.normal(size=(5,5)))B=jnp.asarray(np.random.normal(size=(6,6)))x=jnp.asarray(np.random.normal(size=5*6))KP=KroneckerProduct([A,B])print(KP@x)