From 9b89ae26322a73ebfc9d1dbcd2d48698c5de1031 Mon Sep 17 00:00:00 2001 From: sotashimozono Date: Mon, 14 Sep 2026 10:24:57 +0000 Subject: [PATCH 1/4] Check the scaling function against what Eq. (24) says it is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `f` reaches `InfiniteRandomnessEntanglementPBC` as a number, so the relation can only ask that it be positive. The callable is still visible in `finite_size_entropy_report`, and Eq. (24) states `f` through an expansion, `f(v) = Σₖ Aₖ sin((2k-1)πv)` under `Σₖ Aₖ(2k-1)π = 1`, from which two properties follow without knowing a coefficient: every basis function is symmetric about `v = 1/2`, so an admissible `f` is; and the normalisation IS `f'(0) = 1`. Both are now checked. The second is the one that mattered: an `f` off by a constant shifts `ln[L f]` by `ln` of that constant, which `c₁′` absorbs, so the wrong scaling function was reported as a PASS with a wrong non-universal constant rather than as a failure. Passing the chord `(L/π)sin(πℓ/L)` itself, rather than the dimensionless `sin(πv)/π`, is the way in. The form is still unchecked and this does not pretend otherwise. The higher harmonics are the entire difference between this and the conformal case, and separating them needs a disorder average that is not computed here. So the test that matters most is the one asserting a two-harmonic `f` is ACCEPTED: a check that admitted only `sin(πv)/π` would refuse the physics the relation exists for. The `@experimental` note now says which half is closed. Validated only where it is read, so an `f` no row consumes is not refused for being present. Co-Authored-By: Claude Opus 5 (1M context) --- src/relations/entanglement.jl | 13 +++++--- src/relations/region_entropy.jl | 35 ++++++++++++++++++++ test/relations/test_region_entropy.jl | 47 +++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 4 deletions(-) diff --git a/src/relations/entanglement.jl b/src/relations/entanglement.jl index 1cedc8c..b93c827 100644 --- a/src/relations/entanglement.jl +++ b/src/relations/entanglement.jl @@ -308,10 +308,15 @@ 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. Its two stated properties are +checked where `f` is still a callable, in [`finite_size_entropy_report`](@ref): +reflection symmetry, and the normalisation `Σₖ Aₖ(2k-1)π = 1` read as `f'(0) = 1`. +Its FORM is not, and cannot be from here. The higher harmonics are the whole +difference between this and the conformal case, and separating them 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 even the two 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..89aaad5 100644 --- a/src/relations/region_entropy.jl +++ b/src/relations/region_entropy.jl @@ -449,6 +449,38 @@ b = bag(entanglement_entropy(Region(1:32...)) => 1.06, CentralCharge => 0.5) finite_size_entropy_report(b, PBC(64); c₁=0.4785) ``` """ +# Eq. (24) states `f` through its expansion, `f(v) = Σₖ Aₖ sin((2k-1)πv)` under +# `Σₖ Aₖ(2k-1)π = 1`, and two properties follow from that alone. Every basis +# function is symmetric about `v = 1/2`, since `sin((2k-1)π(1-v)) = sin((2k-1)πv)` +# for odd `2k-1`, so any admissible `f` is; and the normalisation IS `f'(0) = 1`, +# which is what `f(v) → v` means. Neither needs the coefficients, so a caller who +# passes the chord itself, or one normalised to something else, is caught here +# rather than reported as a failing row against a correct entropy. +# +# The form of `f` is NOT checked: the higher harmonics are the difference between +# this and the conformal case, and no oracle here can see them. +function _check_scaling_function(f) + for v in (0.1, 0.25, 0.4) + a, m = f(v), f(1 - v) + isapprox(a, m; rtol=1e-8, atol=1e-12) || error( + "finite_size_entropy_report: the scaling function is not reflection " * + "symmetric, f($v) = $a but f($(1 - v)) = $m. Eq. (24) expands it in " * + "sin((2k-1)πv), and every one of those is symmetric about v = 1/2, so " * + "an asymmetric f is not in the family the relation is about.", + ) + end + v = 1e-5 + slope = f(v) / v + isapprox(slope, 1; atol=1e-6) || error( + "finite_size_entropy_report: the scaling function has f(v)/v → $slope, not " * + "1. Eq. (24) normalises it by Σₖ Aₖ(2k-1)π = 1, which is f'(0) = 1, so an f " * + "off by a constant shifts ln[L f] by ln of that constant and lands in c₁′ " * + "instead of failing. Passing the chord (L/π)sin(πℓ/L) itself is the common " * + "way in: f is the dimensionless sin(πv)/π.", + ) + return nothing +end + function finite_size_entropy_report( b::Bag, bc::BoundaryCondition; c₁::Real, ln_g::Real=0, f=nothing, c₁′::Real=0, atol=1e-8 ) @@ -466,6 +498,9 @@ function finite_size_entropy_report( c̃ = get(b, VariableKey(EffectiveCentralCharge), nothing) ξ = get(b, VariableKey(CorrelationLength), nothing) (c === nothing && c̃ === nothing) && return out + # Only when it will actually be read: validating an `f` that no row consumes + # would refuse an argument that changes nothing. + c̃ !== nothing && f !== nothing && bc isa PBC && _check_scaling_function(f) 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) && diff --git a/test/relations/test_region_entropy.jl b/test/relations/test_region_entropy.jl index de60000..1a747cd 100644 --- a/test/relations/test_region_entropy.jl +++ b/test/relations/test_region_entropy.jl @@ -547,6 +547,53 @@ end @test Set(kinds) == Set([:CFTEntanglementPBC, :InfiniteRandomnessEntanglementPBC]) end +@testset "the scaling function is checked against what Eq. (24) says it is" begin + # `f` reaches the relation as a number, so the relation can only ask that it be + # positive. The callable is visible here, and Eq. (24) states it through an + # expansion, `f(v) = Σₖ Aₖ sin((2k-1)πv)` under `Σₖ Aₖ(2k-1)π = 1`, from which + # two properties follow without knowing a single coefficient. + 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) + + # The point of Eq. (24) is that the random ring is NOT the conformal one, and the + # difference is the higher harmonics. So the check has to admit them: a two-term + # f, normalised the same way, is exactly as legitimate and must pass. A check + # that only accepted `sin(πv)/π` would refuse the physics it exists for. + two(v) = (1 / π - 0.06) * sin(π * v) + 0.02 * sin(3π * v) + @test isapprox(two(1e-5) / 1e-5, 1; atol=1e-6) # the normalisation, restated + @test !isapprox(two(0.3), conf(0.3); rtol=1e-3) # and it really is a different f + @test length(call(two)) == 1 # accepted, and the row is real + @test !only(call(two)).pass # against an entropy built on `conf` + + # Every basis function is symmetric about v = 1/2, so an admissible f is. An + # asymmetric one is not in the family and is refused rather than scored. + @test_throws "not reflection symmetric" call(v -> sin(π * v) / π + 0.05 * v) + + # `Σₖ Aₖ(2k-1)π = 1` IS `f'(0) = 1`. Off by a constant, `ln[L f]` shifts by `ln` + # of it, which is absorbed by `c₁′` rather than showing up as a failure: the + # wrong f would be reported as a pass with a wrong constant. + @test_throws "f(v)/v" call(v -> sin(π * v)) # the chord, missing the 1/π + @test_throws "f(v)/v" call(v -> 2 * sin(π * v) / π) + + # It refuses only where it is read. An `f` no row consumes changes nothing, and + # refusing it would be refusing an argument for being present. + open_chain = bag( + entanglement_entropy(Region(1:300...)) => 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(entanglement_entropy(Region(1:300...)) => 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₁ From 6f30e2879960abe43436145b10458af8819f8998 Mon Sep 17 00:00:00 2001 From: sotashimozono Date: Mon, 14 Sep 2026 10:29:30 +0000 Subject: [PATCH 2/4] Cut the commentary down, and bump to 0.7.11 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comments restated the docstring and the error messages restated the comments. Kept the two that say something the code does not: why a two-harmonic f must be accepted, and that a constant factor lands in c₁′ rather than in the residual. Co-Authored-By: Claude Opus 5 (1M context) --- Project.toml | 2 +- src/relations/region_entropy.jl | 32 +++++++------------ test/relations/test_region_entropy.jl | 44 +++++++++++---------------- 3 files changed, 29 insertions(+), 49 deletions(-) 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/region_entropy.jl b/src/relations/region_entropy.jl index 89aaad5..21453ef 100644 --- a/src/relations/region_entropy.jl +++ b/src/relations/region_entropy.jl @@ -449,34 +449,25 @@ b = bag(entanglement_entropy(Region(1:32...)) => 1.06, CentralCharge => 0.5) finite_size_entropy_report(b, PBC(64); c₁=0.4785) ``` """ -# Eq. (24) states `f` through its expansion, `f(v) = Σₖ Aₖ sin((2k-1)πv)` under -# `Σₖ Aₖ(2k-1)π = 1`, and two properties follow from that alone. Every basis -# function is symmetric about `v = 1/2`, since `sin((2k-1)π(1-v)) = sin((2k-1)πv)` -# for odd `2k-1`, so any admissible `f` is; and the normalisation IS `f'(0) = 1`, -# which is what `f(v) → v` means. Neither needs the coefficients, so a caller who -# passes the chord itself, or one normalised to something else, is caught here -# rather than reported as a failing row against a correct entropy. -# -# The form of `f` is NOT checked: the higher harmonics are the difference between -# this and the conformal case, and no oracle here can see them. +# Two consequences of Eq. (24)'s `f(v) = Σₖ Aₖ sin((2k-1)πv)` under +# `Σₖ Aₖ(2k-1)π = 1`, neither needing a coefficient: the basis is symmetric about +# `v = 1/2`, and the normalisation is `f'(0) = 1`. The FORM is not checked, the +# higher harmonics being exactly what a disorder average would be needed to see. function _check_scaling_function(f) for v in (0.1, 0.25, 0.4) a, m = f(v), f(1 - v) isapprox(a, m; rtol=1e-8, atol=1e-12) || error( - "finite_size_entropy_report: the scaling function is not reflection " * - "symmetric, f($v) = $a but f($(1 - v)) = $m. Eq. (24) expands it in " * - "sin((2k-1)πv), and every one of those is symmetric about v = 1/2, so " * - "an asymmetric f is not in the family the relation is about.", + "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.", ) end v = 1e-5 slope = f(v) / v isapprox(slope, 1; atol=1e-6) || error( - "finite_size_entropy_report: the scaling function has f(v)/v → $slope, not " * - "1. Eq. (24) normalises it by Σₖ Aₖ(2k-1)π = 1, which is f'(0) = 1, so an f " * - "off by a constant shifts ln[L f] by ln of that constant and lands in c₁′ " * - "instead of failing. Passing the chord (L/π)sin(πℓ/L) itself is the common " * - "way in: f is the dimensionless sin(πv)/π.", + "finite_size_entropy_report: f(v)/v → $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 @@ -498,8 +489,7 @@ function finite_size_entropy_report( c̃ = get(b, VariableKey(EffectiveCentralCharge), nothing) ξ = get(b, VariableKey(CorrelationLength), nothing) (c === nothing && c̃ === nothing) && return out - # Only when it will actually be read: validating an `f` that no row consumes - # would refuse an argument that changes nothing. + # Only where it is read; an unconsumed `f` changes nothing. c̃ !== nothing && f !== nothing && bc isa PBC && _check_scaling_function(f) for pair in sort!(collect(ents); by=p -> (length(p.first), repr(p.first))) A, S = pair.first, pair.second diff --git a/test/relations/test_region_entropy.jl b/test/relations/test_region_entropy.jl index 1a747cd..983d08e 100644 --- a/test/relations/test_region_entropy.jl +++ b/test/relations/test_region_entropy.jl @@ -548,47 +548,37 @@ end end @testset "the scaling function is checked against what Eq. (24) says it is" begin - # `f` reaches the relation as a number, so the relation can only ask that it be - # positive. The callable is visible here, and Eq. (24) states it through an - # expansion, `f(v) = Σₖ Aₖ sin((2k-1)πv)` under `Σₖ Aₖ(2k-1)π = 1`, from which - # two properties follow without knowing a single coefficient. 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) - # The point of Eq. (24) is that the random ring is NOT the conformal one, and the - # difference is the higher harmonics. So the check has to admit them: a two-term - # f, normalised the same way, is exactly as legitimate and must pass. A check - # that only accepted `sin(πv)/π` would refuse the physics it exists for. + # 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(1e-5) / 1e-5, 1; atol=1e-6) # the normalisation, restated - @test !isapprox(two(0.3), conf(0.3); rtol=1e-3) # and it really is a different f - @test length(call(two)) == 1 # accepted, and the row is real - @test !only(call(two)).pass # against an entropy built on `conf` - - # Every basis function is symmetric about v = 1/2, so an admissible f is. An - # asymmetric one is not in the family and is refused rather than scored. - @test_throws "not reflection symmetric" call(v -> sin(π * v) / π + 0.05 * v) - - # `Σₖ Aₖ(2k-1)π = 1` IS `f'(0) = 1`. Off by a constant, `ln[L f]` shifts by `ln` - # of it, which is absorbed by `c₁′` rather than showing up as a failure: the - # wrong f would be reported as a pass with a wrong constant. - @test_throws "f(v)/v" call(v -> sin(π * v)) # the chord, missing the 1/π + @test isapprox(two(1e-5) / 1e-5, 1; atol=1e-6) + @test !isapprox(two(0.3), conf(0.3); rtol=1e-3) + @test length(call(two)) == 1 + @test !only(call(two)).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 wrong `f` + # used to be a pass with a wrong constant. + @test_throws "f(v)/v" call(v -> sin(π * v)) # the chord, missing the 1/π @test_throws "f(v)/v" call(v -> 2 * sin(π * v) / π) - # It refuses only where it is read. An `f` no row consumes changes nothing, and - # refusing it would be refusing an argument for being present. - open_chain = bag( - entanglement_entropy(Region(1:300...)) => 1.0, EffectiveCentralCharge => c̃ - ) + # Refused only where read, so an unconsumed `f` is not refused for being present. + 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(entanglement_entropy(Region(1:300...)) => 1.0, CentralCharge => 0.5) + 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 From be6c378df1f4d7fdfc5925d56ce4696c1009c6be Mon Sep 17 00:00:00 2001 From: sotashimozono Date: Mon, 14 Sep 2026 10:41:07 +0000 Subject: [PATCH 3/4] Put the helper above the docstring, not between it and its function Inserting `_check_scaling_function` before `function finite_size_entropy_report` put it inside the gap the docstring was spanning, so the docstring bound to the helper and the report lost its own. Nothing about the code changed and no test of the behaviour noticed; `test_docs_crossrefs` did, because an `@ref` to the report then pointed at a name with no rendered docstring. Co-Authored-By: Claude Opus 5 (1M context) --- src/relations/region_entropy.jl | 46 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/relations/region_entropy.jl b/src/relations/region_entropy.jl index 21453ef..a15787f 100644 --- a/src/relations/region_entropy.jl +++ b/src/relations/region_entropy.jl @@ -414,6 +414,29 @@ function _finite_size_row!(out, rel, A::Region, vars, atol) return _finite_size_row!(out, rel, (A,), vars, atol) end +# Two consequences of Eq. (24)'s `f(v) = Σₖ Aₖ sin((2k-1)πv)` under +# `Σₖ Aₖ(2k-1)π = 1`, neither needing a coefficient: the basis is symmetric about +# `v = 1/2`, and the normalisation is `f'(0) = 1`. The FORM is not checked, the +# higher harmonics being exactly what a disorder average would be needed to see. +function _check_scaling_function(f) + for v in (0.1, 0.25, 0.4) + a, m = f(v), f(1 - v) + 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.", + ) + end + v = 1e-5 + slope = f(v) / v + isapprox(slope, 1; atol=1e-6) || error( + "finite_size_entropy_report: f(v)/v → $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} @@ -449,29 +472,6 @@ b = bag(entanglement_entropy(Region(1:32...)) => 1.06, CentralCharge => 0.5) finite_size_entropy_report(b, PBC(64); c₁=0.4785) ``` """ -# Two consequences of Eq. (24)'s `f(v) = Σₖ Aₖ sin((2k-1)πv)` under -# `Σₖ Aₖ(2k-1)π = 1`, neither needing a coefficient: the basis is symmetric about -# `v = 1/2`, and the normalisation is `f'(0) = 1`. The FORM is not checked, the -# higher harmonics being exactly what a disorder average would be needed to see. -function _check_scaling_function(f) - for v in (0.1, 0.25, 0.4) - a, m = f(v), f(1 - v) - 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.", - ) - end - v = 1e-5 - slope = f(v) / v - isapprox(slope, 1; atol=1e-6) || error( - "finite_size_entropy_report: f(v)/v → $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 - function finite_size_entropy_report( b::Bag, bc::BoundaryCondition; c₁::Real, ln_g::Real=0, f=nothing, c₁′::Real=0, atol=1e-8 ) From a22959673c5def95d51900c12fb0528f86d53f9a Mon Sep 17 00:00:00 2001 From: sotashimozono Date: Mon, 14 Sep 2026 11:17:05 +0000 Subject: [PATCH 4/4] Ask the scaling function on the grid it is actually read at MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six review agents converged on the same defect and found two more. The check as written could refuse an `f` that was never going to be used, refuse an `f` that was correct, and accept one that was wrong. REFUSED WHAT IT NEVER READ. The guard ran once before any region was inspected, so it fired whenever `c̃`, `f` and PBC were present. A bag holding only a whole ring, which has no cuts, would take a normalisation failure and die on it, having never been going to evaluate `f` at all. The comment above the guard asserted the opposite in as many words. REFUSED A CORRECT `f`. The slope was probed at `v = 1e-5`, but `f` is read at `ℓ/N`, so nothing below `1/N` is ever asked of it. An `f` fitted from a disorder average on a chain of a few hundred sites is entitled to be undefined there, and was refused for a value it would never see. ACCEPTED A WRONG `f`. Symmetry was sampled at 0.1, 0.25 and 0.4, all multiples of 1/20, so `sin(20πv)` and `sin(40πv)` vanish at every one of them and at their reflections. Those are even harmonics, which Eq. (24) excludes, and a pair of them tuned to cancel in `f'(0)` passed both checks while being 20% asymmetric elsewhere. All three go away by asking on the grid. Symmetry is checked at each `ℓ/N` that feeds a row, so a component the check cannot see cannot reach a row either, and the check happens where and only where `f` is consumed. `f'(0)` comes from `f(1/N)` and `f(2/N)` by Richardson, error falling as `N^-4`, which are points `f` must support because they are the grid. The tolerance is loose deliberately: it separates an order-unity factor from the estimate's own truncation, and tightening it would refuse a strongly non-conformal `f` on a ring too short to resolve it. Also: a non-real or non-finite `f` now says so, instead of being reported as an asymmetry between two NaNs or reaching the kernel and failing on `isless`. Split into `_check_symmetry_at` and `_check_normalisation`, which name what they check rather than claiming to validate the function. Co-Authored-By: Claude Opus 5 (1M context) --- src/relations/entanglement.jl | 17 ++++--- src/relations/region_entropy.jl | 65 ++++++++++++++++++--------- test/relations/test_region_entropy.jl | 43 ++++++++++++++---- 3 files changed, 86 insertions(+), 39 deletions(-) diff --git a/src/relations/entanglement.jl b/src/relations/entanglement.jl index b93c827..22b1440 100644 --- a/src/relations/entanglement.jl +++ b/src/relations/entanglement.jl @@ -308,15 +308,14 @@ depends on it. end @experimental """ -Eq. (24)'s scaling function is not settled here. Its two stated properties are -checked where `f` is still a callable, in [`finite_size_entropy_report`](@ref): -reflection symmetry, and the normalisation `Σₖ Aₖ(2k-1)π = 1` read as `f'(0) = 1`. -Its FORM is not, and cannot be from here. The higher harmonics are the whole -difference between this and the conformal case, and separating them 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 even the two checks, `f` arriving as a number that can -only be asked to be positive, so a small one returns a negative entropy +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 a15787f..6751011 100644 --- a/src/relations/region_entropy.jl +++ b/src/relations/region_entropy.jl @@ -414,25 +414,44 @@ function _finite_size_row!(out, rel, A::Region, vars, atol) return _finite_size_row!(out, rel, (A,), vars, atol) end -# Two consequences of Eq. (24)'s `f(v) = Σₖ Aₖ sin((2k-1)πv)` under -# `Σₖ Aₖ(2k-1)π = 1`, neither needing a coefficient: the basis is symmetric about -# `v = 1/2`, and the normalisation is `f'(0) = 1`. The FORM is not checked, the -# higher harmonics being exactly what a disorder average would be needed to see. -function _check_scaling_function(f) - for v in (0.1, 0.25, 0.4) - a, m = f(v), f(1 - v) - 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.", - ) - end - v = 1e-5 - slope = f(v) / v - isapprox(slope, 1; atol=1e-6) || error( - "finite_size_entropy_report: f(v)/v → $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.", +# 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 @@ -489,8 +508,8 @@ function finite_size_entropy_report( c̃ = get(b, VariableKey(EffectiveCentralCharge), nothing) ξ = get(b, VariableKey(CorrelationLength), nothing) (c === nothing && c̃ === nothing) && return out - # Only where it is read; an unconsumed `f` changes nothing. - c̃ !== nothing && f !== nothing && bc isa PBC && _check_scaling_function(f) + 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) && @@ -509,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 983d08e..c98ea80 100644 --- a/test/relations/test_region_entropy.jl +++ b/test/relations/test_region_entropy.jl @@ -558,19 +558,46 @@ end # 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(1e-5) / 1e-5, 1; atol=1e-6) @test !isapprox(two(0.3), conf(0.3); rtol=1e-3) - @test length(call(two)) == 1 - @test !only(call(two)).pass # entropy built on `conf`, so it fails + 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 wrong `f` - # used to be a pass with a wrong constant. - @test_throws "f(v)/v" call(v -> sin(π * v)) # the chord, missing the 1/π - @test_throws "f(v)/v" call(v -> 2 * sin(π * 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) + ) - # Refused only where read, so an unconsumed `f` is not refused for being present. + # 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(