Skip to content

Commit 629b79c

Browse files
mtfishmanclaude
andauthored
Use NDTensors.with_auto_fermion in ITensorsExtensions and tests (#340)
## Summary Adopt the new exception-safe scope wrapper `NDTensors.with_auto_fermion` (added in `NDTensors v0.4.25`), replacing the manual `ITensors.enable_auto_fermion()` / `disable_auto_fermion()` save-restore patterns in `ITensorsExtensions.map_eigvals` and across four test files. The wrapper restores the auto-fermion state via `try` / `finally`, fixing exception-safety bugs at callsites where a thrown exception would have left the flag flipped, and also fixing one logic bug in a test that always re-enabled the flag instead of restoring its prior state. ## Changes ### Source - `src/lib/ITensorsExtensions/src/itensor.jl::map_eigvals`: replace the manual disable/enable around the eigendecomposition with a single `NDTensors.with_auto_fermion(!fermionic_itensor) do … end` do-block. Numerical output is bit-identical across all four (UAF × has-fermionic-subspaces) cases on the existing test suite plus a Case-3 differential test. ### Tests - `test/test_opsum_to_ttn.jl` ("Multiple onsite terms"): wrap with `with_auto_fermion()`. Also fixes a real bug in the previous save-restore: the conditional only re-enabled the flag if it was already enabled at entry, so a run with the flag originally off would leave it flipped on. - `test/test_itensorsextensions.jl` ("Fermionic eigendecomp", "Fermionic map eigvals tests"): wrap each fermionic testset with `with_auto_fermion()`. - `test/test_opsum_to_ttn_mpo_cross_check.jl`: wrap "OpSum to TTN Fermions" with `with_auto_fermion()`. Remove a vestigial save/restore from "OpSum to TTN" (the testset never actually toggled the flag, so the captured `auto_fermion_enabled` and the conditional restore were dead code). - `test/test_ttn_position.jl` ("ProjTTN position"): fold the `if use_qns; enable; else; disable; end` conditional into `with_auto_fermion(use_qns) do … end`. ### Version - Patch bump `0.19.0` → `0.19.1`. ## Out of scope The four read-only `using_auto_fermion()` queries that gate logic in package source (`ITensorsExtensions/itensor.jl:63`, `opsum_to_ttn.jl:225/382/581`) are state queries, not state changes; converting them would require larger algorithm-level changes (threading a fermion-mode parameter through call signatures). Left unchanged here. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6e3c3b3 commit 629b79c

6 files changed

Lines changed: 159 additions & 168 deletions

File tree

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "ITensorNetworks"
22
uuid = "2919e153-833c-4bdc-8836-1ea460a35fc7"
3-
version = "0.19.0"
3+
version = "0.19.1"
44
authors = ["Matthew Fishman <mfishman@flatironinstitute.org>, Joseph Tindall <jtindall@flatironinstitute.org> and contributors"]
55

66
[workspace]

src/lib/ITensorsExtensions/src/itensor.jl

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,21 +64,18 @@ function map_eigvals(f::Function, A::ITensor, Linds, Rinds; kws...)
6464
if fermionic_itensor
6565
A = make_bosonic(A::ITensor, Linds, Rinds)
6666
ordered_inds = inds(A)
67-
ITensors.disable_auto_fermion()
6867
end
6968

70-
if isdiag(A)
71-
mapped_A = map_diag(f, A)
72-
else
69+
mapped_A = NDTensors.with_auto_fermion(!fermionic_itensor) do
70+
isdiag(A) && return map_diag(f, A)
7371
Ul, D, Ur = eigendecomp(A, Linds, Rinds; kws...)
74-
mapped_A = Ul * map_diag(f, D) * Ur
72+
return Ul * map_diag(f, D) * Ur
7573
end
7674

7775
# <fermions>
7876
if fermionic_itensor
7977
# Ensure indices in "matrix" form before re-enabling fermion system
8078
mapped_A = permute(mapped_A, ordered_inds)
81-
ITensors.enable_auto_fermion()
8279
end
8380

8481
return mapped_A

test/test_itensorsextensions.jl

Lines changed: 69 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
@eval module $(gensym())
22
using ITensorNetworks.ITensorsExtensions: eigendecomp, map_eigvals
3-
using ITensors: ITensors, ITensor, Index, QN, apply, dag, delta, inds, mapprime, noprime,
4-
norm, op, permute, prime, random_itensor, replaceind, replaceinds, sim, swapprime
3+
using ITensors.NDTensors: with_auto_fermion
4+
using ITensors: ITensor, Index, QN, apply, dag, delta, inds, mapprime, noprime, norm, op,
5+
permute, prime, random_itensor, replaceind, replaceinds, sim, swapprime
56
using StableRNGs: StableRNG
67
using Test: @test, @testset
78
@testset "ITensorsExtensions" begin
@@ -61,64 +62,75 @@ using Test: @test, @testset
6162
end
6263

6364
@testset "Fermionic eigendecomp" begin
64-
ITensors.enable_auto_fermion()
65-
s1 = Index([QN("Nf", 0, -1) => 2, QN("Nf", 1, -1) => 2], "Site,Fermion,n=1")
66-
s2 = Index([QN("Nf", 0, -1) => 2, QN("Nf", 1, -1) => 2], "Site,Fermion,n=2")
67-
68-
# Make a random Hermitian matrix-like 4th order ITensor
69-
T = random_itensor(s1', s2', dag(s2), dag(s1))
70-
T = apply(T, swapprime(dag(T), 0 => 1))
71-
@test T swapprime(dag(T), 0 => 1) # check Hermitian
72-
73-
Ul, D, Ur = eigendecomp(T, [s1', s2'], [dag(s1), dag(s2)]; ishermitian = true)
74-
75-
@test Ul * D * Ur T
76-
ITensors.disable_auto_fermion()
65+
with_auto_fermion() do
66+
s1 = Index(
67+
[QN("Nf", 0, -1) => 2, QN("Nf", 1, -1) => 2], "Site,Fermion,n=1"
68+
)
69+
s2 = Index(
70+
[QN("Nf", 0, -1) => 2, QN("Nf", 1, -1) => 2], "Site,Fermion,n=2"
71+
)
72+
73+
# Make a random Hermitian matrix-like 4th order ITensor
74+
T = random_itensor(s1', s2', dag(s2), dag(s1))
75+
T = apply(T, swapprime(dag(T), 0 => 1))
76+
@test T swapprime(dag(T), 0 => 1) # check Hermitian
77+
78+
Ul, D, Ur = eigendecomp(T, [s1', s2'], [dag(s1), dag(s2)]; ishermitian = true)
79+
80+
@test Ul * D * Ur T
81+
end
7782
end
7883

7984
@testset "Fermionic map eigvals tests" begin
80-
ITensors.enable_auto_fermion()
81-
s1 = Index([QN("Nf", 0, -1) => 2, QN("Nf", 1, -1) => 2], "Site,Fermion,n=1")
82-
s2 = Index([QN("Nf", 0, -1) => 2, QN("Nf", 1, -1) => 2], "Site,Fermion,n=2")
83-
84-
# Make a random Hermitian matrix ITensor
85-
M = random_itensor(s1', dag(s1))
86-
#M = mapprime(prime(M)*swapprime(dag(M),0=>1),2=>1)
87-
M = apply(M, swapprime(dag(M), 0 => 1))
88-
89-
# Make a random Hermitian matrix-like 4th order ITensor
90-
T = random_itensor(s1', s2', dag(s2), dag(s1))
91-
T = apply(T, swapprime(dag(T), 0 => 1))
92-
93-
# Matrix test
94-
sqrtM = map_eigvals(sqrt, M, [s1'], [dag(s1)]; ishermitian = true)
95-
@test M apply(sqrtM, sqrtM)
96-
97-
## Tensor test
98-
sqrtT = map_eigvals(sqrt, T, [s1', s2'], [dag(s1), dag(s2)]; ishermitian = true)
99-
@test T apply(sqrtT, sqrtT)
100-
101-
# Permute and test again
102-
T = permute(T, dag(s2), s2', dag(s1), s1')
103-
sqrtT = map_eigvals(sqrt, T, [s1', s2'], [dag(s1), dag(s2)]; ishermitian = true)
104-
@test T apply(sqrtT, sqrtT)
105-
106-
## Explicitly passing indices in different, valid orders
107-
sqrtT = map_eigvals(sqrt, T, [s2', s1'], [dag(s2), dag(s1)]; ishermitian = true)
108-
@test T apply(sqrtT, sqrtT)
109-
sqrtT = map_eigvals(sqrt, T, [dag(s2), dag(s1)], [s2', s1'], ; ishermitian = true)
110-
@test T apply(sqrtT, sqrtT)
111-
sqrtT = map_eigvals(sqrt, T, [dag(s1), dag(s2)], [s1', s2'], ; ishermitian = true)
112-
@test T apply(sqrtT, sqrtT)
113-
114-
# Test bosonic index case while fermion system is enabled
115-
b = Index([QN("Nb", 0) => 2, QN("Nb", 1) => 2])
116-
T = random_itensor(b', dag(b))
117-
T = apply(T, swapprime(dag(T), 0 => 1))
118-
sqrtT = map_eigvals(sqrt, T, [b'], [dag(b)]; ishermitian = true)
119-
@test T apply(sqrtT, sqrtT)
120-
121-
ITensors.disable_auto_fermion()
85+
with_auto_fermion() do
86+
s1 = Index(
87+
[QN("Nf", 0, -1) => 2, QN("Nf", 1, -1) => 2], "Site,Fermion,n=1"
88+
)
89+
s2 = Index(
90+
[QN("Nf", 0, -1) => 2, QN("Nf", 1, -1) => 2], "Site,Fermion,n=2"
91+
)
92+
93+
# Make a random Hermitian matrix ITensor
94+
M = random_itensor(s1', dag(s1))
95+
#M = mapprime(prime(M)*swapprime(dag(M),0=>1),2=>1)
96+
M = apply(M, swapprime(dag(M), 0 => 1))
97+
98+
# Make a random Hermitian matrix-like 4th order ITensor
99+
T = random_itensor(s1', s2', dag(s2), dag(s1))
100+
T = apply(T, swapprime(dag(T), 0 => 1))
101+
102+
# Matrix test
103+
sqrtM = map_eigvals(sqrt, M, [s1'], [dag(s1)]; ishermitian = true)
104+
@test M apply(sqrtM, sqrtM)
105+
106+
## Tensor test
107+
sqrtT = map_eigvals(sqrt, T, [s1', s2'], [dag(s1), dag(s2)]; ishermitian = true)
108+
@test T apply(sqrtT, sqrtT)
109+
110+
# Permute and test again
111+
T = permute(T, dag(s2), s2', dag(s1), s1')
112+
sqrtT = map_eigvals(sqrt, T, [s1', s2'], [dag(s1), dag(s2)]; ishermitian = true)
113+
@test T apply(sqrtT, sqrtT)
114+
115+
## Explicitly passing indices in different, valid orders
116+
sqrtT = map_eigvals(sqrt, T, [s2', s1'], [dag(s2), dag(s1)]; ishermitian = true)
117+
@test T apply(sqrtT, sqrtT)
118+
sqrtT = map_eigvals(
119+
sqrt, T, [dag(s2), dag(s1)], [s2', s1'], ; ishermitian = true
120+
)
121+
@test T apply(sqrtT, sqrtT)
122+
sqrtT = map_eigvals(
123+
sqrt, T, [dag(s1), dag(s2)], [s1', s2'], ; ishermitian = true
124+
)
125+
@test T apply(sqrtT, sqrtT)
126+
127+
# Test bosonic index case while fermion system is enabled
128+
b = Index([QN("Nb", 0) => 2, QN("Nb", 1) => 2])
129+
T = random_itensor(b', dag(b))
130+
T = apply(T, swapprime(dag(T), 0 => 1))
131+
sqrtT = map_eigvals(sqrt, T, [b'], [dag(b)]; ishermitian = true)
132+
@test T apply(sqrtT, sqrtT)
133+
end
122134
end
123135
end
124136
end

test/test_opsum_to_ttn.jl

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,26 @@
11
@eval module $(gensym())
22
using Graphs: vertices
33
using ITensorNetworks: ITensorNetworks, OpSum, siteinds, ttn
4-
using ITensors: ITensors
4+
using ITensors.NDTensors: with_auto_fermion
55
using NamedGraphs.NamedGraphGenerators: named_grid
66
using Test: @test, @testset
77

88
@testset "OpSum to TTN converter" begin
99
@testset "Multiple onsite terms (regression test for issue #62)" begin
10-
auto_fermion_enabled = ITensors.using_auto_fermion()
11-
if !auto_fermion_enabled
12-
ITensors.enable_auto_fermion()
13-
end
14-
grid_dims = (2, 1)
15-
g = named_grid(grid_dims)
16-
s = siteinds("S=1/2", g)
10+
with_auto_fermion() do
11+
grid_dims = (2, 1)
12+
g = named_grid(grid_dims)
13+
s = siteinds("S=1/2", g)
1714

18-
os1 = OpSum()
19-
os1 += 1.0, "Sx", (1, 1)
20-
os2 = OpSum()
21-
os2 += 1.0, "Sy", (1, 1)
22-
H1 = ttn(os1, s)
23-
H2 = ttn(os2, s)
24-
H3 = ttn(os1 + os2, s)
15+
os1 = OpSum()
16+
os1 += 1.0, "Sx", (1, 1)
17+
os2 = OpSum()
18+
os2 += 1.0, "Sy", (1, 1)
19+
H1 = ttn(os1, s)
20+
H2 = ttn(os2, s)
21+
H3 = ttn(os1 + os2, s)
2522

26-
@test H1 + H2 H3 rtol = 1.0e-6
27-
if auto_fermion_enabled
28-
ITensors.enable_auto_fermion()
23+
@test H1 + H2 H3 rtol = 1.0e-6
2924
end
3025
end
3126
end

test/test_opsum_to_ttn_mpo_cross_check.jl

Lines changed: 46 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ using Graphs: add_edge!, add_vertex!, rem_edge!, vertices
55
using ITensorMPS: ITensorMPS
66
using ITensorNetworks.ITensorsExtensions: replace_vertices
77
using ITensorNetworks: ITensorNetworks, siteinds, ttn
8-
using ITensors.NDTensors: matrix
9-
using ITensors: ITensors, @disable_warn_order, ITensor, Index, combinedind, combiner,
10-
contract, dag, inds, removeqns
8+
using ITensors.NDTensors: matrix, with_auto_fermion
9+
using ITensors: @disable_warn_order, ITensor, Index, combinedind, combiner, contract, dag,
10+
inds, removeqns
1111
using LinearAlgebra: norm
1212
using NamedGraphs.GraphsExtensions: leaf_vertices, post_order_dfs_vertices
1313
using NamedGraphs.NamedGraphGenerators: named_comb_tree
@@ -30,7 +30,6 @@ end
3030
@testset "OpSum to TTN vs ITensorMPS.MPO" begin
3131
@testset "OpSum to TTN" begin
3232
# small comb tree
33-
auto_fermion_enabled = ITensors.using_auto_fermion()
3433
tooth_lengths = fill(2, 3)
3534
c = named_comb_tree(tooth_lengths)
3635

@@ -73,9 +72,6 @@ end
7372
end
7473
@test Tttno_lr Tmpo_lr rtol = 1.0e-6
7574
end
76-
if auto_fermion_enabled
77-
ITensors.enable_auto_fermion()
78-
end
7975
end
8076

8177
@testset "OpSum to TTN QN" begin
@@ -129,54 +125,50 @@ end
129125
end
130126

131127
@testset "OpSum to TTN Fermions" begin
132-
# small comb tree
133-
auto_fermion_enabled = ITensors.using_auto_fermion()
134-
if !auto_fermion_enabled
135-
ITensors.enable_auto_fermion()
136-
end
137-
tooth_lengths = fill(2, 3)
138-
c = named_comb_tree(tooth_lengths)
139-
is = siteinds("Fermion", c; conserve_nf = true)
140-
141-
# test with next-nearest neighbor tight-binding model
142-
t = 1.0
143-
tp = 0.4
144-
U = 0.0
145-
h = 0.5
146-
H = ModelHamiltonians.tight_binding(c; t, tp, h)
147-
148-
# add combination of longer range interactions
149-
Hlr = copy(H)
150-
151-
@testset "Svd approach" for root_vertex in leaf_vertices(is)
152-
# get TTN Hamiltonian directly
153-
Hsvd = ttn(H, is; root_vertex, cutoff = 1.0e-10)
154-
# get corresponding MPO Hamiltonian
155-
sites = [only(is[v]) for v in reverse(post_order_dfs_vertices(c, root_vertex))]
156-
vmap = Dictionary(
157-
reverse(post_order_dfs_vertices(c, root_vertex)),
158-
1:length(sites)
159-
)
160-
Hline = ITensorMPS.MPO(replace_vertices(v -> vmap[v], H), sites)
161-
@disable_warn_order begin
162-
Tmpo = prod(Hline)
163-
Tttno = contract(Hsvd)
128+
with_auto_fermion() do
129+
# small comb tree
130+
tooth_lengths = fill(2, 3)
131+
c = named_comb_tree(tooth_lengths)
132+
is = siteinds("Fermion", c; conserve_nf = true)
133+
134+
# test with next-nearest neighbor tight-binding model
135+
t = 1.0
136+
tp = 0.4
137+
U = 0.0
138+
h = 0.5
139+
H = ModelHamiltonians.tight_binding(c; t, tp, h)
140+
141+
# add combination of longer range interactions
142+
Hlr = copy(H)
143+
144+
@testset "Svd approach" for root_vertex in leaf_vertices(is)
145+
# get TTN Hamiltonian directly
146+
Hsvd = ttn(H, is; root_vertex, cutoff = 1.0e-10)
147+
# get corresponding MPO Hamiltonian
148+
sites =
149+
[only(is[v]) for v in reverse(post_order_dfs_vertices(c, root_vertex))]
150+
vmap = Dictionary(
151+
reverse(post_order_dfs_vertices(c, root_vertex)),
152+
1:length(sites)
153+
)
154+
Hline = ITensorMPS.MPO(replace_vertices(v -> vmap[v], H), sites)
155+
@disable_warn_order begin
156+
Tmpo = prod(Hline)
157+
Tttno = contract(Hsvd)
158+
end
159+
160+
# verify that the norm isn't 0 and thus the same (which would indicate a problem with the autofermion system
161+
@test norm(Tmpo) > 0
162+
@test norm(Tttno) > 0
163+
@test norm(Tmpo) norm(Tttno) rtol = 1.0e-6
164+
165+
# TODO: fix comparison for fermionic tensors
166+
@test_broken Tmpo Tttno
167+
# In the meantime: matricize tensors and convert to dense Matrix to compare element by element
168+
dTmm = to_matrix(Tmpo)
169+
dTtm = to_matrix(Tttno)
170+
@test any(>(1.0e-14), dTmm - dTtm)
164171
end
165-
166-
# verify that the norm isn't 0 and thus the same (which would indicate a problem with the autofermion system
167-
@test norm(Tmpo) > 0
168-
@test norm(Tttno) > 0
169-
@test norm(Tmpo) norm(Tttno) rtol = 1.0e-6
170-
171-
# TODO: fix comparison for fermionic tensors
172-
@test_broken Tmpo Tttno
173-
# In the meantime: matricize tensors and convert to dense Matrix to compare element by element
174-
dTmm = to_matrix(Tmpo)
175-
dTtm = to_matrix(Tttno)
176-
@test any(>(1.0e-14), dTmm - dTtm)
177-
end
178-
if !auto_fermion_enabled
179-
ITensors.disable_auto_fermion()
180172
end
181173
end
182174

0 commit comments

Comments
 (0)