From e389a870fc1065656ffd7f4c030ec7e6a779a35f Mon Sep 17 00:00:00 2001 From: sotashimozono Date: Mon, 14 Sep 2026 11:32:56 +0000 Subject: [PATCH 1/4] An open chain has to say what its boundary entropy is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `CFTEntanglementOBC` is `S - (c/6)ln[2 chord] - ln g - c₁/2`, and `ln g` and `c₁/2` reach the residual only as that sum, neither depending on `ℓ`. So open chain data cannot separate them at any number of block sizes: a caller fitting `c₁` against this relation learns `2 ln g + c₁` and nothing else. This is worse than the scaling function `c₁′` absorbed, where more sizes at least helped. `ln_g` defaulted to 0 in `finite_size_entropy_report`, which is not declining to say, it is asserting a trivial boundary on the caller's behalf. A caller who never measured `ln g` and fitted `c₁` from the open chain got a passing row at every block size, with the boundary's physics sitting inside the constant they call `c₁`. Three tests in this repo were taking that default without saying so. The default is gone for an open chain; a ring never reads `ln_g` and is not asked. The degeneracy itself cannot be fixed, so it is written into the relation's docstring and pinned by a test that runs the same data under both readings and asserts the residuals agree, since a report that passes on both must not be read as having checked either. Also records, in the report's docstring, that one region is not evidence about `c` or `c̃`: with one row the free constant can be chosen after the fact to zero the residual for any charge, so what passes is the triple's mutual consistency. The docstring's own example had exactly one region. Co-Authored-By: Claude Opus 5 (1M context) --- Project.toml | 2 +- src/relations/entanglement.jl | 6 +++++ src/relations/region_entropy.jl | 29 +++++++++++++++++++++-- test/relations/test_region_entropy.jl | 34 ++++++++++++++++++++++++--- 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/Project.toml b/Project.toml index 48a8d70..697f064 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "AbstractQAtlas" uuid = "dcea2817-62f8-4a75-b498-1b50a9ed1e4d" -version = "0.7.11" +version = "0.8.0" authors = ["sota shimozono "] [deps] diff --git a/src/relations/entanglement.jl b/src/relations/entanglement.jl index 22b1440..b3a4220 100644 --- a/src/relations/entanglement.jl +++ b/src/relations/entanglement.jl @@ -224,6 +224,12 @@ than `L/π`, and an open chain has a boundary entropy `ln g` (Affleck & Ludwig) that a ring does not. `c₁` is the same constant as in the ring, entering halved, so reading one geometry's data with the other's formula misses all three. + +`ln g` and `c₁` reach the residual only as the sum `ln g + c₁/2`, and neither +depends on `ℓ`, so open-chain data cannot separate them at any number of block +sizes: a caller who fits `c₁` against this relation learns `2 ln g + c₁` and +nothing more. `c₁` has to come from the ring or the infinite chain, which is +what "the same constant as in the ring" is for. """ @relation :entanglement CFTEntanglementOBC(S, c::CentralCharge, L, ℓ, c₁, ln_g) = begin _require_block(:CFTEntanglementOBC, L, ℓ) diff --git a/src/relations/region_entropy.jl b/src/relations/region_entropy.jl index 6751011..87991f4 100644 --- a/src/relations/region_entropy.jl +++ b/src/relations/region_entropy.jl @@ -479,7 +479,16 @@ The central charge is read from the bag, as `CentralCharge` and, when a random critical chain is being checked, `EffectiveCentralCharge`; the latter also needs `f`, the scaling function, and its own constant `c₁′`. The non-universal constants are arguments because they are not quantities: `c₁`, and `ln_g` for -the boundary entropy an open chain carries. +the boundary entropy an open chain carries. `ln_g` has no default: it and `c₁` +enter [`CFTEntanglementOBC`](@ref) only as a sum, so it is asked for rather than +assumed to be zero. + +A single region is not evidence that `c` or `c̃` is right. Each closed form has +one free constant, so with one row that constant can be chosen after the fact to +zero the residual for ANY central charge, and what passes is the mutual +consistency of the triple rather than the charge. Two or more block sizes are +what make the charge falsifiable; the slope relations, which carry no constant, +are the sharper route where an infinite chain is available. A region whose sites are not integers is skipped, having no adjacency to count cuts with. A region of integer sites lying off the chain `bc` declares is not @@ -492,7 +501,13 @@ finite_size_entropy_report(b, PBC(64); c₁=0.4785) ``` """ function finite_size_entropy_report( - b::Bag, bc::BoundaryCondition; c₁::Real, ln_g::Real=0, f=nothing, c₁′::Real=0, atol=1e-8 + b::Bag, + bc::BoundaryCondition; + c₁::Real, + ln_g::Union{Real,Nothing}=nothing, + f=nothing, + c₁′::Real=0, + atol=1e-8, ) # An empty report must mean "no region matched", so a boundary condition with no # form here is refused rather than producing one: it would otherwise be a @@ -501,6 +516,16 @@ function finite_size_entropy_report( bc isa Union{Infinite,OBC,PBC} || error( "finite_size_entropy_report: no finite-size form registered for $(typeof(bc))." ) + # `ln g` and `c₁/2` are a fixed sum in `CFTEntanglementOBC`, so a default would be + # asserting a trivial boundary rather than declining to say. Asked for, not assumed. + bc isa OBC && + ln_g === nothing && + error( + "finite_size_entropy_report: an open chain needs `ln_g`. It enters only as " * + "`ln g + c₁/2`, so this relation cannot separate it from `c₁` at any number " * + "of block sizes, and defaulting it to 0 would claim a boundary entropy of " * + "zero on the caller's behalf. Pass `ln_g=0.0` to say that deliberately.", + ) out = RegionFiniteSizeRow[] ents = _region_entropies(b) isempty(ents) && return out diff --git a/test/relations/test_region_entropy.jl b/test/relations/test_region_entropy.jl index c98ea80..853d939 100644 --- a/test/relations/test_region_entropy.jl +++ b/test/relations/test_region_entropy.jl @@ -491,10 +491,38 @@ end # the distinction the cut count exists for. at_end = bag(entanglement_entropy(Region(1:16...)) => 0.0, CentralCharge => c) in_bulk = bag(entanglement_entropy(Region(20:35...)) => 0.0, CentralCharge => c) - @test length(finite_size_entropy_report(at_end, OBC(N); c₁=c₁, atol=1e-12)) == 1 - @test only(finite_size_entropy_report(at_end, OBC(N); c₁=c₁, atol=1e-12)).relation isa + @test length(finite_size_entropy_report(at_end, OBC(N); c₁=c₁, ln_g=0.0, atol=1e-12)) == + 1 + @test only(finite_size_entropy_report(at_end, OBC(N); c₁=c₁, ln_g=0.0, atol=1e-12)).relation isa CFTEntanglementOBC - @test isempty(finite_size_entropy_report(in_bulk, OBC(N); c₁=c₁, atol=1e-12)) + @test isempty(finite_size_entropy_report(in_bulk, OBC(N); c₁=c₁, ln_g=0.0, atol=1e-12)) + + # `ln g` and `c₁/2` are a fixed sum, so the open chain cannot tell them apart at + # any number of block sizes. Pinned, because a report that passes on both readings + # of the same data must not be read as having checked either one. + lng, N_ = 0.3, 256 + obc(ℓ) = (c / 6) * log((2 * N_ / π) * sin(π * ℓ / N_)) + lng + c₁ / 2 + two_blocks = bag( + entanglement_entropy(Region(1:16...)) => obc(16), + entanglement_entropy(Region(1:64...)) => obc(64), + CentralCharge => c, + ) + truth = finite_size_entropy_report(two_blocks, OBC(N_); c₁=c₁, ln_g=lng, atol=1e-12) + shifted = finite_size_entropy_report( + two_blocks, OBC(N_); c₁=2 * lng + c₁, ln_g=0.0, atol=1e-12 + ) + @test length(truth) == 2 + @test all(r -> r.pass, truth) + @test all(r -> r.pass, shifted) + @test [r.residual for r in truth] ≈ [r.residual for r in shifted] atol = 1e-14 + + # And the default that would have made that silent is gone: an open chain has to + # say what its boundary entropy is, even when the answer is zero. + @test_throws "an open chain needs `ln_g`" finite_size_entropy_report( + two_blocks, OBC(N_); c₁=c₁, atol=1e-12 + ) + # A ring never reads it, so nothing is asked there. + @test finite_size_entropy_report(two_blocks, PBC(N_); c₁=c₁, atol=1e-12) isa Vector # An infinite chain has no L to supply and gets Eq. (4). binf = bag( From d75d8697989480a19c83c8a0e4f3ebd267ae84d3 Mon Sep 17 00:00:00 2001 From: sotashimozono Date: Mon, 14 Sep 2026 11:36:57 +0000 Subject: [PATCH 2/4] Keep the ln_g default: the refusal did not catch the bug it was named for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removing it forced a keystroke and changed no outcome. A caller who never measured `ln g` and fitted `c₁` from the open chain passed at every block size before, and passes at every block size after typing `ln_g=0.0`. The refusal was shaped like a check while being a signal, which is the thing this branch exists to complain about, and it charged a breaking bump for it. What is left costs nothing and is what actually helps: the degeneracy written into `CFTEntanglementOBC`'s docstring, the test running the same data under both readings and asserting the residuals agree, and the note that one region is not evidence about a central charge. The three call sites now pass `ln_g` explicitly, which says what they assume without requiring it of anyone else. Back to a patch bump, since nothing breaks. Co-Authored-By: Claude Opus 5 (1M context) --- Project.toml | 2 +- src/relations/region_entropy.jl | 24 ++++-------------------- test/relations/test_region_entropy.jl | 8 -------- 3 files changed, 5 insertions(+), 29 deletions(-) diff --git a/Project.toml b/Project.toml index 697f064..edad342 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "AbstractQAtlas" uuid = "dcea2817-62f8-4a75-b498-1b50a9ed1e4d" -version = "0.8.0" +version = "0.7.12" authors = ["sota shimozono "] [deps] diff --git a/src/relations/region_entropy.jl b/src/relations/region_entropy.jl index 87991f4..acd3b0f 100644 --- a/src/relations/region_entropy.jl +++ b/src/relations/region_entropy.jl @@ -479,9 +479,9 @@ The central charge is read from the bag, as `CentralCharge` and, when a random critical chain is being checked, `EffectiveCentralCharge`; the latter also needs `f`, the scaling function, and its own constant `c₁′`. The non-universal constants are arguments because they are not quantities: `c₁`, and `ln_g` for -the boundary entropy an open chain carries. `ln_g` has no default: it and `c₁` -enter [`CFTEntanglementOBC`](@ref) only as a sum, so it is asked for rather than -assumed to be zero. +the boundary entropy an open chain carries. `ln_g` and `c₁` enter +[`CFTEntanglementOBC`](@ref) only as a sum, so no number of block sizes separates +them; see that relation. A single region is not evidence that `c` or `c̃` is right. Each closed form has one free constant, so with one row that constant can be chosen after the fact to @@ -501,13 +501,7 @@ finite_size_entropy_report(b, PBC(64); c₁=0.4785) ``` """ function finite_size_entropy_report( - b::Bag, - bc::BoundaryCondition; - c₁::Real, - ln_g::Union{Real,Nothing}=nothing, - f=nothing, - c₁′::Real=0, - atol=1e-8, + b::Bag, bc::BoundaryCondition; c₁::Real, ln_g::Real=0, f=nothing, c₁′::Real=0, atol=1e-8 ) # An empty report must mean "no region matched", so a boundary condition with no # form here is refused rather than producing one: it would otherwise be a @@ -516,16 +510,6 @@ function finite_size_entropy_report( bc isa Union{Infinite,OBC,PBC} || error( "finite_size_entropy_report: no finite-size form registered for $(typeof(bc))." ) - # `ln g` and `c₁/2` are a fixed sum in `CFTEntanglementOBC`, so a default would be - # asserting a trivial boundary rather than declining to say. Asked for, not assumed. - bc isa OBC && - ln_g === nothing && - error( - "finite_size_entropy_report: an open chain needs `ln_g`. It enters only as " * - "`ln g + c₁/2`, so this relation cannot separate it from `c₁` at any number " * - "of block sizes, and defaulting it to 0 would claim a boundary entropy of " * - "zero on the caller's behalf. Pass `ln_g=0.0` to say that deliberately.", - ) out = RegionFiniteSizeRow[] ents = _region_entropies(b) isempty(ents) && return out diff --git a/test/relations/test_region_entropy.jl b/test/relations/test_region_entropy.jl index 853d939..9a67821 100644 --- a/test/relations/test_region_entropy.jl +++ b/test/relations/test_region_entropy.jl @@ -516,14 +516,6 @@ end @test all(r -> r.pass, shifted) @test [r.residual for r in truth] ≈ [r.residual for r in shifted] atol = 1e-14 - # And the default that would have made that silent is gone: an open chain has to - # say what its boundary entropy is, even when the answer is zero. - @test_throws "an open chain needs `ln_g`" finite_size_entropy_report( - two_blocks, OBC(N_); c₁=c₁, atol=1e-12 - ) - # A ring never reads it, so nothing is asked there. - @test finite_size_entropy_report(two_blocks, PBC(N_); c₁=c₁, atol=1e-12) isa Vector - # An infinite chain has no L to supply and gets Eq. (4). binf = bag( entanglement_entropy(Region(1:8...)) => (c / 3) * log(8) + c₁, CentralCharge => c From 1fa5d8f19f1e44b28ffc88c796ec3ca20b11eceb Mon Sep 17 00:00:00 2001 From: sotashimozono Date: Mon, 14 Sep 2026 11:49:48 +0000 Subject: [PATCH 3/4] Name the last two bare slots, and fix the parents the move left behind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `F` is written by three relations meaning three different things: the free energy in `FreeEnergyLegendre`, the cloning fidelity in `CloningFidelityBound`, and the mean force in `EhrenfestMomentum`. The first two were typed already, so the third was the last `F` that could still meet them on a name. It is a `Force` now. `κ` is the same story: the thermal conductivity in `WiedemannFranz` and `ThermoelectricFigureOfMerit`, both typed, and the large-spin moment exponent in `LargeSpinMoment`, which was not. `S_eff ∼ Ω^{-κ}` makes it an exponent, so `EffectiveMomentExponent` joins `AbstractExponent` beside `LargeSpinExponent`, the other half of the same relation. Neither had a hand-written `quantities`, so nothing had to be retired with them. Separately: all seven exponents moved to `AbstractExponent` in #153 still had `<: AbstractQuantity` in the first line of their own docstrings. The parent changed and the line documenting it did not, which is a claim the code contradicts and no test could see. Co-Authored-By: Claude Opus 5 (1M context) --- src/core/quantities.jl | 14 ++++++++++++++ src/core/relation_variables.jl | 29 ++++++++++++++++++++++------- src/relations/quantum.jl | 4 ++-- src/relations/scaling.jl | 5 ++++- test/relations/test_derivation.jl | 14 ++++++++++++++ test/relations/test_quantum.jl | 25 +++++++++++++++++++------ 6 files changed, 75 insertions(+), 16 deletions(-) diff --git a/src/core/quantities.jl b/src/core/quantities.jl index dce7f8a..46c490c 100644 --- a/src/core/quantities.jl +++ b/src/core/quantities.jl @@ -287,6 +287,20 @@ calculation. struct EnergyVariance <: AbstractQuantity end export EnergyVariance +""" + Force() <: AbstractQuantity + +The mean force `⟨F⟩ = −⟨∂V/∂x⟩`, which [`EhrenfestMomentum`](@ref) equates to +`d⟨p⟩/dt`. + +Not the free energy that wears the same letter in [`FreeEnergyLegendre`](@ref), +nor the cloning fidelity in [`CloningFidelityBound`](@ref): three quantities +share `F` across the registry, and the type is what keeps them apart when a bag +is read rather than a symbol. +""" +struct Force <: AbstractQuantity end +export Force + # ─── Order parameters and responses (tensors in spin space) ───────────── # # These were the quantities blurred into scalars by baking a component diff --git a/src/core/relation_variables.jl b/src/core/relation_variables.jl index 86c80e0..39142f1 100644 --- a/src/core/relation_variables.jl +++ b/src/core/relation_variables.jl @@ -61,7 +61,7 @@ export AbstractExponent # touching the others, since only the thirteen `:scaling` relations are annotated. """ - SpecificHeatExponent() <: AbstractQuantity + SpecificHeatExponent() <: AbstractExponent `α`, the specific heat's divergence at a critical point, `c ∼ |t|^{-α}`. """ @@ -69,7 +69,7 @@ struct SpecificHeatExponent <: AbstractExponent end export SpecificHeatExponent """ - OrderParameterExponent() <: AbstractQuantity + OrderParameterExponent() <: AbstractExponent `β`, the order parameter's vanishing, `m ∼ (-t)^β`. Not [`InverseTemperature`](@ref), which wears the same letter across most of this @@ -79,7 +79,7 @@ struct OrderParameterExponent <: AbstractExponent end export OrderParameterExponent """ - SusceptibilityExponent() <: AbstractQuantity + SusceptibilityExponent() <: AbstractExponent `γ`, the susceptibility's divergence, `χ ∼ |t|^{-γ}`. """ @@ -87,7 +87,7 @@ struct SusceptibilityExponent <: AbstractExponent end export SusceptibilityExponent """ - CriticalIsothermExponent() <: AbstractQuantity + CriticalIsothermExponent() <: AbstractExponent `δ`, the critical isotherm's shape, `m ∼ h^{1/δ}` at `t = 0`. """ @@ -95,7 +95,7 @@ struct CriticalIsothermExponent <: AbstractExponent end export CriticalIsothermExponent """ - CorrelationLengthExponent() <: AbstractQuantity + CorrelationLengthExponent() <: AbstractExponent `ν`, the correlation length's divergence, `ξ ∼ |t|^{-ν}`. The exponent, where [`CorrelationLength`](@ref) is the length itself. @@ -104,7 +104,7 @@ struct CorrelationLengthExponent <: AbstractExponent end export CorrelationLengthExponent """ - AnomalousDimension() <: AbstractQuantity + AnomalousDimension() <: AbstractExponent `η`, the correlation function's decay at criticality, `G(r) ∼ r^{-(d-2+η)}`. """ @@ -112,7 +112,7 @@ struct AnomalousDimension <: AbstractExponent end export AnomalousDimension """ - LargeSpinExponent() <: AbstractQuantity + LargeSpinExponent() <: AbstractExponent `ζ` of the large-spin fixed point, where the effective moment GROWS under renormalization ([IgloiMonthus2005](@cite), §A.5). A random-walk argument on the @@ -125,6 +125,21 @@ occupation in `(0, 1)`. struct LargeSpinExponent <: AbstractExponent end export LargeSpinExponent +""" + EffectiveMomentExponent() <: AbstractExponent + +`κ` of the large-spin fixed point, by which the effective moment grows as the +energy scale falls, `S_eff ∼ Ω^{-κ}` ([IgloiMonthus2005](@cite), §8.2). Tied to +the other two by [`LargeSpinMoment`](@ref), `κ = dζ/z`, which is how a measured +`κ` gives the dynamical exponent when `ζ` is known. + +Not the thermal conductivity that wears the same letter in +[`WiedemannFranz`](@ref) and [`ThermoelectricFigureOfMerit`](@ref), which is why +it is a type here and not a shared symbol. +""" +struct EffectiveMomentExponent <: AbstractExponent end +export EffectiveMomentExponent + """ RelationVariable diff --git a/src/relations/quantum.jl b/src/relations/quantum.jl index 6ceb799..c86c7ad 100644 --- a/src/relations/quantum.jl +++ b/src/relations/quantum.jl @@ -64,9 +64,9 @@ the mean momentum obeys the classical force law, `d⟨p⟩/dt = −⟨∂V/∂x⟩ = ⟨F⟩`, the quantum counterpart of Newton's second law. Variables: `dp_dt`, -`F` = `⟨F⟩`. +and `F` = `⟨F⟩` as a [`Force`](@ref). """ -@relation :quantum EhrenfestMomentum(dp_dt, F) = dp_dt - F +@relation :quantum EhrenfestMomentum(dp_dt, F::Force) = dp_dt - F """ EnergyVarianceEigenstate <: AbstractRelation diff --git a/src/relations/scaling.jl b/src/relations/scaling.jl index b865c8c..52a7176 100644 --- a/src/relations/scaling.jl +++ b/src/relations/scaling.jl @@ -577,7 +577,10 @@ instance is Eqs. (8.6)-(8.8), §8.2, where `ζ = 1/2` and `κ = 0.22(1)` is measured, giving `z = 1/(2κ)`. """ @relation :scaling LargeSpinMoment( - κ, d::SpatialDimension, ζ::LargeSpinExponent, z::DynamicalExponent + κ::EffectiveMomentExponent, + d::SpatialDimension, + ζ::LargeSpinExponent, + z::DynamicalExponent, ) = κ * z - d * ζ """ diff --git a/test/relations/test_derivation.jl b/test/relations/test_derivation.jl index 9197f3c..e62288f 100644 --- a/test/relations/test_derivation.jl +++ b/test/relations/test_derivation.jl @@ -394,6 +394,20 @@ end @test CorrelationMatrixEigenvalue in variable_types(EntanglementSpectrumCorrelation()) @test LargeSpinExponent !== CorrelationMatrixEigenvalue + # `κ` in the same relation was the other half of it, and shares its letter with + # the thermal conductivity in two transport relations. Those were typed already, + # so this was the last bare `κ` that could still meet them on a name. + @test EffectiveMomentExponent in variable_types(LargeSpinMoment()) + @test ThermalConductivity in + Set(Base.typename(T).wrapper for T in variable_types(WiedemannFranz())) + @test VariableKey(EffectiveMomentExponent) != VariableKey(ThermalConductivity) + + # It is an exponent, so it joins `AbstractExponent` rather than being a quantity a + # bag holds a measurement of, and `quantities` leaves it out for that reason. + @test EffectiveMomentExponent <: AbstractExponent + @test !(EffectiveMomentExponent <: AbstractQuantity) + @test !(EffectiveMomentExponent in quantities(LargeSpinMoment())) + # And the exemplar stays split. @test OrderParameterExponent in variable_types(Rushbrooke()) @test !(InverseTemperature in variable_types(Rushbrooke())) diff --git a/test/relations/test_quantum.jl b/test/relations/test_quantum.jl index 5a9c13f..e59d03b 100644 --- a/test/relations/test_quantum.jl +++ b/test/relations/test_quantum.jl @@ -109,20 +109,19 @@ end # The genuinely GENERIC quantum relations stay symbol-keyed, and should: their # variables do not name quantities. `RobertsonUncertainty(ΔA, ΔB, comm)` is about # two arbitrary observables; `MandelstamTammBound(τ, ΔE)` about a time and an - # energy spread; `EhrenfestMomentum`/`Position` about expectation values of - # whichever operator. Typing those would need quantities that do not exist and - # arguably should not. + # energy spread; `EhrenfestPosition` about `d⟨x⟩/dt = ⟨p⟩/m` for whichever + # operator. Typing those would need quantities that do not exist and arguably + # should not. # # `LiebRobinsonBound` used to be in this list and is not any more. Its `v_LR` is - # not a generic symbol — it IS a named quantity, `LiebRobinsonVelocity` — so - # typing it makes the inequality discoverable from the quantity + # not a generic symbol, it IS a named quantity, `LiebRobinsonVelocity`, so typing + # it makes the inequality discoverable from the quantity # (`relations_constraining`), which is the whole point of the type-keyed front # door. Its other slot, `v`, stays untyped: that is a measured information # velocity, and nothing names it yet. @test all( r -> isempty(variable_types(r)), ( - EhrenfestMomentum(), EhrenfestPosition(), HellmannFeynman(), RobertsonUncertainty(), @@ -131,6 +130,20 @@ end EnergyVarianceEigenstate(), ), ) + + # `EhrenfestMomentum` left that group for the same reason `LiebRobinsonBound` + # did. `⟨F⟩` is not a generic expectation value, it is a force, and three + # relations in the registry write `F` meaning three different things: the free + # energy in `FreeEnergyLegendre`, the cloning fidelity in `CloningFidelityBound`, + # and this. The other two were already typed, so this one was the last `F` that + # could still collide with them on the name-keyed side. + @test variable_types(EhrenfestMomentum()) == (Force,) + @test quantities(EhrenfestMomentum()) == (Force,) + @test Force !== FreeEnergy + @test VariableKey(Force) != VariableKey(FreeEnergy) + @test check(EhrenfestMomentum(), bag(Force => -0.8); dp_dt=-0.8, atol=1e-12) + @test !check(EhrenfestMomentum(), bag(Force => 0.8); dp_dt=-0.8, atol=1e-9) + @test EhrenfestMomentum() in relations_constraining(Force) end @testset "LiebRobinsonBound is keyed on LiebRobinsonVelocity" begin From d8d75782d386ad29e7690b1d6788830744ac1154 Mon Sep 17 00:00:00 2001 From: sotashimozono Date: Mon, 14 Sep 2026 12:54:08 +0000 Subject: [PATCH 4/4] Review: three claims that were wrong, and the slot the scope missed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prose this branch added to write down two facts got three of its own facts wrong, which is the same defect it exists to fix. "Each closed form has one free constant" is false for `OffCriticalEntanglementSaturation`: `S - ncuts (c/6) ln ξ` has no additive constant at all, `ξ` and `c` both being read from the bag, so a single region DOES constrain the charge there. Measured: residual 8.3e-5 at c = 0.4999 and exactly zero at 0.5. "Three relations write `F`" is four: `GrandPotentialLegendre` also types it `FreeEnergy`. And "the other two were already typed" is wrong about `CloningFidelityBound`, whose `F` is bare and stays bare under the `@bound` convention that types the ceiling and not the measured side. That claim came from reading `variable_types` as though it were keyed by the slot I cared about, when it returns the set of types present. "The last `F` that could still collide on the name-keyed side" has it backwards. The name-keyed node stays shared and always will, `variables` being deliberately unchanged by typing. What a type buys is that the bag-keyed graph separates them. `φ` in `ActivatedMomentGrowth` was the third bare slot of this kind and the scope missed it, because the criterion used was "the letter is typed elsewhere" rather than the design doc's own "identity-bearing". It is the structural twin of `κ`: the moment exponent of the other random fixed point, pinned by an identity among otherwise-typed exponents, used in one relation. Typing it also makes the name `EffectiveMomentExponent` wrong, since "effective moment" covers both fixed points; `LargeSpinMomentExponent` and `ActivatedMomentExponent` name the fixed point, as `LargeSpinExponent` beside them already does. Pre-existing, same class: `LatentHeat`'s docstring claimed `AbstractQuantity` against an `AbstractThermalPotential` struct. `check_all(::NamedTuple)`'s docstring was severed from it by an intervening definition, so the symbol resolved to the `Bag` method's docstring and the crossref gate, being method-blind, passed. `CFTEntanglementInfinite` was missing from `relations_constraining(VonNeumannEntropy)` while both its siblings were there. The degeneracy test compared two equivalent readings and never an inequivalent one, so it showed agreement without showing the check could disagree. A reading off by 0.01 in `ln g` is now asserted to fail at every block size. Co-Authored-By: Claude Opus 5 (1M context) --- src/core/quantities.jl | 22 ++++++++-------- src/core/relation_variables.jl | 36 +++++++++++++++++++++------ src/relations/interface.jl | 10 ++++---- src/relations/quantity_links.jl | 7 +++--- src/relations/region_entropy.jl | 13 +++++----- src/relations/scaling.jl | 7 ++++-- test/relations/test_derivation.jl | 27 +++++++++++--------- test/relations/test_quantum.jl | 14 +++++------ test/relations/test_region_entropy.jl | 12 ++++++--- 9 files changed, 91 insertions(+), 57 deletions(-) diff --git a/src/core/quantities.jl b/src/core/quantities.jl index 46c490c..dda5927 100644 --- a/src/core/quantities.jl +++ b/src/core/quantities.jl @@ -210,7 +210,7 @@ struct ParticleNumber <: AbstractQuantity end export ParticleNumber """ - LatentHeat() <: AbstractQuantity + LatentHeat() <: AbstractThermalPotential The latent heat `L = T ΔS` of a first-order transition — the entropy jump across the phase boundary times the temperature. Enters the @@ -277,16 +277,6 @@ The potential-energy expectation `⟨V⟩` — the `V` of the virial theorem struct PotentialEnergy <: AbstractThermalPotential end export PotentialEnergy -""" - EnergyVariance() <: AbstractQuantity - -The energy variance `Var(H) = ⟨H²⟩ − ⟨H⟩²` — zero iff the state is an -exact eigenstate, the convergence metric of a variational / DMRG -calculation. -""" -struct EnergyVariance <: AbstractQuantity end -export EnergyVariance - """ Force() <: AbstractQuantity @@ -301,6 +291,16 @@ is read rather than a symbol. struct Force <: AbstractQuantity end export Force +""" + EnergyVariance() <: AbstractQuantity + +The energy variance `Var(H) = ⟨H²⟩ − ⟨H⟩²` — zero iff the state is an +exact eigenstate, the convergence metric of a variational / DMRG +calculation. +""" +struct EnergyVariance <: AbstractQuantity end +export EnergyVariance + # ─── Order parameters and responses (tensors in spin space) ───────────── # # These were the quantities blurred into scalars by baking a component diff --git a/src/core/relation_variables.jl b/src/core/relation_variables.jl index 39142f1..51dfac5 100644 --- a/src/core/relation_variables.jl +++ b/src/core/relation_variables.jl @@ -126,19 +126,39 @@ struct LargeSpinExponent <: AbstractExponent end export LargeSpinExponent """ - EffectiveMomentExponent() <: AbstractExponent + LargeSpinMomentExponent() <: AbstractExponent `κ` of the large-spin fixed point, by which the effective moment grows as the -energy scale falls, `S_eff ∼ Ω^{-κ}` ([IgloiMonthus2005](@cite), §8.2). Tied to -the other two by [`LargeSpinMoment`](@ref), `κ = dζ/z`, which is how a measured -`κ` gives the dynamical exponent when `ζ` is known. +energy scale falls, `S_eff ∼ Ω^{-κ}` ([IgloiMonthus2005](@cite), §A.5, with §8.2 +the 1D instance). Tied to the other two by [`LargeSpinMoment`](@ref), +`κ = dζ/z`, which is how a measured `κ` gives the dynamical exponent when `ζ` is +known. + +Named for its fixed point rather than for the physics, like +[`LargeSpinExponent`](@ref) beside it: the infinite-disorder fixed point has a +moment exponent too, [`ActivatedMomentExponent`](@ref), and "effective moment" +alone does not say which. Not the thermal conductivity that wears the same letter in -[`WiedemannFranz`](@ref) and [`ThermoelectricFigureOfMerit`](@ref), which is why -it is a type here and not a shared symbol. +[`WiedemannFranz`](@ref) and [`ThermoelectricFigureOfMerit`](@ref). +""" +struct LargeSpinMomentExponent <: AbstractExponent end +export LargeSpinMomentExponent + +""" + ActivatedMomentExponent() <: AbstractExponent + +`φ` of the infinite-disorder fixed point, where the moment grows in the logarithm +of the energy scale rather than a power of it, `μ ∼ |ln Ω|^φ` +([IgloiMonthus2005](@cite), §A.3). Pinned by [`ActivatedMomentGrowth`](@ref), +`φψ = d - x_m`. + +The activated twin of [`LargeSpinMomentExponent`](@ref). It is bookkeeping +rather than a separately measured number, which is a statement about how it is +obtained and not about whether it is a quantity of its own. """ -struct EffectiveMomentExponent <: AbstractExponent end -export EffectiveMomentExponent +struct ActivatedMomentExponent <: AbstractExponent end +export ActivatedMomentExponent """ RelationVariable diff --git a/src/relations/interface.jl b/src/relations/interface.jl index 6966cca..f720405 100644 --- a/src/relations/interface.jl +++ b/src/relations/interface.jl @@ -952,16 +952,16 @@ function relation_report(data::NamedTuple; atol=0, domain::Union{Nothing,Symbol} end export relation_report +# The shared "all applicable relations passed, and at least one applied" rule in one +# place, so the NamedTuple and Bag `check_all` methods cannot drift on "empty ⇒ false". +_all_passed(report) = !isempty(report) && all(row -> row.pass, report) + """ check_all(data::NamedTuple; atol=0, domain=nothing) -> Bool -`true` iff every applicable relation passes on `data` — and at least one +`true` iff every applicable relation passes on `data`, and at least one relation applies: an empty match is `false`, never a silent green. """ -# the shared "all applicable relations passed, and at least one applied" rule — one -# place, so the NamedTuple and Bag `check_all` methods can't drift on "empty ⇒ false". -_all_passed(report) = !isempty(report) && all(row -> row.pass, report) - function check_all(data::NamedTuple; atol=0, domain::Union{Nothing,Symbol}=nothing) return _all_passed(relation_report(data; atol=atol, domain=domain)) end diff --git a/src/relations/quantity_links.jl b/src/relations/quantity_links.jl index 32d3f57..41f21ad 100644 --- a/src/relations/quantity_links.jl +++ b/src/relations/quantity_links.jl @@ -26,6 +26,7 @@ also_constrains(::SusceptibilityResponse) = (Magnetization,) # fundamental: χ also_constrains(::ParticleNumberResponse) = (GrandPotential,) # grand-canonical: N = −∂Ω/∂μ 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(::CFTEntanglementPBC) = (VonNeumannEntropy,) # entanglement: S(ℓ) on a ring also_constrains(::CFTEntanglementOBC) = (VonNeumannEntropy,) # entanglement: S(ℓ) at an open end @@ -147,9 +148,9 @@ quantities(::TypicalBelowAverage) = (Typical, DisorderAveraged) quantities(::AnnealedFreeEnergyBound) = (FreeEnergy,) # ── Quantum-mechanical foundations ── -# VirialTheorem is type-keyed (quantum.jl), `quantities` auto-derived. The Ehrenfest / -# Hellmann–Feynman / uncertainty / Lieb–Robinson relations stay symbol-keyed (generic -# operators / derivatives). EnergyVarianceEigenstate stays symbol-keyed — its subject +# VirialTheorem and EhrenfestMomentum are type-keyed (quantum.jl), `quantities` +# auto-derived, as is LiebRobinsonBound. EhrenfestPosition, Hellmann-Feynman and the +# uncertainty relations stay symbol-keyed (generic operators and derivatives). EnergyVarianceEigenstate stays symbol-keyed — its subject # EnergyVariance enters only through the moment combination ⟨H²⟩ − E², not a slot. quantities(::EnergyVarianceEigenstate) = (EnergyVariance,) diff --git a/src/relations/region_entropy.jl b/src/relations/region_entropy.jl index acd3b0f..ce0664d 100644 --- a/src/relations/region_entropy.jl +++ b/src/relations/region_entropy.jl @@ -483,12 +483,13 @@ the boundary entropy an open chain carries. `ln_g` and `c₁` enter [`CFTEntanglementOBC`](@ref) only as a sum, so no number of block sizes separates them; see that relation. -A single region is not evidence that `c` or `c̃` is right. Each closed form has -one free constant, so with one row that constant can be chosen after the fact to -zero the residual for ANY central charge, and what passes is the mutual -consistency of the triple rather than the charge. Two or more block sizes are -what make the charge falsifiable; the slope relations, which carry no constant, -are the sharper route where an infinite chain is available. +A single region is usually not evidence that `c` or `c̃` is right. The four +logarithmic forms each carry one free constant, so with one row that constant can +be chosen after the fact to zero the residual for ANY central charge, and what +passes is the mutual consistency of the triple rather than the charge. Two or +more block sizes are what make the charge falsifiable there. The exceptions are +the forms with no constant to spend, [`OffCriticalEntanglementSaturation`](@ref) +and the slope relations, where one row does constrain the charge. A region whose sites are not integers is skipped, having no adjacency to count cuts with. A region of integer sites lying off the chain `bc` declares is not diff --git a/src/relations/scaling.jl b/src/relations/scaling.jl index 52a7176..6a73455 100644 --- a/src/relations/scaling.jl +++ b/src/relations/scaling.jl @@ -302,7 +302,10 @@ for the RTFIC is that review's Eq. (3.18), §3.5, reached by a different route. Variables: `φ`, `d`, `x_m`, `ψ`. """ @relation :scaling ActivatedMomentGrowth( - φ, d::SpatialDimension, x_m::ScalingDimension, ψ::ActivatedExponent + φ::ActivatedMomentExponent, + d::SpatialDimension, + x_m::ScalingDimension, + ψ::ActivatedExponent, ) = φ * ψ - (d - x_m) # ─── Appendix A: the four scaling types of a random system ─────────────── @@ -577,7 +580,7 @@ instance is Eqs. (8.6)-(8.8), §8.2, where `ζ = 1/2` and `κ = 0.22(1)` is measured, giving `z = 1/(2κ)`. """ @relation :scaling LargeSpinMoment( - κ::EffectiveMomentExponent, + κ::LargeSpinMomentExponent, d::SpatialDimension, ζ::LargeSpinExponent, z::DynamicalExponent, diff --git a/test/relations/test_derivation.jl b/test/relations/test_derivation.jl index e62288f..5b91a11 100644 --- a/test/relations/test_derivation.jl +++ b/test/relations/test_derivation.jl @@ -394,19 +394,22 @@ end @test CorrelationMatrixEigenvalue in variable_types(EntanglementSpectrumCorrelation()) @test LargeSpinExponent !== CorrelationMatrixEigenvalue - # `κ` in the same relation was the other half of it, and shares its letter with - # the thermal conductivity in two transport relations. Those were typed already, - # so this was the last bare `κ` that could still meet them on a name. - @test EffectiveMomentExponent in variable_types(LargeSpinMoment()) + # `κ` in the same relation shares its letter with the thermal conductivity in two + # transport relations. As with `:F`, the name-keyed node stays shared; the type + # is what separates them once a bag is involved. + @test LargeSpinMomentExponent in variable_types(LargeSpinMoment()) + @test VariableKey(LargeSpinMomentExponent) != VariableKey(ThermalConductivity) @test ThermalConductivity in - Set(Base.typename(T).wrapper for T in variable_types(WiedemannFranz())) - @test VariableKey(EffectiveMomentExponent) != VariableKey(ThermalConductivity) - - # It is an exponent, so it joins `AbstractExponent` rather than being a quantity a - # bag holds a measurement of, and `quantities` leaves it out for that reason. - @test EffectiveMomentExponent <: AbstractExponent - @test !(EffectiveMomentExponent <: AbstractQuantity) - @test !(EffectiveMomentExponent in quantities(LargeSpinMoment())) + Set(AbstractQAtlas._family(T) for T in variable_types(WiedemannFranz())) + + # Each fixed point has its own moment exponent and they are not the same number: + # a power of Ω at the large-spin one, a power of |ln Ω| at the infinite-disorder + # one. Naming either of them for the physics alone would take the other's name. + @test ActivatedMomentExponent in variable_types(ActivatedMomentGrowth()) + @test LargeSpinMomentExponent !== ActivatedMomentExponent + @test isempty( + filter(t -> t === nothing, last.(variable_slots(ActivatedMomentGrowth()))) + ) # And the exemplar stays split. @test OrderParameterExponent in variable_types(Rushbrooke()) diff --git a/test/relations/test_quantum.jl b/test/relations/test_quantum.jl index e59d03b..ea20286 100644 --- a/test/relations/test_quantum.jl +++ b/test/relations/test_quantum.jl @@ -131,15 +131,15 @@ end ), ) - # `EhrenfestMomentum` left that group for the same reason `LiebRobinsonBound` - # did. `⟨F⟩` is not a generic expectation value, it is a force, and three - # relations in the registry write `F` meaning three different things: the free - # energy in `FreeEnergyLegendre`, the cloning fidelity in `CloningFidelityBound`, - # and this. The other two were already typed, so this one was the last `F` that - # could still collide with them on the name-keyed side. + # `⟨F⟩` is not a generic expectation value, it is a force. Four relations write + # `F` and they mean three things: the free energy in `FreeEnergyLegendre` and + # `GrandPotentialLegendre`, the cloning fidelity in `CloningFidelityBound`, and + # this. The name-keyed graph still puts all four at one node and always will, + # `variables` being deliberately unchanged by typing; what a type buys is that + # the bag-keyed graph does not. `CloningFidelityBound`'s `F` stays bare under the + # `@bound` convention that types the ceiling and not the measured side. @test variable_types(EhrenfestMomentum()) == (Force,) @test quantities(EhrenfestMomentum()) == (Force,) - @test Force !== FreeEnergy @test VariableKey(Force) != VariableKey(FreeEnergy) @test check(EhrenfestMomentum(), bag(Force => -0.8); dp_dt=-0.8, atol=1e-12) @test !check(EhrenfestMomentum(), bag(Force => 0.8); dp_dt=-0.8, atol=1e-9) diff --git a/test/relations/test_region_entropy.jl b/test/relations/test_region_entropy.jl index 9a67821..eac09bb 100644 --- a/test/relations/test_region_entropy.jl +++ b/test/relations/test_region_entropy.jl @@ -497,9 +497,8 @@ end CFTEntanglementOBC @test isempty(finite_size_entropy_report(in_bulk, OBC(N); c₁=c₁, ln_g=0.0, atol=1e-12)) - # `ln g` and `c₁/2` are a fixed sum, so the open chain cannot tell them apart at - # any number of block sizes. Pinned, because a report that passes on both readings - # of the same data must not be read as having checked either one. + # Pinned because a report that passes on both readings of the same data must not + # be read as having checked either one. lng, N_ = 0.3, 256 obc(ℓ) = (c / 6) * log((2 * N_ / π) * sin(π * ℓ / N_)) + lng + c₁ / 2 two_blocks = bag( @@ -516,6 +515,13 @@ end @test all(r -> r.pass, shifted) @test [r.residual for r in truth] ≈ [r.residual for r in shifted] atol = 1e-14 + # And the negative control the pair needs: a reading that does NOT preserve the + # sum is a different physical claim and is refused. Without this the block shows + # that two equivalent readings agree and never that an inequivalent one does not. + off = finite_size_entropy_report(two_blocks, OBC(N_); c₁=c₁, ln_g=lng + 0.01, atol=1e-9) + @test !any(r -> r.pass, off) + @test all(r -> isapprox(r.residual, -0.01; atol=1e-12), off) + # An infinite chain has no L to supply and gets Eq. (4). binf = bag( entanglement_entropy(Region(1:8...)) => (c / 3) * log(8) + c₁, CentralCharge => c