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.14"
version = "0.7.15"
authors = ["sota shimozono <shimozono-sota631@g.ecc.u-tokyo.ac.jp>"]

[deps]
Expand Down
1 change: 1 addition & 0 deletions src/AbstractQAtlas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ include("core/distributions.jl")
include("core/fields.jl")
include("core/relation_variables.jl") # the RelationVariable layer (type-keyed variables)
include("core/region.jl") # the Region set layer (entanglement support, §5)
include("core/conventions.jl") # how a value is written, and the conversion into the one the relations use

# structure — model-independent definitional correspondences between the
# core quantities: transition classification, the critical
Expand Down
235 changes: 235 additions & 0 deletions src/core/conventions.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
# core/conventions.jl: the convention layer.
#
# A `VariableKey` pins WHICH quantity and WHERE it is evaluated. It does not pin
# how the number is written, so one type at one support is still two different
# values when one calculation counts bits and the other nats. Today that is
# prose: relations/entanglement.jl's header says "Entropies here are in NATS"
# and nothing reads it.
#
# Three parts, kept separate because they are declared by different people:
# the owner of a quantity says what the relations are written in
# (`canonical_convention`), a project says what ITS numbers are written in
# (`conventions`), and `bag` converts between them on entry so an undeclared
# value can never reach a relation.
#
# Conversion is opt-in per convention pair and refuses otherwise, because most
# pairs are not a rescaling: Pauli and spin-1/2 operators move each term of a
# Hamiltonian by a different factor, and a disorder average is not a rescaled
# typical value.

"""
Convention

Parent for the ways one quantity's value can be written: the base of the
logarithm in an entropy, the normalisation of the operators an energy is built
from, the statistic a disorder average reports.

Open by design. A new axis is a new subtype plus a [`convert_convention`](@ref)
method and needs no change to this file, which is why the package ships the
protocol rather than a list of axes. A quantity with two independent axes gets
one subtype carrying both, since [`canonical_convention`](@ref) answers with a
single value.
"""
abstract type Convention end
export Convention

"""
canonical_convention(Q::Type) -> Union{Convention,Nothing}

The convention this package's relations are written in for `Q`, or `nothing`
when `Q` has no convention axis at all.

`nothing` is the default and is deliberately not filled in by supertype: the
Tsallis entropy is `(1 - Tr ρ^q)/(q - 1)`, which carries no logarithm, so
declaring a base for every [`AbstractEntanglementMeasure`](@ref) would give it
an axis it does not have. Same opt-in reasoning as
[`obeys_entropy_inequalities`](@ref).
"""
canonical_convention(::Type{<:RelationVariable}) = nothing
export canonical_convention

"""
convert_convention(to::Convention, from::Convention, Q::Type, v)

`v`, written in `from`, expressed in `to`.

Equal conventions return `v` untouched, so an exact-arithmetic value stays
exact. Anything else refuses unless a method says the pair converts: two
conventions that are not a rescaling have no conversion, and passing `v`
through there would hand a relation a number from the wrong statistic.
"""
function convert_convention(to::Convention, from::Convention, @nospecialize(Q::Type), v)
to == from && return v
return error(
"convert_convention: no conversion from $from to $to for $Q. Define a " *
"`convert_convention` method if the two are a rescaling of each other; " *
"if they are not, the values have to be recomputed rather than converted.",
)
end
export convert_convention

# ─── The log-base axis ───────────────────────────────────────────────────

"""
LogBase(base::Real) <: Convention

The base of the logarithm an entropy is measured with: [`Nats`](@ref) is
`LogBase(ℯ)` and [`Bits`](@ref) is `LogBase(2)`.

A coefficient multiplying a logarithm (`c`, `c̃`) is unchanged by the base, since
it rescales entropy and logarithm alike. An additive constant (`c₁`, `ln g`) and
a bare difference of entropies are not, and that is where a transcribed formula
silently gains or loses a factor of `ln 2`.
"""
struct LogBase <: Convention
base::Float64
function LogBase(base::Real)
base > 0 && base != 1 ||
throw(ArgumentError("LogBase: base must be positive and not 1; got $base"))
return new(Float64(base))
end
end
export LogBase

"""
Nats

`LogBase(ℯ)`, the convention every entropy relation in this package is written in.
"""
const Nats = LogBase(ℯ)
export Nats

"""
Bits

`LogBase(2)`, `S = -Tr ρ log₂ ρ`, which is what the entanglement literature
usually counts.
"""
const Bits = LogBase(2)
export Bits

function convert_convention(to::LogBase, from::LogBase, @nospecialize(Q::Type), v)
to == from && return v
return v * (log(from.base) / log(to.base))
end

# The twelve entanglement measures whose ABQ definition contains a logarithm, or
# is an additive combination of ones that do. The four that are absent are absent
# because their defining relation has no logarithm to take a base of:
# `TsallisEntropy` is `(1 - Tr ρ^q)/(q - 1)`, `Concurrence` is a wavefunction
# amplitude, and `Tangle`/`ThreeTangle` are built from it by squaring.
for Q in (
:VonNeumannEntropy,
:FermionicEntanglementEntropy,
:RenyiEntropy,
:MutualInformation,
:ConditionalEntropy,
:RelativeEntropy,
:MeasurementEntropy,
:MarkovEntropy,
:TripartiteInformation,
:TopologicalEntanglementEntropy,
:LogarithmicNegativity,
:PageEntropy,
)
@eval canonical_convention(::Type{$Q}) = Nats
end

# ─── What a project declares ─────────────────────────────────────────────

"""
ConventionSet

What one project's numbers are written in, as a map from quantity type to
[`Convention`](@ref). Build one with [`conventions`](@ref) and hand it to
[`bag`](@ref).

Declared once next to the calculation, not repeated at each call, because the
convention is a property of how the numbers were produced.
"""
struct ConventionSet
declared::Dict{Type,Convention}
end
export ConventionSet

"""
conventions(pairs...) -> ConventionSet

Declare what this project's values are written in:

```julia
conventions(VonNeumannEntropy => Bits) # this one quantity
conventions(AbstractEntanglementMeasure => Bits) # every entropy that has a base
```

A key may be a concrete type, an INSTANCE (reduced to its type), or an abstract
supertype. The supertype form is the usable one when a bag holds several
entropies: it reaches every subtype that declares a
[`canonical_convention`](@ref) and skips the ones that have no such axis, so
naming `AbstractEntanglementMeasure` does not claim a base for the Tsallis
entropy. Naming a concrete type that has no axis is an error, since that is a
claim about that type.

Lookup is most-specific-first, so a concrete entry overrides a supertype entry.
"""
function conventions(pairs::Pair...)
d = Dict{Type,Convention}()
for (k, c) in pairs
T = k isa Type ? k : typeof(k)
T <: RelationVariable ||
error("conventions: $T is not a relation variable, so no relation reads it.")
c isa Convention ||
error("conventions: the value for $T is $(typeof(c)), not a Convention.")
haskey(d, T) && error(
"conventions: duplicate key $T. A quantity is written in one " *
"convention; two entries for it cannot both be what the numbers are.",
)
# A concrete type with no axis is a claim about that type, so it is
# refused here rather than silently ignored at conversion time.
if isconcretetype(T) && canonical_convention(T) === nothing
error(
"conventions: $T declares no convention axis, so $c cannot be " *
"converted away from. If $T does have one, give it a " *
"`canonical_convention` method; if the declaration was meant for " *
"its siblings, key it on the supertype instead.",
)
end
d[T] = c
end
return ConventionSet(d)
end
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`.
"""
function declared_convention(cs::ConventionSet, @nospecialize(Q::Type))
T = Q
while T !== Any
haskey(cs.declared, T) && return cs.declared[T]
T = supertype(T)
end
return nothing
end
export declared_convention

"""
in_canonical_convention(cs::ConventionSet, Q::Type, v)

`v` rewritten in the convention this package's relations use for `Q`.

Returns `v` untouched when `cs` says nothing about `Q`, or when `Q` has no
convention axis, which is what makes the layer opt-in: a project that declares
nothing gets the behaviour it had before.
"""
function in_canonical_convention(cs::ConventionSet, @nospecialize(Q::Type), v)
from = declared_convention(cs, Q)
from === nothing && return v
to = canonical_convention(Q)
to === nothing && return v
return convert_convention(to, from, Q, v)
end
export in_canonical_convention
23 changes: 23 additions & 0 deletions src/relations/interface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,29 @@ function bag(pairs::Pair...)
end
export bag

"""
bag(cs::ConventionSet, pairs...) -> Bag

A bag whose values are converted, on entry, out of the conventions `cs` declares
and into the ones this package's relations are written in.

This is the only door: a relation never sees the project's convention, so there
is no call that silently skips the conversion. A quantity `cs` says nothing
about, or one with no convention axis, is stored unchanged.

```julia
cs = conventions(AbstractEntanglementMeasure => Bits)
bag(cs, VonNeumannEntropy() => 3.0) # stored as 3.0 * log(2), in nats
```
"""
function bag(cs::ConventionSet, pairs::Pair...)
b = bag(pairs...)
for key in collect(keys(b))
b[key] = in_canonical_convention(cs, key.type, b[key])
end
return b
end

# Look up one identity slot in a bag, PRESENCE-aware: `Some(value)` if the key is
# present (even when the stored value is itself `nothing`), else `nothing` — so a
# truly absent key is never confused with a present-but-`nothing` one (issue C2).
Expand Down
113 changes: 113 additions & 0 deletions test/core/test_conventions.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# The convention layer: what a project's numbers are written in, and the
# conversion into what the relations here are written in.
#
# The measurement that justifies the layer is the `region_report` pair below.
# `S = 1.75` on two qubits is legal in bits (the ceiling is 2) and impossible in
# nats (the ceiling is 2 ln 2 = 1.386), so a bits-counting calculation handed to
# this package raw is reported as violating a physical bound it does not violate.

using AbstractQAtlas
using Test: @test, @test_throws, @testset

# A quantity, an axis and a conversion, none of them in `src/`: the claim that
# the layer is a protocol rather than a list of axes is a test, not a docstring.
struct ConventionProbeQuantity <: AbstractQuantity end
struct WholeUnits <: Convention end
struct HalfUnits <: Convention end
AbstractQAtlas.canonical_convention(::Type{ConventionProbeQuantity}) = WholeUnits()
AbstractQAtlas.convert_convention(::WholeUnits, ::HalfUnits, ::Type, v) = 2v

@testset "an axis is declared per quantity, never by supertype" begin
# The twelve whose ABQ definition contains a logarithm, or is an additive
# combination of ones that do.
for Q in (
VonNeumannEntropy,
FermionicEntanglementEntropy,
RenyiEntropy,
MutualInformation,
ConditionalEntropy,
RelativeEntropy,
MeasurementEntropy,
MarkovEntropy,
TripartiteInformation,
TopologicalEntanglementEntropy,
LogarithmicNegativity,
PageEntropy,
)
@test canonical_convention(Q) === Nats
end
# `(1 - Tr ρ^q)/(q - 1)` has no logarithm, and neither has a concurrence or
# a tangle built by squaring one. A supertype declaration would give all four
# a base they do not have.
for Q in (TsallisEntropy, Concurrence, Tangle, ThreeTangle)
@test canonical_convention(Q) === nothing
end
@test canonical_convention(Temperature) === nothing
end

@testset "a declaration is refused when it claims something it cannot mean" begin
@test_throws ErrorException conventions(Float64 => Bits)
@test_throws ErrorException conventions(VonNeumannEntropy => 2)
@test_throws ErrorException conventions(
VonNeumannEntropy => Bits, VonNeumannEntropy => Nats
)
# Naming a concrete type is a claim about that type, so a type with no axis
# is an error; naming its supertype is a sweep, and skips it silently.
@test_throws ErrorException conventions(TsallisEntropy => Bits)
@test conventions(AbstractEntanglementMeasure => Bits) isa ConventionSet
end

@testset "lookup is most specific first" begin
cs = conventions(AbstractEntanglementMeasure => Bits, VonNeumannEntropy => Nats)
@test declared_convention(cs, VonNeumannEntropy) === Nats
@test declared_convention(cs, RenyiEntropy) === Bits
@test declared_convention(cs, Temperature) === nothing
end

@testset "conversion" begin
@test convert_convention(Nats, Bits, VonNeumannEntropy, 1.0) ≈ log(2)
@test convert_convention(Bits, Nats, VonNeumannEntropy, log(2)) ≈ 1.0
# Equal conventions never touch the value, so an exact input stays exact.
v = convert_convention(Nats, Nats, VonNeumannEntropy, 3//2)
@test v === 3//2
# Two axes that are not a rescaling of each other have no conversion, and
# returning the value unchanged there is the failure this refuses.
@test_throws ErrorException convert_convention(
Nats, HalfUnits(), VonNeumannEntropy, 1.0
)
@test_throws ArgumentError LogBase(1)
@test_throws ArgumentError LogBase(0)
end

@testset "bag converts on entry, and only what is declared" begin
cs = conventions(AbstractEntanglementMeasure => Bits)
b = bag(cs, VonNeumannEntropy() => 3.0, TsallisEntropy(2) => 1.0, Temperature => 2.0)
@test b[VariableKey(VonNeumannEntropy)] ≈ 3.0 * log(2)
@test b[VariableKey(TsallisEntropy, OrderSupport(2.0))] == 1.0 # no axis
@test b[VariableKey(Temperature)] == 2.0 # not declared
# An undeclared project is the behaviour this package had before the layer.
@test bag(VonNeumannEntropy() => 3.0)[VariableKey(VonNeumannEntropy)] == 3.0
# The plain constructor's refusals still hold through this door.
@test_throws ErrorException bag(cs, VonNeumannEntropy() => nothing)
@test_throws ErrorException bag(
cs, VonNeumannEntropy() => 1.0, VonNeumannEntropy() => 2.0
)
end

@testset "a new axis needs no change to the package" begin
cs = conventions(ConventionProbeQuantity => HalfUnits())
@test bag(cs, ConventionProbeQuantity() => 2.5)[VariableKey(ConventionProbeQuantity)] ==
5.0
end

@testset "the declaration flips a physical-bound verdict, in both directions" begin
cs = conventions(AbstractEntanglementMeasure => Bits)
verdict(b) = all(r -> r.pass, region_report(b; local_dim=2))
# Two qubits: the ceiling is 2 bits, and 2 ln 2 = 1.386 nats.
@test !verdict(bag(entanglement_entropy(1, 2) => 1.75)) # false alarm
@test verdict(bag(cs, entanglement_entropy(1, 2) => 1.75)) # declared away
# The control: 2.5 bits is over the ceiling in bits too, so declaring the
# convention must not make it pass.
@test !verdict(bag(entanglement_entropy(1, 2) => 2.5))
@test !verdict(bag(cs, entanglement_entropy(1, 2) => 2.5))
end
Loading