phi1234.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. ######################################################
  2. #
  3. # Fock space Hamiltonian truncation for phi^4 theory in 2 dimensions
  4. # Authors: Slava Rychkov (slava.rychkov@lpt.ens.fr) and Lorenzo Vitale (lorenzo.vitale@epfl.ch)
  5. # December 2014
  6. #
  7. ######################################################
  8. import scipy
  9. from scipy import pi
  10. import numpy as np
  11. import scipy.sparse.linalg
  12. import scipy.sparse
  13. import scipy.interpolate
  14. from operator import attrgetter
  15. from math import factorial
  16. from statefuncs import Basis, NotInBasis, omega, State
  17. from oscillators import NormalOrderedOperator as NOO
  18. import collections
  19. import renorm
  20. import itertools
  21. import time
  22. tol = 0.0001
  23. """ P denotes spatial parity, while K field parity. For now only the P-even sector is implemented """
  24. def comb(*x):
  25. """ computes combinatorial factor for list of elements """
  26. #note: we could do this with np.unique if the list was a numpy array
  27. #print(collections.Counter(x).values())
  28. return factorial(len(x))/np.prod(scipy.special.factorial(list(collections.Counter(x).values())))
  29. #return factorial(len(x))/np.prod(list(map(factorial,collections.Counter(x).values())))
  30. #return factorial(len(x))/scipy.prod(set(map(factorial,collections.Counter(x).values())))
  31. class Matrix():
  32. """ Matrix with specified state bases for row and column indexes.
  33. This class is useful to easily extract submatrices """
  34. def __init__(self, basisI, basisJ, M=None):
  35. self.basisI = basisI
  36. self.basisJ = basisJ
  37. #if no M is given, set M to be a basisI.size by 1 sparse matrix.
  38. if(M == None):
  39. self.M = scipy.sparse.coo_matrix((basisI.size, 1))
  40. else:
  41. self.M = M
  42. self.check()
  43. def addColumn(self, newcolumn):
  44. m = scipy.sparse.coo_matrix(newcolumn).transpose()
  45. self.M = scipy.sparse.hstack([self.M,m])
  46. def finalize(self):
  47. """Drops first column of M and ensures M is in COO format"""
  48. self.M = self.M.tocsc()[:,1:].tocoo()
  49. self.check()
  50. def check(self):
  51. """Check that M has the right dimensions"""
  52. if self.M.shape != (self.basisI.size, self.basisJ.size):
  53. raise ValueError('Matrix shape inconsistent with given bases')
  54. def __add__(self, other):
  55. """ Sum of matrices """
  56. return Matrix(self.basisI, self.basisJ, self.M+other.M)
  57. def __mul__(self, other):
  58. """ Multiplication of matrix with matrix or number"""
  59. if(other.__class__ == self.__class__):
  60. return Matrix(self.basisI, other.basisJ, self.M*other.M)
  61. else:
  62. return Matrix(self.basisI, self.basisJ, self.M*float(other))
  63. def to(self, form):
  64. """ Format conversion """
  65. return Matrix(self.basisI, self.basisJ, self.M.asformat(form))
  66. def sub(self, subBasisI=None, subBasisJ=None):
  67. """ This extracts a submatrix given a subspace of the initial vector space, both for rows and columns """
  68. if subBasisI != None and subBasisJ != None:
  69. rows = [self.basisI.lookup(state)[1] for state in subBasisI]
  70. columns = [self.basisJ.lookup(state)[1] for state in subBasisJ]
  71. return Matrix(subBasisI, subBasisJ, self.M.tocsr()[scipy.array(rows)[:,scipy.newaxis],scipy.array(columns)])
  72. elif subBasisI != None and subBasisJ == None:
  73. rows = [self.basisI.lookup(state)[1] for state in subBasisI]
  74. return Matrix(subBasisI, self.basisJ, self.M.tocsr()[scipy.array(rows),:])
  75. elif subBasisI == None and subBasisJ != None:
  76. columns = [self.basisJ.lookup(state)[1] for state in subBasisJ]
  77. return Matrix(self.basisI, subBasisJ, self.M.tocsr()[:,scipy.array(columns)])
  78. else:
  79. return self
  80. def transpose(self):
  81. """Transpose the matrix (switch basisI and basisJ and transpose M)"""
  82. return Matrix(self.basisJ, self.basisI, self.M.transpose())
  83. def __repr__(self):
  84. return str(self.M)
  85. class Phi1234():
  86. """ main class
  87. Attributes
  88. ----------
  89. L : float
  90. Circumference of the circle on which to quantize
  91. m : float
  92. Mass of the scalar field
  93. Emax: float
  94. Maximum energy cutoff
  95. All basic dictionaries in this class have two keys, 1 and -1,
  96. corresponding to values of k-parity.
  97. h0: dict of Matrix objects
  98. Values are Matrix objects corresponding to the free Hamiltonian
  99. potential: dict of dict of Matrix
  100. Values are dictionaries where the keys are orders in
  101. the field (0,2,4 correspond to phi^0,phi^2,phi^4) and the values
  102. are the potential matrices at each order stored as Matrix objects
  103. When actually diagonalizing the Hamiltonian, we further restrict
  104. to a subspace with some different nmax, possibly?
  105. H: dict of Matrix objects
  106. Like h0, but on a restricted subspace defined by basis rather than
  107. fullBasis
  108. V: dict of dict of Matrix
  109. Like potential but restricted to the basis subspace
  110. """
  111. def __init__(self):
  112. self.L = None
  113. self.m = None
  114. self.Emax = None
  115. self.h0 = {1: None, -1: None}
  116. self.potential = {1 :{ }, -1:{ }}
  117. self.h0Sub = {1: None, -1: None}
  118. self.H = {1: None, -1: None}
  119. self.V = {1: {}, -1: {}}
  120. self.eigenvalues = {1: None, -1: None}
  121. self.eigsrenlocal = {1: None, -1: None}
  122. self.eigsrensubl = {1: None, -1: None}
  123. self.eigenvectors = {1: None, -1: None}
  124. # Eigenvalues and eigenvectors for different K-parities
  125. self.basis = {1: None, -1: None}
  126. self.fullBasis = {1: None, -1: None}
  127. def buildFullBasis(self,k,L,m,Emax):
  128. """ Builds the full Hilbert space basis """
  129. self.L=float(L)
  130. self.m=float(m)
  131. #create a Basis object (see statefuncs.py)
  132. #and set it as self.fullBasis
  133. #a Basis contains a list of states, each state defined by a set of
  134. #occupation numbers for each Fourier mode
  135. self.fullBasis[k] = Basis(L=self.L, Emax=Emax, m=self.m, K=k)
  136. def buildBasis(self,k,Emax):
  137. """
  138. Builds the Hilbert space basis for which the Hamiltonian to actually diagonalize
  139. is calculated (in general it's a subspace of fullBasis)
  140. Note that this is called in phi4eigs, but not in generating
  141. the original potential matrix and free Hamiltonian.
  142. """
  143. self.basis[k] = Basis(m=self.m, L=self.L, Emax=Emax, K=k, nmax=self.fullBasis[k].nmax)
  144. # We use the vector length (nmax) of the full basis. In this way we can compare elements between the two bases
  145. self.Emax = float(Emax)
  146. for nn in (0,2,4):
  147. self.V[k][nn] = self.potential[k][nn].sub(self.basis[k], self.basis[k]).M.tocoo()
  148. self.h0Sub[k] = self.h0[k].sub(self.basis[k],self.basis[k]).M.tocoo()
  149. def buildMatrix(self):
  150. """ Builds the full Hamiltonian in the basis of the free Hamiltonian eigenvectors.
  151. This is computationally intensive. It can be skipped by loading the matrix from file """
  152. """
  153. Possible speedups:
  154. Simplify the diagonal operator loops?
  155. Can we apply a numpy mask to simplify the loops without the
  156. conditionals?
  157. Basically this loops over the operators at each order in phi
  158. and stores all the normal order operators explicitly in lists.
  159. """
  160. L=self.L
  161. m=self.m
  162. for k in (1,-1):
  163. basis = self.fullBasis[k]
  164. lookupBasis = self.fullBasis[k]
  165. Emax = basis.Emax
  166. nmax = basis.nmax
  167. diagOps = {0: None, 2:None, 4:None}
  168. offdiagOps = {0: None, 2:None, 4:None}
  169. diagOps[0] = [ NOO([],[],L,m) ]
  170. offdiagOps[0] = []
  171. #the 2 is a combinatorial factor since both aa^dagger and a^dagger a contribute
  172. diagOps[2] = [ NOO([a],[a],L,m, extracoeff=2.) for a in range(-nmax,nmax+1) ]
  173. #the symmetry factor is 1 if a=-a and 2 otherwise
  174. offdiagOps[2] = [ NOO([a,-a],[],L,m,extracoeff=comb(a,-a))
  175. for a in range(-nmax,nmax+1) if a<=-a<=nmax and
  176. omega(a,L,m)+omega(-a,L,m) <= Emax+tol]
  177. # the default symmetry factor is 6 (4 choose 2) if a and b are distinct
  178. # and c, a+b-c are distinct
  179. # notice the index for b runs from a to nmax so we only get unique
  180. # pairs a and b, i.e. (1,1), (1,2), (1,3), (2,2), (2,3), (3,3).
  181. diagOps[4] = [ NOO([a,b],[c,a+b-c],L,m, extracoeff=6.*comb(a,b)*comb(c,a+b-c))
  182. for a in range(-nmax,nmax+1) for b in range (a,nmax+1)
  183. for c in range(-nmax,nmax+1) if
  184. ( c<=a+b-c<=nmax
  185. and (a,b) == (c,a+b-c)
  186. and -Emax-tol <= omega(a,L,m)+omega(b,L,m) - omega(c,L,m)-omega(a+b-c,L,m) <=Emax+tol)]
  187. offdiagOps[4] = [ NOO([a,b,c,-a-b-c],[],L,m,extracoeff=comb(a,b,c,-a-b-c))
  188. for a in range(-nmax,nmax+1) for b in range (a,nmax+1)
  189. for c in range(b,nmax+1) if c<=-a-b-c<=nmax and
  190. omega(a,L,m)+omega(b,L,m) + omega(c,L,m)+omega(-a-b-c,L,m)<= Emax+tol] \
  191. + [ NOO([a,b,c],[a+b+c],L,m, extracoeff = 4. * comb(a,b,c))
  192. for a in range(-nmax, nmax+1) for b in range (a,nmax+1)
  193. for c in range(b,nmax+1) if
  194. (-nmax<=a+b+c<=nmax
  195. and -Emax-tol <= omega(a,L,m)+omega(b,L,m)+ omega(c,L,m)-omega(a+b+c,L,m) <=Emax+tol)] \
  196. + [ NOO([a,b],[c,a+b-c],L,m, extracoeff = 6. * comb(a,b)*comb(c,a+b-c))
  197. for a in range(-nmax,nmax+1) for b in range (a,nmax+1)
  198. for c in range(-nmax,nmax+1) if
  199. ( c<=a+b-c<=nmax
  200. and (a,b) != (c,a+b-c)
  201. and sorted([abs(a),abs(b)]) < sorted([abs(c),abs(a+b-c)])
  202. and -Emax-tol <= omega(a,L,m)+omega(b,L,m)- omega(c,L,m)-omega(a+b-c,L,m) <=Emax+tol)]
  203. #save as h0 a lookupBasis.size by 1 sparse matrix initialized to zeros
  204. #really just a single column
  205. #and store the bases as h0[k].basisI and h0[k].basisJ
  206. #self.h0[k] = Matrix(lookupBasis, basis)
  207. tempEnergies = np.empty(basis.size)
  208. #initialTime = time.time()
  209. #this was xrange in the original; range in 3.x was xrange in 2.x -IL
  210. for j in range(basis.size):
  211. #make a new column of the appropriate length
  212. #newcolumn = scipy.zeros(lookupBasis.size)
  213. #set the jth entry in this column to be the energy of
  214. #the jth state in the basis
  215. #newcolumn[j] = basis[j].energy
  216. tempEnergies[j] = basis[j].energy
  217. #self.h0[k].addColumn(newcolumn)
  218. #self.h0[k].finalize()
  219. """
  220. basically this is just a diagonal matrix of the eigenvalues
  221. since the Hamiltonian h0 is diagonal in this basis this is faster.
  222. the loop adding columns takes 0.45711565017700195 s
  223. just creating the sparse matrix takes 0.000997304916381836 s.
  224. """
  225. temph0 = scipy.sparse.diags(tempEnergies,format="coo")
  226. self.h0[k] = Matrix(lookupBasis,basis,temph0)
  227. #ran some tests, doing scipy.sparse.diags does seem more straightforward
  228. #print(time.time()-initialTime)
  229. """
  230. We build the potential in this basis. self.potential is a
  231. dictionary with two keys corresponding to k=1 and k=-1.
  232. Each of the entries is a list of sparse matrices.
  233. Each sparse matrix consists of a sum of off-diagonal components
  234. and diagonal components in COO format.
  235. Possible speedup: could we apply each operator to all states
  236. in one shot? Then we would just loop over operators.
  237. Right now, for each state, we apply each operator one at a time
  238. and mark down its image in the corresponding column (the origin
  239. state) and row (the image state).
  240. If we apply an operator to a vector of states, we will get
  241. a vector of their images which can be collectively checked for
  242. validity by checking the dictionary keys/energy ranges, and
  243. then this is just the matrix form of the operator.
  244. """
  245. # for each order (0,2,4) in phi
  246. for n in offdiagOps.keys():
  247. offdiag_V = Matrix(lookupBasis, basis)
  248. diagonal = np.zeros(basis.size)
  249. # for each state in the basis
  250. for j in range(basis.size):
  251. newcolumn = np.zeros(lookupBasis.size)
  252. # for each off-diagonal operator at a given order
  253. for op in offdiagOps[n]:
  254. try:
  255. # apply this operator to find whether the
  256. # new state is still in the basis
  257. (x,i) = op.apply(basis,j,lookupBasis)
  258. # if so, add the corresponding value to the matrix
  259. # this is basically writing the effects of the
  260. # operator in the basis of the free states
  261. if(i != None):
  262. newcolumn[i]+=x
  263. except NotInBasis:
  264. pass
  265. offdiag_V.addColumn(newcolumn)
  266. # for each diagonal operator at the same order n
  267. for op in diagOps[n]:
  268. (x,i) = op.apply(basis,j,lookupBasis)
  269. # It should be j=i
  270. if i!= None:
  271. if i != j:
  272. raise RuntimeError('Non-diagonal operator')
  273. diagonal[i]+=x
  274. offdiag_V.finalize()
  275. diag_V = scipy.sparse.spdiags(diagonal,0,basis.size,basis.size)
  276. self.potential[k][n] = (offdiag_V+offdiag_V.transpose()+Matrix(lookupBasis, basis, diag_V)).to('coo')*self.L
  277. def saveMatrix(self, fname):
  278. """ Saves the free Hamiltonian and potential matrices to file """
  279. t = (fname, self.L, self.m, \
  280. self.fullBasis[1].Emax, self.fullBasis[1].nmax, \
  281. self.fullBasis[-1].Emax, self.fullBasis[-1].nmax, \
  282. self.h0[1].M.data,self.h0[1].M.row,self.h0[1].M.col, \
  283. self.potential[1][0].M.data,self.potential[1][0].M.row,self.potential[1][0].M.col, \
  284. self.potential[1][2].M.data,self.potential[1][2].M.row,self.potential[1][2].M.col, \
  285. self.potential[1][4].M.data,self.potential[1][4].M.row,self.potential[1][4].M.col, \
  286. self.h0[-1].M.data,self.h0[-1].M.row,self.h0[-1].M.col, \
  287. self.potential[-1][0].M.data,self.potential[-1][0].M.row,self.potential[-1][0].M.col, \
  288. self.potential[-1][2].M.data,self.potential[-1][2].M.row,self.potential[-1][2].M.col, \
  289. self.potential[-1][4].M.data,self.potential[-1][4].M.row,self.potential[-1][4].M.col \
  290. )
  291. scipy.savez(*t)
  292. def loadMatrix(self, fname):
  293. """ Loads the free Hamiltonian and potential matrices from file """
  294. f = scipy.load(fname)
  295. self.L = f['arr_0'].item()
  296. self.m = f['arr_1'].item()
  297. Emax = {1:f['arr_2'].item(), -1:f['arr_4'].item()}
  298. nmax = {1:f['arr_3'].item(), -1:f['arr_5'].item()}
  299. for i, k in enumerate((1,-1)):
  300. n = 12
  301. z = 6
  302. self.buildFullBasis(L=self.L, m=self.m, Emax=Emax[k], k=k)
  303. basisI = self.fullBasis[k]
  304. basisJ = self.fullBasis[k]
  305. self.h0[k] = Matrix(basisI, basisJ, scipy.sparse.coo_matrix((f['arr_'+(str(z+i*n))], (f['arr_'+(str(z+1+i*n))], f['arr_'+(str(z+2+i*n))])), shape=(basisI.size, basisJ.size)))
  306. self.potential[k][0] = Matrix(basisI, basisJ, scipy.sparse.coo_matrix((f['arr_'+(str(z+3+i*n))], (f['arr_'+(str(z+4+i*n))], f['arr_'+(str(z+5+i*n))])), shape=(basisI.size, basisJ.size)))
  307. self.potential[k][2] = Matrix(basisI, basisJ, scipy.sparse.coo_matrix((f['arr_'+(str(z+6+i*n))], (f['arr_'+(str(z+7+i*n))], f['arr_'+(str(z+8+i*n))])), shape=(basisI.size, basisJ.size)))
  308. self.potential[k][4] = Matrix(basisI, basisJ, scipy.sparse.coo_matrix((f['arr_'+(str(z+9+i*n))], (f['arr_'+(str(z+10+i*n))], f['arr_'+(str(z+11+i*n))])), shape=(basisI.size, basisJ.size)))
  309. def setcouplings(self, g4, g2=0.):
  310. self.g2 = float(g2)
  311. self.g4 = float(g4)
  312. def renlocal(self,Er):
  313. self.g0r, self.g2r, self.g4r = renorm.renlocal(self.g2,self.g4,self.Emax,Er)
  314. self.Er = Er
  315. def computeHamiltonian(self, k=1, ren=False):
  316. """ Computes the (renormalized) Hamiltonian to diagonalize
  317. k : K-parity quantum number
  318. ren : if True, computes the eigenvalue with the "local" renormalization procedure, otherwise the "raw" eigenvalues
  319. """
  320. if not(ren):
  321. self.H[k] = self.h0Sub[k] + self.V[k][2]*self.g2 + self.V[k][4]*self.g4
  322. else:
  323. self.H[k] = self.h0Sub[k] + self.V[k][0]*self.g0r + self.V[k][2]*self.g2r + self.V[k][4]*self.g4r
  324. def computeEigval(self, k=1, ren=False, corr=False, sigma=0, n=10):
  325. """ Diagonalizes the Hamiltonian and possibly computes the subleading renormalization corrections
  326. k (int): K-parity quantum number
  327. ren (bool): it should have the same value as the one passed to computeHamiltonian()
  328. corr (bool): if True, computes the subleading renormalization corrections, otherwise not.
  329. n (int): number of lowest eigenvalues to compute
  330. sigma : value around which the Lanczos method looks for the lowest eigenvalue.
  331. """
  332. if not ren:
  333. (self.eigenvalues[k], eigenvectorstranspose) = scipy.sparse.linalg.eigsh(self.H[k], k=n, sigma=sigma,
  334. which='LM', return_eigenvectors=True)
  335. else:
  336. (self.eigsrenlocal[k], eigenvectorstranspose) = scipy.sparse.linalg.eigsh(self.H[k], k=n, sigma=sigma,
  337. which='LM', return_eigenvectors=True)
  338. eigenvectors = eigenvectorstranspose.T
  339. if corr:
  340. print("Adding subleading corrections to k="+str(k), " eigenvalues")
  341. self.eigsrensubl[k] = np.zeros(n)
  342. cutoff = 5.
  343. for i in range(n):
  344. cbar = eigenvectors[i]
  345. if abs(sum([x*x for x in cbar])-1.0) > 10**(-13):
  346. raise RuntimeError('Eigenvector not normalized')
  347. Ebar = self.eigsrenlocal[k][i]
  348. self.eigsrensubl[k][i] += Ebar
  349. ktab, rentab = renorm.rensubl(self.g2, self.g4, Ebar, self.Emax, self.Er, cutoff=cutoff)
  350. tckren = { }
  351. tckren[0] = scipy.interpolate.interp1d(ktab,rentab.T[0],kind='linear')
  352. tckren[2] = scipy.interpolate.interp1d(ktab,rentab.T[1],kind='linear')
  353. tckren[4] = scipy.interpolate.interp1d(ktab,rentab.T[2],kind='linear')
  354. for nn in (0,2,4):
  355. #for a,b,Vab in itertools.izip(self.V[k][nn].row,self.V[k][nn].col,self.V[k][nn].data):
  356. for a,b,Vab in zip(self.V[k][nn].row,self.V[k][nn].col,self.V[k][nn].data):
  357. if a > b:
  358. continue
  359. elif a == b:
  360. c = 1
  361. else:
  362. c = 2
  363. Eab2= (self.basis[k][a].energy + self.basis[k][b].energy)/2.
  364. coeff = tckren[nn](Eab2)
  365. self.eigsrensubl[k][i] += c * coeff * cbar[a] * cbar[b] * Vab
  366. def vacuumE(self, ren="raw"):
  367. if ren=="raw":
  368. return self.eigenvalues[1][0]
  369. elif ren=="renlocal":
  370. return self.eigsrenlocal[1][0]
  371. elif ren=="rensubl":
  372. return self.eigsrensubl[1][0]
  373. else:
  374. raise ValueError("Wrong argument")
  375. # The vacuum is K-even
  376. def spectrum(self, k, ren="raw"):
  377. if ren=="raw":
  378. eigs = self.eigenvalues
  379. elif ren=="renlocal":
  380. eigs = self.eigsrenlocal
  381. elif ren=="rensubl":
  382. eigs = self.eigsrensubl
  383. else:
  384. raise ValueError("Wrong argument")
  385. # Now subtract vacuum energies
  386. if k==1:
  387. return scipy.array([x-self.vacuumE(ren=ren) for x in eigs[k][1:]])
  388. elif k==-1:
  389. return scipy.array([x-self.vacuumE(ren=ren) for x in eigs[k]])
  390. else:
  391. raise ValueError("Wrong argument")