Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "AbstractQAtlas"
uuid = "dcea2817-62f8-4a75-b498-1b50a9ed1e4d"
version = "0.7.16"
version = "0.7.17"
authors = ["sota shimozono <shimozono-sota631@g.ecc.u-tokyo.ac.jp>"]

[deps]
Expand Down
87 changes: 87 additions & 0 deletions examples/critical_entanglement.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Reading a central charge off measured entanglement, and what a convention
# declaration buys.
#
# The numbers are not pasted in from anywhere: a critical free-fermion chain's
# block correlation matrix is a closed form, so the entropies below are computed
# here and the central charge that comes out is a measurement, not a fixture. Its
# distance from the exact `c = 1` is the finite-block correction, which is why
# `test/core/test_examples.jl` checks it to 1e-3 and not to machine precision.
#
# Wrapped in a module because `test/core/test_examples.jl` includes this file, and
# every test file is included into one namespace: `block_entropy` and a couple of
# block sizes at top level would be in every other test file's way.
#
# Run: julia --project=. examples/critical_entanglement.jl

module CriticalEntanglementExample

using AbstractQAtlas
using LinearAlgebra: eigvals, Symmetric

export block_entropy, central_charges

"""
correlation_matrix(ℓ) -> Symmetric

The `ℓ x ℓ` block of the half-filled critical free-fermion chain's two-point
function, `C_jk = sin(π(j-k)/2) / (π(j-k))`, `C_jj = 1/2`.
"""
function correlation_matrix(ℓ::Integer)
return Symmetric([
j == k ? 0.5 : sin(π * (j - k) / 2) / (π * (j - k)) for j in 1:ℓ, k in 1:ℓ
])
end

"Entanglement entropy of a block of `ℓ` sites, in nats."
function block_entropy(ℓ::Integer)
return free_fermion_entanglement_entropy(eigvals(correlation_matrix(ℓ)))
end

"""
central_charges(small = 16, large = 64) -> NamedTuple

`c` read off the same measurement entered three ways: in nats, in bits with
nothing declared, and in bits declared.

Two block sizes, because the charge is not falsifiable from one: the logarithmic
forms each carry a non-universal constant that can absorb any `c`, and the slope
through two blocks carries none. The middle entry is the failure the convention
layer exists for, and it is wrong by exactly the base nobody mentioned.
"""
function central_charges(small::Integer=16, large::Integer=64)
a, b = Region((1:small)...), Region((1:large)...)
span = log(large) - log(small)
slope(bg) = (bg[entanglement_entropy(b)] - bg[entanglement_entropy(a)]) / span
pack(f) = (entanglement_entropy(a) => f(small), entanglement_entropy(b) => f(large))
# Read out of the bag, so a declared convention reaches the slope through it.
c(bg) = derive_crosschecked(:c; dS_dlogℓ=slope(bg), ncuts=2)
in_bits(ℓ) = block_entropy(ℓ) / log(2)
return (
nats=c(bag(pack(block_entropy)...)),
bits_undeclared=c(bag(pack(in_bits)...)),
bits_declared=c(
bag(conventions(AbstractEntanglementMeasure => Bits), pack(in_bits)...)
),
)
end

end # module CriticalEntanglementExample

if abspath(PROGRAM_FILE) == @__FILE__
let cs = CriticalEntanglementExample.central_charges()
println("c from blocks of 16 and 64 sites, exact value 1:")
println(" nats, as the calculation produced them : ", round(cs.nats; digits=4))
println(
" the same numbers in bits, undeclared : ",
round(cs.bits_undeclared; digits=4),
)
println(
" which is 1/ln2 = ",
round(1 / log(2); digits=4),
", the base it was never told",
)
println(
" the same numbers in bits, declared : ", round(cs.bits_declared; digits=4)
)
end
end
29 changes: 15 additions & 14 deletions ext/AbstractQAtlasForwardDiffExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,24 @@
module AbstractQAtlasForwardDiffExt

using AbstractQAtlas
import AbstractQAtlas: peierls_current, thermal_derivative # extended below → must import
import AbstractQAtlas: nth_derivative, peierls_current, thermal_derivative
using AbstractQAtlas:
_genealogy_derivative, response_order, indices, Susceptibility, SpecificHeat, Energy
_genealogy_derivative,
_susceptibility_derivative,
indices,
Susceptibility,
SpecificHeat,
Energy
using ForwardDiff: derivative

# n-th derivative of a scalar function by nested ForwardDiff (n small —
# response orders are 1–3).
_nth(f, x, n::Integer) = n == 0 ? f(x) : _nth(y -> derivative(f, y), x, n - 1)

# `AutoDiff` is a route like the others: this is the method whose absence made
# every route-taking entry point special-case it by name.
nth_derivative(::AbstractQAtlas.AutoDiff, f, x, n::Integer) = _nth(f, x, n)

# ── generic, genealogy-driven response ────────────────────────────────────
# Which order, which field and the net sign all come from `_genealogy_derivative`
# (src/derivative_routes.jl), shared with the finite-difference routes so the two
Expand All @@ -28,19 +37,11 @@ function thermal_derivative(q::AbstractQuantity, F, x::Number)
return _genealogy_derivative(q, F, x, _nth)
end

# χ⁽ⁿ⁾_{α;β₁…βₙ} = −∂ⁿ⁺¹F/∂h_α∂h_{β₁}…∂h_{βₙ}. With a SINGLE-field function
# F(h) only the DIAGONAL component (all indices equal) is defined — an
# off-diagonal component needs partials w.r.t. distinct field directions,
# so guard against silently returning the diagonal for an off-diagonal ask.
# χ⁽ⁿ⁾_{α;β₁…βₙ} = −∂ⁿ⁺¹F/∂h_α∂h_{β₁}…∂h_{βₙ}, diagonal only from a single-field
# F(h). The guard and the order live in `_susceptibility_derivative`, shared with
# the finite-difference routes; this supplies the AD `nth`.
function thermal_derivative(χ::Susceptibility, F, h::Number)
idx = indices(χ)
all(==(idx[1]), idx) || error(
"thermal_derivative(::Susceptibility, F, h::Number) with a single-field " *
"function computes only the DIAGONAL χ⁽ⁿ⁾ (all indices equal); got " *
"off-diagonal $(idx). Pass a multi-field potential F(h⃗) and the field-" *
"component ordering: thermal_derivative(χ, F, h⃗, components).",
)
return -_nth(F, h, response_order(χ) + 1)
return _susceptibility_derivative(χ, F, h, _nth)
end

# Multi-field / off-diagonal: F is a function of a field VECTOR `h⃗`, and
Expand Down
1 change: 1 addition & 0 deletions src/AbstractQAtlas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ end
module QuantumInformation
using ..AbstractQAtlas
import ..AbstractQAtlas: fetch # unexported, so bare `fetch` here would be Base's
import ..AbstractQAtlas: _solve # extended for a closed-form inverse (EntanglementSpectrumCorrelation:ζ)
using ExperimentalAPI: @experimental
include("relations/entanglement.jl")
end
Expand Down
29 changes: 22 additions & 7 deletions src/core/conventions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -203,16 +203,31 @@ export conventions
"""
declared_convention(cs::ConventionSet, Q::Type) -> Union{Convention,Nothing}

What `cs` says `Q`'s values are written in, walking up to `Q`'s supertypes and
taking the most specific entry; `nothing` when nothing in `cs` covers `Q`.
What `cs` says `Q`'s values are written in, taking the most specific entry that
`Q` is a subtype of; `nothing` when nothing in `cs` covers `Q`.

Matched by `<:`, not by walking `supertype`, because a parametric quantity's
supertype chain SKIPS its own family: `supertype(Energy{:per_site})` is
`AbstractThermalPotential`, so a walk never reaches the `Energy` a project keyed
its declaration on, and the value goes into the bag unconverted.

Covers with no unique most specific member are refused rather than resolved by
`Dict` order. Decided after collecting every cover, not folded pairwise: two
unrelated covers can be reconciled by a third that refines both, and a fold that
errors on meeting the first incomparable pair reports a false ambiguity for four
of the six orders that Dict iteration can hand it.
"""
function declared_convention(cs::ConventionSet, @nospecialize(Q::Type))
T = Q
while T !== Any
haskey(cs.declared, T) && return cs.declared[T]
T = supertype(T)
covers = [(T, c) for (T, c) in cs.declared if Q <: T]
isempty(covers) && return nothing
for (T, c) in covers
all(U -> T <: U, first.(covers)) && return c
end
return nothing
return error(
"declared_convention: $Q is covered by $(join(first.(covers), ", ")), with no " *
"one of them a subtype of all the others, so none is the most specific. Key " *
"the declaration on whichever one the values were actually written in.",
)
end
export declared_convention

Expand Down
Loading
Loading