diff --git a/Project.toml b/Project.toml index 7ae16ea..48a8d70 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "AbstractQAtlas" uuid = "dcea2817-62f8-4a75-b498-1b50a9ed1e4d" -version = "0.7.10" +version = "0.7.11" authors = ["sota shimozono "] [deps] diff --git a/src/relations/entanglement.jl b/src/relations/entanglement.jl index 1cedc8c..22b1440 100644 --- a/src/relations/entanglement.jl +++ b/src/relations/entanglement.jl @@ -308,10 +308,14 @@ depends on it. end @experimental """ -Eq. (24)'s scaling function is not settled here: `f` is caller-supplied, no -independent oracle checks the form, and the only case pinned is its conformal -one-harmonic reduction. No bound on `f` follows from the geometry either, so a -positive but small value returns a negative entropy rather than a refusal +Eq. (24)'s scaling function is not settled here. Reflection symmetry and the +normalisation are checked where `f` is still a callable, in +[`finite_size_entropy_report`](@ref); its FORM is not, and cannot be from here, +since separating the higher harmonics needs a disorder average this package does +not compute. The only form pinned is the one-harmonic reduction, which is +[`CFTEntanglementPBC`](@ref). Reaching the relation directly bypasses both checks, +`f` arriving as a number that can only be asked to be positive, so a small one +returns a negative entropy """ InfiniteRandomnessEntanglementPBC """ diff --git a/src/relations/region_entropy.jl b/src/relations/region_entropy.jl index a030434..6751011 100644 --- a/src/relations/region_entropy.jl +++ b/src/relations/region_entropy.jl @@ -414,6 +414,48 @@ function _finite_size_row!(out, rel, A::Region, vars, atol) return _finite_size_row!(out, rel, (A,), vars, atol) end +# Eq. (24) states `f` through `f(v) = Σₖ Aₖ sin((2k-1)πv)` under `Σₖ Aₖ(2k-1)π = 1`, +# and two consequences hold whatever the coefficients are: every basis function is +# symmetric about `v = 1/2`, and the normalisation is `f'(0) = 1`. The FORM is not +# checked, and a future check of it must still admit harmonics past `k = 1`, those +# being what Eq. (24) says over the conformal reduction rather than an error in it. +# +# Asked on the grid `f` is read at, `ℓ/N`, so a component the check cannot see cannot +# reach a row either. `f'(0)` is estimated from `f(1/N)` and `f(2/N)` by Richardson, +# error falling as `N^-4`, rather than from a point near zero that an `f` fitted on a +# finite chain has no reason to be defined at. +function _check_symmetry_at(f, v) + a, m = f(v), f(1 - v) + (a isa Real && m isa Real && isfinite(a) && isfinite(m)) || error( + "finite_size_entropy_report: f($v) = $a and f($(1 - v)) = $m, but the scaling " * + "function must be a finite real on the grid it is read at.", + ) + isapprox(a, m; rtol=1e-8, atol=1e-12) || error( + "finite_size_entropy_report: f($v) = $a but f($(1 - v)) = $m, so the scaling " * + "function is not symmetric about v = 1/2 and is not in Eq. (24)'s family.", + ) + return nothing +end + +# Loose on purpose: it separates an order-unity factor, which is what a missing `1/π` +# or a chord passed whole looks like, from the estimate's own truncation. Tightening +# it would start refusing a strongly non-conformal `f` on a ring too short to resolve +# its harmonics, which is a statement about the grid and not about `f`. +function _check_normalisation(f, N) + s1, s2 = f(1 / N), f(2 / N) + (s1 isa Real && s2 isa Real && isfinite(s1) && isfinite(s2)) || error( + "finite_size_entropy_report: f(1/$N) = $s1 and f(2/$N) = $s2, but the scaling " * + "function must be a finite real on the grid it is read at.", + ) + slope = (4 * s1 * N - s2 * N / 2) / 3 + isapprox(slope, 1; atol=0.1) || error( + "finite_size_entropy_report: f'(0) is about $slope, not 1, so the scaling " * + "function breaks Eq. (24)'s normalisation. A constant factor shifts ln[L f] " * + "into c₁′ and passes; f is the dimensionless sin(πv)/π, not the chord.", + ) + return nothing +end + """ finite_size_entropy_report(b::Bag, bc::BoundaryCondition; c₁, kwargs...) -> Vector{RegionFiniteSizeRow} @@ -466,6 +508,8 @@ function finite_size_entropy_report( c̃ = get(b, VariableKey(EffectiveCentralCharge), nothing) ξ = get(b, VariableKey(CorrelationLength), nothing) (c === nothing && c̃ === nothing) && return out + uses_f = c̃ !== nothing && f !== nothing && bc isa PBC + f_normalised = false for pair in sort!(collect(ents); by=p -> (length(p.first), repr(p.first))) A, S = pair.first, pair.second (isempty(A) || !(eltype(A.sites) <: Integer) || eltype(A.sites) === Bool) && @@ -484,7 +528,9 @@ function finite_size_entropy_report( ) end end - if c̃ !== nothing && f !== nothing && bc isa PBC && n == 2 + if uses_f && n == 2 + f_normalised || (_check_normalisation(f, bc.N); f_normalised=true) + _check_symmetry_at(f, ℓ / bc.N) _finite_size_row!( out, InfiniteRandomnessEntanglementPBC(), diff --git a/test/relations/test_region_entropy.jl b/test/relations/test_region_entropy.jl index de60000..c98ea80 100644 --- a/test/relations/test_region_entropy.jl +++ b/test/relations/test_region_entropy.jl @@ -547,6 +547,70 @@ end @test Set(kinds) == Set([:CFTEntanglementPBC, :InfiniteRandomnessEntanglementPBC]) end +@testset "the scaling function is checked against what Eq. (24) says it is" begin + c̃, c₁′, N = log(2) / 2, 0.31, 1024 + conf(v) = sin(π * v) / π + S̄(ℓ) = (c̃ / 3) * log(N * conf(ℓ / N)) + c₁′ + b = bag(entanglement_entropy(Region(1:300...)) => S̄(300), EffectiveCentralCharge => c̃) + call(g) = finite_size_entropy_report(b, PBC(N); c₁=0.0, f=g, c₁′=c₁′, atol=1e-12) + + # Eq. (24) is about the random ring NOT being the conformal one, and the higher + # harmonics are the whole difference, so a two-term `f` must be accepted. A check + # that took only `sin(πv)/π` would pass everything below and refuse the physics. + two(v) = (1 / π - 0.06) * sin(π * v) + 0.02 * sin(3π * v) + @test !isapprox(two(0.3), conf(0.3); rtol=1e-3) + rows = call(two) + @test length(rows) == 1 + @test !only(rows).pass # entropy built on `conf`, so it fails + + @test_throws "not symmetric" call(v -> sin(π * v) / π + 0.05 * v) + + # A constant factor lands in `c₁′` rather than in the residual, so the normalisation + # is the only thing that can see it. + @test_throws "f'(0)" call(v -> sin(π * v)) # the chord, missing the 1/π + @test_throws "f'(0)" call(v -> 2 * sin(π * v) / π) + + # Both are asked on the grid `f` is read at. A component invisible there is + # invisible to the row too, so there is no sample set to slip between: this `f` is + # symmetric at three round points and wildly asymmetric at the one the bag uses. + sneak(v) = sin(π * v) / π + 0.08 * (sin(20π * v) - 0.5 * sin(40π * v)) + @test isapprox(sneak(0.25), sneak(0.75); atol=1e-12) + @test_throws "not symmetric" call(sneak) + + # And the normalisation is estimated from `1/N` and `2/N`, points on that same + # grid, so an `f` fitted on a finite chain and undefined off it is still accepted. + function fitted(v) + (1 / N <= v <= 1 - 1 / N) || error("outside the fitted range") + return two(v) + end + @test length(call(fitted)) == 1 + + # An `f` that is not a finite real says so, rather than being reported as an + # asymmetry between two NaNs or reaching the kernel and failing on `isless`. + @test_throws "finite real" call(v -> NaN) + @test_throws "finite real" call(v -> complex(sin(π * v) / π, 0.0)) + + # Checked only at the points that feed a row. A whole-ring region has no cuts, so + # `f` is never read and must not be refused: the report is empty, not an error. + whole = bag(entanglement_entropy(Region(1:N...)) => 1.0, EffectiveCentralCharge => c̃) + @test isempty( + finite_size_entropy_report(whole, PBC(N); c₁=0.0, f=v -> -1.0, c₁′=c₁′, atol=1e-12) + ) + + # Same for the other two ways of not being read. + ent = entanglement_entropy(Region(1:300...)) + open_chain = bag(ent => 1.0, EffectiveCentralCharge => c̃) + @test isempty( + finite_size_entropy_report( + open_chain, OBC(N); c₁=0.0, f=v -> -1.0, c₁′=c₁′, atol=1e-12 + ), + ) + clean = bag(ent => 1.0, CentralCharge => 0.5) + @test length( + finite_size_entropy_report(clean, PBC(N); c₁=0.0, f=v -> -1.0, atol=1e-12) + ) == 1 +end + @testset "an infinite chain gives the slope relations their derivative exactly" begin c, c₁ = 0.5, 0.4785 S(ℓ) = (c / 3) * log(ℓ) + c₁