Unexpected error in 2D quantum number conserving DMRG (with unusual "fractal" path)

Hi All,

I am trying to perform 2-D DMRG on a simple model (spinful free fermions) using a fractal “Hilbert curve” path as opposed to the more conventional “snake”. This strategy is proposed in [2507.11820v1] Fractal Path Strategies for Efficient 2D DMRG Simulations. The short description of the error is that combining S_z conservation with the choice of using a “Hilbert curve DMRG snake” leads to the LoadError: Eigen currently only supports block diagonal matrices. upon running the DMRG algorithm. If I use S_z conservation with a more conventional DMRG snake (the naive keyword in the code below) or if I use the Hilbert curve snake without S_z conservation, there is no error.

Given the above, I am sure that the error is not something silly like having OpSum terms that do not manifestly conserve the QN. I would have guessed that the long-range nature of the Hamiltonian (enforced by the Hilbert curve path) is responsible for this weird interaction with S_z conservation, but any 2D DMRG implementation should suffer from this issue.

Any advice is welcome, as well as any insight into how the QN functionality works and interacts with long-range interactions.

The code is provided at the very end in terms of functions; to run the computation, please use

nsweeps = 10, maxdim = 50;
conserve_sz = true #Toggle between false and true as needed
restart = false;
snaketype = "naive"; #Change to "hilbert" to test the fractal curve
nfl_spin_main(; Nx=4, Ny=4,  t_SING, filling, nsweeps, maxdim, threaded_blocksparse=false, restart, conserve_sz, snaketype);
using ITensors, ITensorMPS, ITensorInfiniteMPS
using LinearAlgebra
using Random

using Strided
using HDF5


function FL_spinful(; Nx::Int, Ny::Int, t_SING=0.0, yperiodic::Bool=true,snaketype="naive")
  N = Nx * Ny
  lattice = square_lattice(Nx, Ny; yperiodic=yperiodic,snaketype)
  opsum = OpSum()
  for b in lattice
    bondtype = bond_type(b,Ny)
    tmat = [1 0; 0 1]
    opsum -= t_SING*tmat[1,1], "Cdagup", b.s1, "Cup", b.s2
    opsum -= t_SING*tmat[1,2], "Cdagup", b.s1, "Cdn", b.s2
    opsum -= t_SING*tmat[2,1], "Cdagdn", b.s1, "Cup", b.s2
    opsum -= t_SING*tmat[2,2], "Cdagdn", b.s1, "Cdn", b.s2

    opsum -= conj(t_SING*tmat[1,1]), "Cdagup", b.s2, "Cup", b.s1
    opsum -= conj(t_SING*tmat[2,1]), "Cdagup", b.s2, "Cdn", b.s1
    opsum -= conj(t_SING*tmat[1,2]), "Cdagdn", b.s2, "Cup", b.s1
    opsum -= conj(t_SING*tmat[2,2]), "Cdagdn", b.s2, "Cdn", b.s1
  end


  return opsum
end

function square_lattice_snaked(Nx::Int, Ny::Int; yperiodic=false, snaketype="naive")::Lattice
    yperiodic = yperiodic && (Ny > 2)
    N = Nx * Ny
    Nbond = 2N - Ny + (yperiodic ? 0 : -Nx)
    latt = Lattice(undef, Nbond)
    b = 0
    for n in 1:N
      x, y = snake_to_xy(n, Nx, Ny, snaketype)
      if x < Nx
        latt[b += 1] = LatticeBond(n, xy_to_snake(x+1,y,Nx,Ny,snaketype), x, y, x + 1, y)
      end
      if Ny > 1
        if y < Ny
          latt[b += 1] = LatticeBond(n, xy_to_snake(x,y+1,Nx,Ny,snaketype), x, y, x, y + 1)
        end
        if yperiodic && y == 1
          #latt[b += 1] = LatticeBond(n, n + Ny - 1, x, y, x, y + Ny - 1)
          latt[b += 1] = LatticeBond(xy_to_snake(x,y+Ny-1,Nx,Ny,snaketype), n, x, y + Ny - 1, x, y)
        end
      end
    end
    
   


    return latt
end

And the Lattice and LatticeBond structs are as defined in ITensorMPS.jl/src/lattices/lattices.jl at e9650e5a2070690cd51ddc1f54644007189de537 · ITensor/ITensorMPS.jl · GitHub. The function to start the DMRG computation is below; the input snaketype is set to “naive” by default (how one usually does 2d DMRG) and one can set it to to “hilbert” when using the Hilbert space-filling curve.

function main(;
  Nx::Int=8,
  Ny::Int=4,
  t_SING::Float64=1.0,
  filling::Int=1,
  filling_plus_x::Int=0,
  maxdim::Int=300,
  conserve_ky=false,
  threaded_blocksparse=false,
  nsweeps=15,
  yperiodic=true,
  random_init=true,
  seed=1234,
  restart = false,
  conserve_sz = true,
  writing=true,
  n_preheating=0,
  snaketype="naive"
)
  # Helps make results reproducible when comparing
  # sequential vs. threaded.
  itensor_rng = Xoshiro()
  Random.seed!(itensor_rng, seed)

  @show Threads.nthreads()

  # Disable other threading
  BLAS.set_num_threads(1)
  Strided.set_num_threads(1)

  ITensors.enable_threaded_blocksparse(threaded_blocksparse)
  @show ITensors.using_threaded_blocksparse()

  N = Nx * Ny

  cutoff = [1e-6]
  noise = [1e-6, 1e-7, 1e-8, 0.0]





  sites = siteinds("Electron", N; conserve_qns=true, conserve_sz)

  os = NFL_spinful(; Nx, Ny, t,h,t_SING,U,U_NN, JH, yperiodic,snaketype)



  # Create starting state with checkerboard
  # pattern
  state = map(CartesianIndices((Ny,Nx))) do I #to handle filling 1+x, I may need to move away from "do" syntax. See Ryan Levy's comment...
    y=I[1]
    x=I[2]
    return (mod(x+y,filling)==0) ? (iseven(div(x+y,filling)) ? "Dn" : "Up") : "0"
  end
  display(state)

  sz_append = conserve_sz ? "_szcons" : ""

  restart_dirname  = "nfl_spinful_NN_filling_"*string(filling)*"_t_"*string(t)*"_h_"*string(h)*"_U_"*string(U)*"_UNN_"*string(U_NN)*"_JH_"*string(JH)*"_t_SING_"*string(t_SING)*"_Nx_"*string(Nx)*"_Ny_"*string(Ny)*sz_append
  restart_filename = restart_dirname*"/MPS_GS.h5"


  psi0 = if random_init
    random_mps(itensor_rng, sites, state; linkdims=2)
  else
    MPS(sites, state)
  end
  if restart && isdir(restart_dirname) && false
    f = h5open(restart_filename,"r")
    psi0 = read(f,"psi",MPS)
    sites = siteinds(psi0)
    psi0 = replace_siteinds(psi0, sites);
    close(f)
  end
  if restart && isdir(restart_dirname)
    psi0 = h5open(restart_filename,"r") do f
      read(f,"psi",MPS)
    end
    sites = siteinds(psi0)
    psi0 = replace_siteinds(psi0, sites);
    #close(f)
    #H = replace_siteinds(H, sites);
  end
  @show flux(psi0)
  @show maxlinkdim(psi0)
  H = MPO(os, sites)
    # Number of structural nonzero elements in a bulk
  # Hamiltonian MPO tensor
  @show nnz(H[end ÷ 2])
  @show nnzblocks(H[end ÷ 2])




  @time energy, psi = @time dmrg(H, psi0; nsweeps, maxdim,mindim, cutoff, noise)
  @show flux(psi)
  @show maxlinkdim(psi)

There is an additional block of code for defining the functions xy_to_snake and snake_to_xy, included below:


# Stores generated Gilbert curves. First entry is a pair (Nx, Ny), and second is the map defining the curve 
const _gilbertcache = IdDict{Tuple{Int,Int},  Vector{CartesianIndex{2}}  }()


"""
    gilbertindices(dims::Tuple{Int,Int}; majdim=dims[1] >= dims[2] ? 1 : 2)

Constructs a vector of `CartesianIndex` objects, orderd by a generalized Hilbert
space-filling, starting at `CartesianIndex(1,1)`.  It will end at
`CartesianIndex(dims[1],1)` if `majdim==1`, or `CartesianIndex(1,dims[2])` if `majdim==2`
(or the closest feasible point).
"""
gilbertindices(dims::Tuple{Int,Int}; kwargs...) =
    gilbertorder(CartesianIndices(dims); kwargs...)


"""
    gilbertorder(mat::AbstractMatrix; majdim=size(mat,1) >= size(mat,2) ? 1 : 2)

Constructs a vector of the elements of `mat`, ordered by a generalized Hilbert
space-filling curve. The list will start at `mat[1,1]`, and end at `mat[end,1]` if
`majdim==1` or `mat[1,end]` if `majdim==2`  (or the closest feasible point).
"""
function gilbertorder(mat::AbstractMatrix{T}; majdim=size(mat,1) >= size(mat,2) ? 1 : 2) where {T}
    list = sizehint!(T[], length(mat))
    if majdim == 1
        append_gilbert!(list, mat)
    else
        append_gilbert!(list, permutedims(mat,(2,1)))
    end
end

function append_gilbert!(list, mat::AbstractMatrix)
    # 1 |*    |
    #   | )   |
    # a |v    |
    a,b = size(mat)
    if a == 1 || b == 1
        # single in one dimension
        append!(list, mat)
    elseif 2a > 3b
        # long case: split into two
        #   +-----+
        # 1 |*    |
        #   ||    |
        # a2|v    |
        #   +-----+
        #   |*    |
        #   ||    |
        # a |v    |
        #   +-----+
        a2 = div(a,2)
        if isodd(a2) && a > 2
            a2 += 1
        end
        append_gilbert!(list, mat[1:a2,:])
        append_gilbert!(list, mat[a2+1:a,:])
    else
        # standard case: split into three
        #      b2
        #   +---+---+
        # 1 |*->|*   |
        #   |   ||   |
        # a2|   ||   |
        #   +---+|   |
        #   |   ||   |
        # a |<-*|v   |
        #   +---+----+
        a2 = div(a,2)
        b2 = div(b,2)
        if isodd(b2) && b > 2
            b2 += 1
        end
        append_gilbert!(list, permutedims(mat[1:a2,1:b2],(2,1)))
        append_gilbert!(list, mat[:,b2+1:b])
        append_gilbert!(list, permutedims(mat[a:-1:a2+1,b2:-1:1],(2,1)))
    end
end

"""
    linearindices(list::Vector{CartesianIndex{2}})

Construct an integer matrix `M` containing the integers `1:length(list)` such that
`M[list[i]] == i`.
"""
function linearindices(list::Vector{CartesianIndex{2}})
    cmax = maximum(list)
    L = zeros(Int,cmax.I)
    for (i,c) in enumerate(list)
        L[c] = i
    end
    return L
end



function xy_to_snake(x::Int,y::Int,Nx::Int,Ny::Int,snaketype::String)
  #Maps 2d coordinates onto a 1d snake
  #valid snake types: naive, naive_y, hilbert
  #naive_y and hilbert MUST have periodic set to false
  ind = 0
  if snaketype  == "naive"
    ind = y+(x-1)*Ny
  elseif snaketype == "naive_y"
    ind = x+(y-1)*Ny
  elseif snaketype == "hilbert"
    #ind = xy_to_hilbert(x, y, Nx, Ny)
    maplist = get!(_gilbertcache, (Nx, Ny)) do #Get or create the Hilbert map
    list = gilbertindices((Nx,Ny))
    end

    #mapmatrix = GilbertCurves.linearindices(maplist) #fold it into a matrix for ease
    mapmatrix = linearindices(maplist) #fold it into a matrix for ease
    ind = mapmatrix[x,y]
  else
    throw("Not a valid snake type")
  end
  return ind
end


function snake_to_xy(n::Int,Nx::Int,Ny::Int,snaketype::String)
  #Maps 1d snake onto a 2d coordinate
  #valid snake types: naive, naive_y, hilbert
  #naive_y and hilbert MUST have periodic set to false
  x=0
  y=0
  if snaketype  == "naive"
    x = div(n - 1, Ny) + 1
    y = mod(n - 1, Ny) + 1
  elseif snaketype == "naive_y"
    x = mod(n - 1, Nx) + 1
    y = div(n - 1, Nx) + 1
  elseif snaketype == "hilbert"
    #coords = hilbert_to_xy(n, Nx, Ny)
    #x = coords[1]
    #y=coords[2]

    maplist = get!(_gilbertcache, (Nx, Ny)) do #Get or create the Hilbert map
    list = gilbertindices((Nx,Ny))
    end

    cartindex = maplist[n]
    x=cartindex[1]
    y=cartindex[2]
  else
    throw("Not a valid snake type")
  end
  return x,y
end



Hi Amogh,
Is the error definitely coming from the OpSum to MPO conversion step? I ask because I haven’t seen a case before where the site ordering mattered for OpSum. Maybe switching the order is fixing the bug for a different reason.

What is the call stack like when you get the bug ie what function is it actually coming from?

Hi,

The stack trace is as follows:

LoadError: Eigen currently only supports block diagonal matrices.
Stacktrace:
  [1] error(s::String)
    @ Base ./error.jl:35
  [2] eigen(T::Hermitian{Float64, NDTensors.BlockSparseTensor{Float64, 2, Tuple{…}, NDTensors.BlockSparse{…}}}; min_blockdim::Nothing, mindim::Int64, maxdim::Int64, cutoff::Float64, use_absolute_cutoff::Nothing, use_relative_cutoff::Nothing)
    @ NDTensors ~/.julia/packages/NDTensors/F4sTz/src/blocksparse/linearalgebra.jl:230
  [3] eigen(A::ITensor, Linds::Vector{…}, Rinds::Vector{…}; mindim::Int64, maxdim::Int64, cutoff::Float64, use_absolute_cutoff::Nothing, use_relative_cutoff::Nothing, ishermitian::Bool, tags::ITensors.TagSets.GenericTagSet{…}, lefttags::Nothing, righttags::Nothing, plev::Nothing, leftplev::Nothing, rightplev::Nothing)
    @ ITensors ~/.julia/packages/ITensors/AOPBm/src/tensor_operations/matrix_decomposition.jl:378
  [4] factorize_eigen(A::ITensor, Linds::Tuple{…}; ortho::String, eigen_perturbation::ITensor, mindim::Int64, maxdim::Int64, cutoff::Float64, tags::ITensors.TagSets.GenericTagSet{…}, use_absolute_cutoff::Nothing, use_relative_cutoff::Nothing)
    @ ITensors ~/.julia/packages/ITensors/AOPBm/src/tensor_operations/matrix_decomposition.jl:681
  [5] factorize(A::ITensor, Linds::Tuple{…}; mindim::Int64, maxdim::Int64, cutoff::Float64, ortho::String, tags::ITensors.TagSets.GenericTagSet{…}, plev::Nothing, which_decomp::Nothing, eigen_perturbation::ITensor, svd_alg::Nothing, use_absolute_cutoff::Nothing, use_relative_cutoff::Nothing, min_blockdim::Nothing, singular_values!::Nothing, dir::Nothing)
    @ ITensors ~/.julia/packages/ITensors/AOPBm/src/tensor_operations/matrix_decomposition.jl:829
  [6] 
    @ ITensorMPS ~/.julia/packages/ITensorMPS/3OORN/src/mps.jl:563
  [7] replacebond!
    @ ~/.julia/packages/ITensorMPS/3OORN/src/mps.jl:534 [inlined]
  [8] #replacebond!#409
    @ ~/.julia/packages/ITensorMPS/3OORN/src/mps.jl:610 [inlined]
  [9] macro expansion
    @ ~/.julia/packages/ITensorMPS/3OORN/src/dmrg.jl:280 [inlined]
 [10] macro expansion
    @ ./timing.jl:395 [inlined]
 [11] 
    @ ITensorMPS ~/.julia/packages/ITensorMPS/3OORN/src/dmrg.jl:206
 [12] dmrg
    @ ~/.julia/packages/ITensorMPS/3OORN/src/dmrg.jl:158 [inlined]
 [13] #dmrg#509
    @ ~/.julia/packages/ITensorMPS/3OORN/src/dmrg.jl:28 [inlined]
 [14] dmrg
    @ ~/.julia/packages/ITensorMPS/3OORN/src/dmrg.jl:21 [inlined]
 [15] #dmrg#515
    @ ~/.julia/packages/ITensorMPS/3OORN/src/dmrg.jl:389 [inlined]
 [16] dmrg
    @ ~/.julia/packages/ITensorMPS/3OORN/src/dmrg.jl:379 [inlined]
 [17] macro expansion
    @ ./timing.jl:279 [inlined]
 [18] 
    @ Main ~/Desktop/Dipolar Symmetry Breaking/Exotic1D_Dipole/ChernDipole.jl:1338
 [19] top-level scope
    @ ~/Desktop/Dipolar Symmetry Breaking/Exotic1D_Dipole/SpinfulRunner.jl:24
 [20] include(fname::String)
    @ Base.MainInclude ./client.jl:489
 [21] top-level scope
    @ REPL[94]:1
in expression starting at /Users/2718281828/Desktop/Dipolar Symmetry Breaking/Exotic1D_Dipole/SpinfulRunner.jl:24
Some type information was truncated. Use `show(err)` to see complete types.

Within the code I’ve written, the line throwing the error is

@time energy, psi = @time dmrg(H, psi0; nsweeps, maxdim,mindim, cutoff, noise)

I.e. the step where the DMRG computation is attempted. (The other line SpinfulRunner.jl:24 just sets inputs and runs the main function.) The MPO conversion H = MPO(os, sites) seems to be fine.

Hi Amogh,

The error might be from a rogue “yperiodic=true” somewhere, I can get your code to run with both paths and Sz conservation when I manually set “yperiodic=false” everywhere (albeit with some other tinkering, I assumed FL_spinful is meant to be the same as NFL_spinful etc.). I don’t know what the interaction of the Sz conservation and cylindrical boundary conditions is, but only with cylindrical boundary conditions on Nx = Ny = 4 do I get the Eigen block diagonal error for both paths, not just Hilbert curve. I do however get the same error for Nx = Ny = 8 even for “yperiodic=false” on both paths, and no error when turning off Sz conservation, so there is something strange here.

I checked your Hilbert curve mapping against the one I used in the paper and we get the same energy despite the MPO having slight differences, so your implementation looks good and I don’t think there is an issue there. However, I will say that the improvement from fractal paths is very sensitive to the Hamiltonian used, and the groundstate energy from the zig-zag path (your “naive” path) and the snake path converge to a lower groundstate energy for your Hamiltonian for most “modest” bond dimensions. In a lot of cases, for the 4x4 square lattice the snake or zig-zag paths perform well, and sometimes better, than the Hilbert curve. This is not necessarily true for larger systems where the fractal paths start to standout, but I only have experience with the Heisenberg antiferromagnet so your mileage may vary.

Hope this helps!

1 Like

Hi,

I apologize for the error; indeed as @olibellwood pointed out there was a small error in the code snippet that would have prevented it from running without modification. For convenience I have fixed the function “main” below. This won’t fix the main issue, which persists as @olibellwood pointed out above, but it will make it more convenient to run the code.

function main(;
  Nx::Int=8,
  Ny::Int=4,
  t_SING::Float64=1.0,
  filling::Int=1,
  filling_plus_x::Int=0,
  maxdim::Int=300,
  conserve_ky=false,
  threaded_blocksparse=false,
  nsweeps=15,
  yperiodic=true,
  random_init=true,
  seed=1234,
  restart = false,
  conserve_sz = true,
  writing=true,
  n_preheating=0,
  snaketype="naive"
)
  # Helps make results reproducible when comparing
  # sequential vs. threaded.
  itensor_rng = Xoshiro()
  Random.seed!(itensor_rng, seed)

  @show Threads.nthreads()

  # Disable other threading
  BLAS.set_num_threads(1)
  Strided.set_num_threads(1)

  ITensors.enable_threaded_blocksparse(threaded_blocksparse)
  @show ITensors.using_threaded_blocksparse()

  N = Nx * Ny

  cutoff = [1e-6]
  noise = [1e-6, 1e-7, 1e-8, 0.0]





  sites = siteinds("Electron", N; conserve_qns=true, conserve_sz)

  os = FL_spinful(; Nx, Ny, t,h,t_SING,U,U_NN, JH, yperiodic,snaketype)



  # Create starting state with checkerboard
  # pattern
  state = map(CartesianIndices((Ny,Nx))) do I #to handle filling 1+x, I may need to move away from "do" syntax. See Ryan Levy's comment...
    y=I[1]
    x=I[2]
    return (mod(x+y,filling)==0) ? (iseven(div(x+y,filling)) ? "Dn" : "Up") : "0"
  end
  display(state)

  sz_append = conserve_sz ? "_szcons" : ""

  restart_dirname  = "nfl_spinful_NN_filling_"*string(filling)*"_t_"*string(t)*"_h_"*string(h)*"_U_"*string(U)*"_UNN_"*string(U_NN)*"_JH_"*string(JH)*"_t_SING_"*string(t_SING)*"_Nx_"*string(Nx)*"_Ny_"*string(Ny)*sz_append
  restart_filename = restart_dirname*"/MPS_GS.h5"


  psi0 = if random_init
    random_mps(itensor_rng, sites, state; linkdims=2)
  else
    MPS(sites, state)
  end
  if restart && isdir(restart_dirname) && false
    f = h5open(restart_filename,"r")
    psi0 = read(f,"psi",MPS)
    sites = siteinds(psi0)
    psi0 = replace_siteinds(psi0, sites);
    close(f)
  end
  if restart && isdir(restart_dirname)
    psi0 = h5open(restart_filename,"r") do f
      read(f,"psi",MPS)
    end
    sites = siteinds(psi0)
    psi0 = replace_siteinds(psi0, sites);
    #close(f)
    #H = replace_siteinds(H, sites);
  end
  @show flux(psi0)
  @show maxlinkdim(psi0)
  H = MPO(os, sites)
    # Number of structural nonzero elements in a bulk
  # Hamiltonian MPO tensor
  @show nnz(H[end ÷ 2])
  @show nnzblocks(H[end ÷ 2])




  @time energy, psi = @time dmrg(H, psi0; nsweeps, maxdim,mindim, cutoff, noise)
  @show flux(psi)
  @show maxlinkdim(psi)

Rant

Even with your most recent main function, there were some changes I had to make to get this to work. I cannot stress enough how much more likely you are to get help if you post a working minimal example and put in some effort to eliminate code orthogonal to the problem at hand. Examples:

I installed the unregistered ITensorInfiniteMPS package, only to later realize it was not actually required.

There was an error involving the restart logic, which was extrenuous in the first place. Deleting the code resolved this particular error.

Actual comment

After making the hopefully not breaking changes required to get things to run I got the Eigen currently only supports block diagonal matrices. error for both the naive and hilbert mappings with spin conservation. Commenting out the terms in the OpSum that couple spin-up and spin-down electrons (which did not preserve the spin but had zero coefficient) fixed the problem.

This seems like a bug in the MPO construction algorithm, which I thought would error out with Hamiltonian terms with non-zero quantum number flux.

function FL_spinful(; Nx::Int, Ny::Int, t_SING=0.0, yperiodic::Bool=true,snaketype="naive")
  N = Nx * Ny
  lattice = square_lattice_snaked(Nx, Ny; yperiodic=yperiodic,snaketype)
  opsum = OpSum()
  for b in lattice
    tmat = [1 0; 0 1]
    opsum -= t_SING*tmat[1,1], "Cdagup", b.s1, "Cup", b.s2
    # opsum -= t_SING*tmat[1,2], "Cdagup", b.s1, "Cdn", b.s2
    # opsum -= t_SING*tmat[2,1], "Cdagdn", b.s1, "Cup", b.s2
    opsum -= t_SING*tmat[2,2], "Cdagdn", b.s1, "Cdn", b.s2

    opsum -= conj(t_SING*tmat[1,1]), "Cdagup", b.s2, "Cup", b.s1
    # opsum -= conj(t_SING*tmat[2,1]), "Cdagup", b.s2, "Cdn", b.s1
    # opsum -= conj(t_SING*tmat[1,2]), "Cdagdn", b.s2, "Cup", b.s1
    opsum -= conj(t_SING*tmat[2,2]), "Cdagdn", b.s2, "Cdn", b.s1
  end


  return opsum
end

Hi @corbett5,

Thanks so much for the fix! It resolves everything. Sorry about the faulty code; I’ll address that next time.

Thanks again,

Amogh

1 Like

Sounds like we should try to catch that kind of mistake earlier in the MPO construction code.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.