diff --git a/Project.toml b/Project.toml index edad342..db5c6f4 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "AbstractQAtlas" uuid = "dcea2817-62f8-4a75-b498-1b50a9ed1e4d" -version = "0.7.12" +version = "0.7.13" authors = ["sota shimozono "] [deps] diff --git a/src/relations/entanglement.jl b/src/relations/entanglement.jl index b3a4220..5bed9f2 100644 --- a/src/relations/entanglement.jl +++ b/src/relations/entanglement.jl @@ -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 @@ -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 @@ -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 diff --git a/src/relations/quantity_links.jl b/src/relations/quantity_links.jl index 41f21ad..2108ead 100644 --- a/src/relations/quantity_links.jl +++ b/src/relations/quantity_links.jl @@ -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) diff --git a/src/relations/scaling.jl b/src/relations/scaling.jl index 6a73455..a06f42d 100644 --- a/src/relations/scaling.jl +++ b/src/relations/scaling.jl @@ -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 diff --git a/src/structure/scaling_dimensions.jl b/src/structure/scaling_dimensions.jl index 6eda7ff..a4db051 100644 --- a/src/structure/scaling_dimensions.jl +++ b/src/structure/scaling_dimensions.jl @@ -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 diff --git a/test/relations/test_derivation.jl b/test/relations/test_derivation.jl index 5b91a11..9bb70a5 100644 --- a/test/relations/test_derivation.jl +++ b/test/relations/test_derivation.jl @@ -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())) diff --git a/test/relations/test_entanglement.jl b/test/relations/test_entanglement.jl index d82fad2..1e3512b 100644 --- a/test/relations/test_entanglement.jl +++ b/test/relations/test_entanglement.jl @@ -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 @@ -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. @@ -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 diff --git a/test/relations/test_interface.jl b/test/relations/test_interface.jl index 3cc5065..edb2a90 100644 --- a/test/relations/test_interface.jl +++ b/test/relations/test_interface.jl @@ -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 @@ -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 diff --git a/test/relations/test_scaling.jl b/test/relations/test_scaling.jl index 07f8be8..e9eb4ce 100644 --- a/test/relations/test_scaling.jl +++ b/test/relations/test_scaling.jl @@ -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