Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/src/assets/tnrkit.bib
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,13 @@ @article{Evenbly_2018
year = {2018},
month = {aug}
}

@article{ueda_2025,
title={Global Tensor Network Renormalization for 2D Quantum systems: A new window to probe universal data from thermal transitions},
author={Atsushi Ueda and Sander De Meyer and Adwait Naravane and Victor Vanthilt and Frank Verstraete},
year={2025},
eprint={2508.05406},
archivePrefix={arXiv},
primaryClass={cond-mat.str-el},
url={https://arxiv.org/abs/2508.05406},
}
4 changes: 4 additions & 0 deletions src/TNRKit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ include("schemes/impurityhotrg.jl")
# Correlation methods
include("schemes/correlationhotrg.jl")

#Thermal TNR
include("schemes/ttnr.jl")
# Loop Methods
include("schemes/looptnr.jl")
include("schemes/symmetric_looptnr.jl")
Expand All @@ -62,6 +64,8 @@ export HOTRG
export HOTRG_3D
export ATRG
export ATRG_3D
export TNO, TNOTensor
export ThermalTNR, apply!
Comment on lines +67 to +68
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rethink exports. Does the user need apply!, TNO, TNOTensor ?


export CTM
export Sublattice_CTM
Expand Down
309 changes: 309 additions & 0 deletions src/schemes/ttnr.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
"""
TNOTensor{E,S}

Type alias for a local tensor-network-operator tensor with two physical legs and four
virtual legs, i.e. an `AbstractTensorMap{E,S,2,4}`.

Following the PEPO convention used in PEPSKit, the local tensor is interpreted as
`P_out ⊗ P_in <- N ⊗ E ⊗ S ⊗ W`.
"""
const TNOTensor{E, S} = AbstractTensorMap{E, S, 2, 4}

"""
$(TYPEDEF)

Matrix-like container for a tensor network operator (TNO) unit cell.

The entries are local operator tensors with two physical indices and four virtual
indices. The container behaves like an `AbstractMatrix`, so it supports `size`,
`axes`, indexing and iteration.

### Constructors
$(FUNCTIONNAME)(A::AbstractMatrix{<:TNOTensor})
$(FUNCTIONNAME)(A::TNOTensor; unitcell=(1, 1))

### Fields

$(TYPEDFIELDS)
"""
struct TNO{E, S, TT <: TNOTensor{E, S}} <: AbstractMatrix{TT}
"Matrix of local TNO tensors over the unit cell."
A::Matrix{TT}

function TNO(A::AbstractMatrix{TT}) where {E, S, TT <: TNOTensor{E, S}}
size(A, 1) > 0 || throw(ArgumentError("The unit cell must have a positive number of rows."))
size(A, 2) > 0 || throw(ArgumentError("The unit cell must have a positive number of columns."))
return new{E, S, TT}(Matrix(A))
end
end

function TNO(A::TT; unitcell::Tuple{Int, Int} = (1, 1)) where {E, S, TT <: TNOTensor{E, S}}
rows, cols = unitcell
rows > 0 || throw(ArgumentError("The unit cell must have a positive number of rows."))
cols > 0 || throw(ArgumentError("The unit cell must have a positive number of columns."))
return TNO([copy(A) for _ in 1:rows, _ in 1:cols])
end

Base.IndexStyle(::Type{<:TNO}) = IndexCartesian()
Base.eltype(::Type{TNO{E, S, TT}}) where {E, S, TT} = TT
Base.size(tno::TNO) = size(tno.A)
Base.axes(tno::TNO) = axes(tno.A)
Base.getindex(tno::TNO, i::Int, j::Int) = tno.A[i, j]
Base.setindex!(tno::TNO, value, i::Int, j::Int) = setindex!(tno.A, value, i, j)
Base.copy(tno::TNO) = TNO(copy(tno.A))

"""
$(TYPEDEF)

Minimal storage object for thermal tensor network renormalization on a square-lattice
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is a stylistic break from all the other scheme's docstrings. Please fix

TNO.

### Constructors
$(FUNCTIONNAME)(T::TNO)

### Running the algorithm
run!(::ThermalTNR, T::TNO, trunc::TruncationStrategy, criterion::stopcrit[
, finalizer=default_Finalizer, finalize_beginning=true, verbosity=1])

### Fields

$(TYPEDFIELDS)

### References
* [Ueda et al. (2025)](@cite ueda_2025)
"""
mutable struct ThermalTNR{E, S} <: TNRScheme{E, S}
"Tensor network operator stored in the current TTNR layer."
T::TNO{E, S}

function ThermalTNR(T::TT) where {E, S, TT <: TNO{E, S}}
return new{E, S}(T)
end
end

const _TNO_NORTH_AXIS = 3
const _TNO_EAST_AXIS = 4
const _TNO_SOUTH_AXIS = 5
const _TNO_WEST_AXIS = 6

@inline _right_index(j, ncols) = mod1(j + 1, ncols)
@inline _down_index(i, nrows) = mod1(i + 1, nrows)
@inline _up_index(i, nrows) = mod1(i - 1, nrows)

function _check_tno_bond_structure(tno::TNO)
nrows, ncols = size(tno)

for i in 1:nrows, j in 1:ncols
T = tno[i, j]
T_right = tno[i, _right_index(j, ncols)]
T_down = tno[_down_index(i, nrows), j]

space(T, _TNO_EAST_AXIS) == space(T_right, _TNO_WEST_AXIS)' ||
throw(ArgumentError("East-west bond mismatch between sites ($i, $j) and ($i, $(_right_index(j, ncols)))."))

space(T, _TNO_SOUTH_AXIS) == space(T_down, _TNO_NORTH_AXIS)' ||
throw(ArgumentError("North-south bond mismatch between sites ($i, $j) and ($(_down_index(i, nrows)), $j)."))
end

return nothing
end

function _check_tno_compatibility(top::TNO, bottom::TNO)
size(top) == size(bottom) ||
throw(ArgumentError("The two TNOs must have the same unit-cell dimensions."))

_check_tno_bond_structure(top)
_check_tno_bond_structure(bottom)

for i in axes(top, 1), j in axes(top, 2)
T_top = top[i, j]
T_bottom = bottom[i, j]

space(T_top, 2) == space(T_bottom, 1)' ||
throw(ArgumentError("Physical output/input mismatch at site ($i, $j)."))
end

return nothing
end

function QR_two_pepo_left(O1::TNOTensor, O2::TNOTensor, ind::Int)
pb = (1, ind)
p, q1 = ind_pair(O1, pb)
_, Rb = left_orth(permute(O1, (q1, p)))

pt = (2, ind)
p, q2 = ind_pair(O2, pt)
_, Rt = left_orth(permute(O2, (q2, p)))

@tensor M[-1 -2; -3 -4] := Rt[-3; 1 -1] * Rb[-4; 1 -2]
_, R = left_orth(permute(M, ((3, 4), (1, 2))))
return R
end

function QR_two_pepo_right(O1::TNOTensor, O2::TNOTensor, ind::Int)
pb = (1, ind)
p, q1 = ind_pair(O1, pb)
Rb, _ = right_orth(permute(O1, (p, q1)))

pt = (2, ind)
p, q2 = ind_pair(O2, pt)
Rt, _ = right_orth(permute(O2, (p, q2)))

@tensor M[-1 -2; -3 -4] := Rt[1 -1; -3] * Rb[1 -2; -4]
R, _ = right_orth(permute(M, ((1, 2), (3, 4))))
return R
end

function QR_two_pepo(O1::TNOTensor, O2::TNOTensor, ind::Int; side = :left)
if side == :left
return QR_two_pepo_left(O1, O2, ind)
elseif side == :right
return QR_two_pepo_right(O1, O2, ind)
else
throw(ArgumentError("side should be :left or :right"))
end
end

function R1R2(
A1::TNOTensor, A2::TNOTensor, A3::TNOTensor, A4::TNOTensor,
ind1::Int, ind2::Int; check_space = true
)
RA1 = QR_two_pepo_left(A1, A2, ind1)
RA2 = QR_two_pepo_right(A3, A4, ind2)
if check_space && domain(RA1) != codomain(RA2)
throw(ArgumentError("space mismatch"))
end
return RA1, RA2
end

function find_P1P2(
A1::TNOTensor, A2::TNOTensor, A3::TNOTensor, A4::TNOTensor,
p1::Int, p2::Int, trunc::TruncationStrategy; check_space = true
)
R1, R2 = R1R2(A1, A2, A3, A4, p1, p2; check_space = check_space)
return oblique_projector(R1, R2, trunc)
end

function _bond_projectors(top::TNO, bottom::TNO, trunc::TruncationStrategy)
nrows, ncols = size(top)
north_proj = Matrix{Any}(undef, nrows, ncols)
east_proj = Matrix{Any}(undef, nrows, ncols)
south_proj = Matrix{Any}(undef, nrows, ncols)
west_proj = Matrix{Any}(undef, nrows, ncols)

for i in 1:nrows, j in 1:ncols
inorth = _up_index(i, nrows)
jeast = _right_index(j, ncols)

Pnorth, Psouth = find_P1P2(
top[i, j], bottom[i, j], top[inorth, j], bottom[inorth, j], 3, 5, trunc
)
Peast, Pwest = find_P1P2(
top[i, j], bottom[i, j], top[i, jeast], bottom[i, jeast], 4, 6, trunc
)

north_proj[i, j] = Pnorth
south_proj[inorth, j] = Psouth
east_proj[i, j] = Peast
west_proj[i, jeast] = Pwest
end

return north_proj, east_proj, south_proj, west_proj
end

function _compose_local_tno(top::TNOTensor, bottom::TNOTensor, Pnorth, Peast, Psouth, Pwest)

@tensor merged[-1 -2; -3 -4 -5 -6] :=
top[1 -2; 7 8 9 10] *
bottom[-1 1; 3 4 5 6] *
Pwest[-6; 6 10] *
Pnorth[3 7; -3] *
Peast[4 8; -4] *
Psouth[-5; 5 9]

return merged
end

"""
apply!(top::TNO, bottom::TNO, trunc::TruncationStrategy)

Compose two TNO layers sitewise. The physical input leg of `top` is contracted with the
physical output leg of `bottom`, after which each pair of corresponding virtual bonds is
merged into a single fused bond.

The result is returned as a new `TNO` with the same unit-cell shape.
"""
function apply!(top::TNO, bottom::TNO, trunc::TruncationStrategy)
_check_tno_compatibility(top, bottom)
north_proj, east_proj, south_proj, west_proj = _bond_projectors(top, bottom, trunc)
merged = [
_compose_local_tno(
top[i, j], bottom[i, j],
north_proj[i, j], east_proj[i, j], south_proj[i, j], west_proj[i, j]
) for
i in axes(top, 1), j in axes(top, 2)
]
return TNO(merged)
end

function apply!(top::ThermalTNR, bottom::ThermalTNR, trunc::TruncationStrategy)
top.T = apply!(top.T, bottom.T, trunc)
return top
end

function step!(scheme::ThermalTNR, layer::TNO, trunc::TruncationStrategy)
scheme.T = apply!(scheme.T, layer, trunc)
return scheme
end

function step!(scheme::ThermalTNR, layer::ThermalTNR, trunc::TruncationStrategy)
return step!(scheme, layer.T, trunc)
end

function run!(
scheme::ThermalTNR, layer::TNO, trscheme::TruncationStrategy,
criterion::stopcrit, finalizer::Finalizer{E};
finalize_beginning = true, verbosity = 1
) where {E}
data = Vector{E}()

LoggingExtras.withlevel(; verbosity) do
@infov 1 "Starting simulation\n $(scheme)\n"
if finalize_beginning
push!(data, finalizer.f!(scheme))
end

steps = 0
crit = true

t = @elapsed while crit
@infov 2 "Step $(steps + 1), data[end]: $(!isempty(data) ? data[end] : "empty")"
step!(scheme, layer, trscheme)
push!(data, finalizer.f!(scheme))

steps += 1
crit = criterion(steps, data)
end

@infov 1 "Simulation finished\n $(stopping_info(criterion, steps, data))\n Elapsed time: $(t)s\n Iterations: $steps"
end
return data
end

function run!(
scheme::ThermalTNR, layer::ThermalTNR, trscheme::TruncationStrategy,
criterion::stopcrit, finalizer::Finalizer{E}; kwargs...
) where {E}
return run!(scheme, layer.T, trscheme, criterion, finalizer; kwargs...)
end

function run!(scheme::ThermalTNR, layer, trscheme, criterion; kwargs...)
return run!(scheme, layer, trscheme, criterion, default_Finalizer; kwargs...)
end

function Base.show(io::IO, scheme::ThermalTNR)
println(io, "ThermalTNR - Thermal Tensor Network Renormalization")
println(io, " * T: $(summary(scheme.T))")
println(io, " * unit cell: $(size(scheme.T))")
return nothing
end
11 changes: 11 additions & 0 deletions src/utility/finalize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ function finalize!(scheme::HOTRG_3D)
return n
end

function finalize!(scheme::ThermalTNR)
log_norm_sum = 0.0
for i in axes(scheme.T, 1), j in axes(scheme.T, 2)
T = scheme.T[i, j]
n = norm(@tensor T[1 1; 2 3 2 3])
scheme.T[i, j] /= n
log_norm_sum += log(n)
end
return exp(log_norm_sum / length(scheme.T))
end

function finalize!(scheme::SLoopTNR)
tr_norm = trnorm_2x2(scheme.T)
scheme.T /= tr_norm^0.25
Expand Down
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ include("schemes_honeycomb.jl") # do they give the correct results (with the exp
include("models.jl") # do they give the correct results (with the expected accuracy)?
include("fermions.jl") # do they give the correct results (with the expected accuracy)?
include("entropies.jl") # do they work?
include("ttnr.jl") # do ThermalTNR and TNO behave as expected?
include("algebras.jl")
Loading
Loading