Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "AbstractQAtlas"
uuid = "dcea2817-62f8-4a75-b498-1b50a9ed1e4d"
version = "0.7.12"
version = "0.7.13"
authors = ["sota shimozono <shimozono-sota631@g.ecc.u-tokyo.ac.jp>"]

[deps]
Expand Down
39 changes: 37 additions & 2 deletions src/relations/entanglement.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@
# relations and the Region-typed layer above them cannot drift apart.
_chord(L, ℓ) = (L / π) * sin(π * ℓ / L)

# A slope relation with no cuts constrains nothing: the residual is `slope - 0`, so
# any central charge passes it. `entanglement_cuts` returns 0 for a whole ring, which
# is a correct count and not a slope to take.
function _require_cuts(what::Symbol, ncuts)
ncuts == 0 && error(
"$what: ncuts = 0 leaves the residual independent of the central charge, so " *
"the check would pass for any value of it. A region with no cuts has no " *
"entanglement to grow and no slope to read.",
)
return nothing
end

# `sin` is periodic, so an ℓ outside the chain does not merely give a wrong number:
# ℓ = 250 on L = 100 returns exactly the ℓ = 50 answer. And ℓ = L is not caught by a
# blow-up either, since `sin(float(π))` is 1.2e-16 rather than 0, which turns into a
Expand All @@ -41,6 +53,26 @@ Variables: `S2`, `purity`.
"""
@relation :entanglement RenyiTwoPurity(S2, purity) = S2 + log(purity)

"""
CFTEntanglementChordSlope <: AbstractRelation

`dS/d(ln[(L/π) sin(πℓ/L)]) = ncuts · c/6` (Calabrese & Cardy,
[CalabreseCardy2004](@cite)).

The finite-chain form. [`CFTEntanglementSlope`](@ref) carries no `L` and so cannot
say how large `ℓ` may be, and out of range it returns a number rather than refusing:
near `ℓ = L` the complement is a few sites and purity alone caps `S`, which no
`ncuts` describes. This form holds over the whole chain and reduces to that one as
`ℓ/L → 0`.

Variables: `dS_dlogchord` (caller-computed), `c`, `ncuts`.
"""
@relation :entanglement CFTEntanglementChordSlope(dS_dlogchord, c::CentralCharge, ncuts) =
begin
_require_cuts(:CFTEntanglementChordSlope, ncuts)
dS_dlogchord - ncuts * c / 6
end

"""
CFTEntanglementSlope <: AbstractRelation

Expand All @@ -62,11 +94,14 @@ The last row is the one "OBC ⇒ `c/6`" gets wrong.

Variables: `dS_dlogℓ` (caller-computed slope against `ln ℓ`), `c`, `ncuts`.
`c` is the typed subject; [`VonNeumannEntropy`](@ref) arrives via the supplied
derivative, hence [`also_constrains`](@ref). `ncuts` has no default:
derivative, hence [`also_constrains`](@ref). For `ℓ` comparable to `L` this form
is out of domain and [`CFTEntanglementChordSlope`](@ref) is the one to use. `ncuts` has no default:
[`Region`](@ref) carries no adjacency or boundary, so nothing can compute it.
"""
@relation :entanglement CFTEntanglementSlope(dS_dlogℓ, c::CentralCharge, ncuts) =
@relation :entanglement CFTEntanglementSlope(dS_dlogℓ, c::CentralCharge, ncuts) = begin
_require_cuts(:CFTEntanglementSlope, ncuts)
dS_dlogℓ - ncuts * c / 6
end

"""
InfiniteRandomnessEntanglementSlope <: AbstractRelation
Expand Down
12 changes: 11 additions & 1 deletion src/relations/quantity_links.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,17 @@ also_constrains(::ParticleNumberResponse) = (GrandPotential,) # grand-canonical
also_constrains(::StaticFromDynamicalStructureFactor) = (DynamicalStructureFactor,) # Sq = ∫S(q,ω)dω/2π (supplied)
also_constrains(::ChernFromBerryCurvature) = (BerryCurvature,) # topology: C = ∫Ω d²k/2π (supplied integral)
also_constrains(::CFTEntanglementInfinite) = (VonNeumannEntropy,) # the L→∞ sibling
also_constrains(::CFTEntanglementSlope) = (VonNeumannEntropy,) # entanglement: dS/d(ln ℓ) (supplied derivative)
also_constrains(::CFTEntanglementSlope) = (VonNeumannEntropy,) # entanglement: dS/d(ln ℓ), supplied
also_constrains(::CFTEntanglementChordSlope) = (VonNeumannEntropy,) # the finite-chain twin
# The spatial correlator's decay, so the subject is the two-point function itself, and
# split by reduction for the same reason the autocorrelations above are: at an
# infinite-randomness point the average is set by rare pairs and the typical is not.
function also_constrains(::CriticalCorrelationDecay)
return (DisorderAveraged{ConnectedSpinCorrelation},)
end
function also_constrains(::ActivatedCriticalCorrelation)
return (Typical{ConnectedSpinCorrelation},)
end
also_constrains(::CFTEntanglementPBC) = (VonNeumannEntropy,) # entanglement: S(ℓ) on a ring
also_constrains(::CFTEntanglementOBC) = (VonNeumannEntropy,) # entanglement: S(ℓ) at an open end
function also_constrains(::OffCriticalEntanglementSaturation)
Expand Down
35 changes: 35 additions & 0 deletions src/relations/scaling.jl
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,41 @@ magnetization, both in §4.1 and both stated for free boundary conditions.
@relation :scaling ActivatedFiniteSizeScaling(dloglogO_dlogL, ψ::ActivatedExponent) =
dloglogO_dlogL - ψ

"""
CriticalCorrelationDecay <: AbstractRelation

`⟨C(r)⟩ ∼ r^{-2 x_m}`, so `d ln⟨C⟩/d ln r = -2 x_m` ([IgloiMonthus2005](@cite),
Eq. (A.6); §A.3 carries it to an infinite-randomness point, where the average is
dominated by rare pairs).

The AVERAGE correlation. Not interchangeable with
[`ActivatedCriticalCorrelation`](@ref), the typical one: the correlation function is
non-self-averaging here, so the two read different exponents off one ground state.

Distance is a third scale, neither the `L` of [`ActivatedFiniteSizeScaling`](@ref)
nor the `ξ` of [`ActivatedDynamicalScaling`](@ref), which diverges at criticality.

Variables: `dlogC_dlogr` (caller-computed), `x_m`.
"""
@relation :scaling CriticalCorrelationDecay(dlogC_dlogr, x_m::ScalingDimension) =
dlogC_dlogr + 2 * x_m

"""
ActivatedCriticalCorrelation <: AbstractRelation

`exp⟨ln|C(r)|⟩ ∼ exp(-a r^ψ)`, so `d ln(-⟨ln|C|⟩)/d ln r = ψ`
([IgloiMonthus2005](@cite), Eq. (6.26), derived there for the random singlet phase
where `ψ = 1/2`).

The TYPICAL correlation, and the worse-conditioned of the two probes: on the same
chains its uncorrected slope sits three of its own errors above `1/2` where
[`CriticalCorrelationDecay`](@ref)'s is already within one.

Variables: `dloglogC_dlogr` (caller-computed), `ψ`.
"""
@relation :scaling ActivatedCriticalCorrelation(dloglogC_dlogr, ψ::ActivatedExponent) =
dloglogC_dlogr - ψ

"""
TypicalCorrelationLength <: AbstractRelation

Expand Down
5 changes: 5 additions & 0 deletions src/structure/scaling_dimensions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ exponent is derived by [`critical_exponents`](@ref).
at construction rather than one per call, and at an infinite-randomness fixed
point there is no finite `d + z` to confuse it with anyway.

Concretely: `fetch(Universality(:IsingSDRG), CentralCharge(); d = 2)` in QAtlas
refuses any other `d`, `d` being the CFT's dimension there, and returns the
EFFECTIVE charge under `CentralCharge`. Carrying either number straight here is
the case above. Tracked as #157.

Arguments are promoted to a common type; pass `Rational`s where the values are
rational. `ψ ≤ 0` is refused rather than accepted: it names a conventional
fixed point, which is [`ScalingDimensions`](@ref)'s job, and it would divide by
Expand Down
6 changes: 6 additions & 0 deletions test/relations/test_derivation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,12 @@ end
filter(t -> t === nothing, last.(variable_slots(ActivatedMomentGrowth())))
)

# A general law and its own limit must NOT be grouped. A family is alternatives of
# which at most one holds, and one member agreeing satisfies it; these two are both
# true on different data, so grouping would let the chord route excuse an
# out-of-domain plain-ℓ one instead of reporting the disagreement.
@test law_family(CFTEntanglementSlope()) !== law_family(CFTEntanglementChordSlope())

# And the exemplar stays split.
@test OrderParameterExponent in variable_types(Rushbrooke())
@test !(InverseTemperature in variable_types(Rushbrooke()))
Expand Down
119 changes: 98 additions & 21 deletions test/relations/test_entanglement.jl
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,25 @@ end
@test !any(r -> r isa CFTEntanglementSlope, applicable_relations(b_irfp; slope...))
end

function covariance(N)
A = zeros(2N, 2N)
for j in 1:N
A[2j - 1, 2j] = -2.0
end
for j in 1:(N - 1)
A[2j, 2j + 1] = -2.0
end
A = A - transpose(A)
F = svd(A)
Γ = F.U * F.Vt
return (Γ .- transpose(Γ)) ./ 2
end
function ed_entropy(Γ, sites)
idx = vcat(([2j - 1, 2j] for j in sites)...)
ν = eigvals(Hermitian(im .* Γ[idx, idx]))
return free_fermion_entanglement_entropy([(1 + real(x)) / 2 for x in ν if real(x) > 0])
end

@testset "the open-chain form against exact diagonalisation and a published constant" begin
# The closed forms were otherwise checked only by retyping them in the test, so a
# prefactor transcribed wrongly from the source would be transcribed wrongly here
Expand All @@ -462,27 +481,6 @@ end
c = 1 / 2
c₁_source = log(2) * 0.6904133 # Table 1, converted from their bits to nats

function covariance(N)
A = zeros(2N, 2N)
for j in 1:N
A[2j - 1, 2j] = -2.0
end
for j in 1:(N - 1)
A[2j, 2j + 1] = -2.0
end
A = A - transpose(A)
F = svd(A)
Γ = F.U * F.Vt
return (Γ .- transpose(Γ)) ./ 2
end
function ed_entropy(Γ, sites)
idx = vcat(([2j - 1, 2j] for j in sites)...)
ν = eigvals(Hermitian(im .* Γ[idx, idx]))
return free_fermion_entanglement_entropy([
(1 + real(x)) / 2 for x in ν if real(x) > 0
])
end

N = 256
Γ = covariance(N)
# Read c₁ back THROUGH the relation, so the production chord is what is exercised.
Expand All @@ -503,6 +501,85 @@ end
@test abs(wrong(64) - c₁_source) > 0.1
end

@testset "the chord slope is in domain over the whole chain and ln ℓ is not" begin
# Exact ED ground state of the critical uniform chain, block at an open end.
function fitslope(xs, ys)
x̄, ȳ = sum(xs) / length(xs), sum(ys) / length(ys)
return sum((xs .- x̄) .* (ys .- ȳ)) / sum((xs .- x̄) .^ 2)
end
c, ncuts = 1 / 2, 1

function slopes(N)
Γ = covariance(N)
ls = filter(
l -> 2 <= l <= N - 2,
unique(
round.(
Int,
N .* [0.03, 0.05, 0.08, 0.12, 0.19, 0.3, 0.45, 0.6, 0.75, 0.88, 0.95],
),
),
)
ys = [ed_entropy(Γ, 1:l) for l in ls]
chord = [(N / π) * sin(π * l / N) for l in ls]
idx = findall(l -> l <= N ÷ 4, ls)
near, yn = ls[idx], ys[idx]
cn = chord[idx]
return (
plain_full=fitslope(log.(ls), ys),
chord_full=fitslope(log.(chord), ys),
plain_near=fitslope(log.(near), yn),
chord_near=fitslope(log.(cn), yn),
)
end
s64, s128, s256 = slopes(64), slopes(128), slopes(256)

# In the regime the present relation is for, it is right.
@test isapprox(s128.plain_near, ncuts * c / 6; atol=0.005)

# Over the whole chain it is not, and by a lot: the block's complement is a few
# sites, purity caps `S`, and no `ncuts` describes that.
@test abs(s128.plain_full - ncuts * c / 6) > 0.04

# The chord form is in domain over the same whole chain.
@test isapprox(s128.chord_full, ncuts * c / 6; atol=0.01)

# Two ratios, not one: a 1/L correction halves per doubling and keeps doing it,
# a wrong law does not move. A threshold cannot separate those.
e = [abs(s.chord_full - c / 6) for s in (s64, s128, s256)]
@test e[2] < 0.6 * e[1]
@test e[3] < 0.6 * e[2]
@test e[3] < e[2] < e[1]
q = [abs(s.plain_full - c / 6) for s in (s64, s128, s256)]
@test q[2] > 0.9 * q[1]
@test q[3] > 0.9 * q[2]

# The relation itself: the chord slope reads the central charge back.
@test isapprox(
solve(
CFTEntanglementChordSlope(), Val(:c); dS_dlogchord=s128.chord_full, ncuts=ncuts
),
c;
atol=0.06,
)
# The limit: for ℓ ≪ L the two abscissas coincide, so one dataset gives both
# relations the same charge.
@test isapprox(s128.plain_near, s128.chord_near; rtol=0.05)
@test isapprox(
solve(CFTEntanglementSlope(), Val(:c); dS_dlogℓ=s128.plain_near, ncuts=ncuts),
solve(
CFTEntanglementChordSlope(), Val(:c); dS_dlogchord=s128.chord_near, ncuts=ncuts
);
rtol=0.05,
)

# With no cuts the residual does not depend on `c`, so both refuse.
@test_throws "ncuts = 0" residual(
CFTEntanglementChordSlope(); dS_dlogchord=0.0, c=9.9, ncuts=0
)
@test_throws "ncuts = 0" residual(CFTEntanglementSlope(); dS_dlogℓ=0.0, c=9.9, ncuts=0)
end

@testset "every route from an entropy to c returns the same c" begin
# `related_quantities(CentralCharge)` carries five distinct edges to
# VonNeumannEntropy, so one measurement can be read for `c` five ways. Each was
Expand Down
6 changes: 3 additions & 3 deletions test/relations/test_interface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ AbstractQAtlas.domain(::_NonAffineDemo) = :test_only
# domain that has no line here shows up as a mismatch in this number alone.
# Universal-only: the model-specific relations (spin glass, Drude mobility,
# single-band Hall) live in QAtlas.
@test length(rels) == 167
@test length(rels) == 170
@test allunique(typeof.(rels))
@test length(all_relations(; domain=:scaling)) == 28 # +ActivatedDynamicalScaling, ActivatedFiniteSizeScaling, +14 Appendix-A scaling types, +WeinribHalperinExponent, +QuantumHyperscaling; +TypicalCorrelationLength, GriffithsExponentDivergence, GriffithsSusceptibility, GriffithsSpecificHeat, ActivatedMomentGrowth
@test length(all_relations(; domain=:scaling)) == 30 # +ActivatedDynamicalScaling, ActivatedFiniteSizeScaling, +14 Appendix-A scaling types, +WeinribHalperinExponent, +QuantumHyperscaling; +TypicalCorrelationLength, GriffithsExponentDivergence, GriffithsSusceptibility, GriffithsSpecificHeat, ActivatedMomentGrowth; +CriticalCorrelationDecay, ActivatedCriticalCorrelation (#158)
@test length(all_relations(; domain=:thermodynamic)) == 15
@test length(all_relations(; domain=:fundamental)) == 9 # +GrandPotentialLegendre, ParticleNumberResponse (grand-canonical); +ElectricCurrentResponse (j = −∂H/∂A)
@test length(all_relations(; domain=:topology)) == 3
Expand All @@ -31,7 +31,7 @@ AbstractQAtlas.domain(::_NonAffineDemo) = :test_only
@test length(all_relations(; domain=:holographic)) == 1 # BekensteinEntropyBound — the one non-quantum universal bound
@test length(all_relations(; domain=:ensemble)) == 2
@test length(all_relations(; domain=:fluctuation)) == 5 # +TypicalBelowAverage, +AnnealedFreeEnergyBound
@test length(all_relations(; domain=:entanglement)) == 32 # +InfiniteRandomnessEntanglementSlope, CFTEntanglementPBC/OBC, OffCriticalEntanglementSaturation, HalvedChainEntropyDifference, CFTEntanglementInfinite, InfiniteRandomnessEntanglementPBC
@test length(all_relations(; domain=:entanglement)) == 33 # +InfiniteRandomnessEntanglementSlope, CFTEntanglementPBC/OBC, OffCriticalEntanglementSaturation, HalvedChainEntropyDifference, CFTEntanglementInfinite, InfiniteRandomnessEntanglementPBC; +CFTEntanglementChordSlope (#156)
@test length(all_relations(; domain=:wick)) == 2
@test length(all_relations(; domain=:cft)) == 4
@test isempty(all_relations(; domain=:spinglass)) # model-specific — lives in QAtlas now
Expand Down
47 changes: 47 additions & 0 deletions test/relations/test_scaling.jl
Original file line number Diff line number Diff line change
Expand Up @@ -637,3 +637,50 @@ end
SpecificHeatExponent, CorrelationLengthExponent, SpatialDimension, DynamicalExponent
)
end

@testset "the two critical correlation probes are separate statements" begin
# What a ground state yields: the correlation function at criticality against
# distance, which is a third scale, neither `L` nor the divergent `ξ`.
x_m = (3 - sqrt(5)) / 4 # the golden-mean IRFP value, IgloiMonthus2005
ψ = 1 / 2

# The anchor is the literature value; these pin the sign and the factor 2.
@test solve(CriticalCorrelationDecay(), Val(:x_m); dlogC_dlogr=-2 * x_m) ≈ x_m
@test solve(CriticalCorrelationDecay(), Val(:dlogC_dlogr); x_m=x_m) ≈ -2 * x_m
@test check(CriticalCorrelationDecay(); dlogC_dlogr=-0.38197, x_m=x_m, atol=1e-5)
@test !check(CriticalCorrelationDecay(); dlogC_dlogr=+2 * x_m, x_m=x_m, atol=1e-9)

@test solve(ActivatedCriticalCorrelation(), Val(:ψ); dloglogC_dlogr=ψ) ≈ ψ
@test check(ActivatedCriticalCorrelation(); dloglogC_dlogr=ψ, ψ=ψ, atol=1e-12)

# A measured slope from the issue that asked for these, L = 512 over 500 samples,
# stated in units of its own published error rather than a tolerance picked to
# pass: it sits about one standard error from the exact value.
measured, σ = -0.4029, 0.0204
r = abs(residual(CriticalCorrelationDecay(); dlogC_dlogr=measured, x_m=x_m))
@test r < 1.5σ
# A guard on the literals above, not a floor on accuracy: it catches pasting the
# exact value in place of the measured one. A genuinely better measurement would
# trip it and should arrive with this line removed.
@test r > 0.5σ

# Signed and as a ratio, so a sign flip cannot satisfy it the way a magnitude
# bound can: the uncorrected typical slope is three of its own errors ABOVE 1/2.
@test residual(ActivatedCriticalCorrelation(); dloglogC_dlogr=0.56, ψ=ψ) / 0.02 ≈ 3.0 atol =
0.05

# Different exponents off one ground state, so neither substitutes for the other.
@test variable_types(CriticalCorrelationDecay()) == (ScalingDimension,)
@test variable_types(ActivatedCriticalCorrelation()) == (ActivatedExponent,)
@test isdisjoint(
variables(CriticalCorrelationDecay()), variables(ActivatedCriticalCorrelation())
)

# Split by reduction, so the two readings cannot meet at one node.
@test also_constrains(CriticalCorrelationDecay()) ==
(DisorderAveraged{ConnectedSpinCorrelation},)
@test also_constrains(ActivatedCriticalCorrelation()) ==
(Typical{ConnectedSpinCorrelation},)
@test CriticalCorrelationDecay() in
relations_constraining(DisorderAveraged{ConnectedSpinCorrelation})
end
Loading