diff --git a/Project.toml b/Project.toml index 89c3895..8fbe4fa 100644 --- a/Project.toml +++ b/Project.toml @@ -3,9 +3,5 @@ uuid = "db38d531-b084-4210-bfec-cee5c3c1a709" version = "0.0.4" authors = ["Shuhei Ohno", "Martin Mikkelsen"] -[deps] -OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" - [compat] -OrderedCollections = "1.8.1" julia = "1.11" diff --git a/README.md b/README.md index aa82ca1..de41dab 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,16 @@ See https://juliafewbody.github.io/FewBodyDB.jl. +## Usage + +```julia +using FewBodyDB + +entry = db(:Bubin2005Jan, Symbol("HD⁺"), :energy, (J = 0, v = 0)) +entry.value # -0.5978979685 +bib(entry) # BibTeX source +``` + ## Citation See [`CITATION.bib`](CITATION.bib) for the relevant reference(s). diff --git a/docs/src/index.md b/docs/src/index.md index 4b97ff5..752bbcc 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -4,46 +4,66 @@ CurrentModule = FewBodyDB # FewBodyDB.jl -This package is a database of calculation results for few-body systems. It was developed for testing solvers in [JuliaFewBody](https://github.com/JuliaFewBody) projects. +FewBodyDB is a database of reference results for few-body systems. It is +intended for validating numerical solvers in +[JuliaFewBody](https://github.com/JuliaFewBody) projects. ## Usage -The `@get` macro returns the bibliographic value corresponding to the given key. +[`db`](@ref) returns a typed [`DatabaseEntry`](@ref), including the result's +provenance and state metadata. ```@repl using FewBodyDB -@get Bubin2005Jan, HD⁺, energy, (J=0, v=0) -println(@bib Bubin2005Jan) +entry = db(:Bubin2005Jan, Symbol("HD⁺"), :energy, (J = 0, v = 0)) +entry.value +entry.state +println(bib(entry)) ``` +A slash-separated key returned by [`dbkeys`](@ref) can also be used directly: + +```@repl +using FewBodyDB +db("Bubin2005Jan/HD⁺/energy/(J = 0, v = 0)").value +``` + +The old `@get` and `@bib` macros remain available as compatibility wrappers, +but function calls are preferred for new code. + ## Data ```@eval using FewBodyDB using Markdown -Markdown.parse(string("| command | value |\n| :------ | :---- |\n", [string("| `@get ", replace(k, "/" => ", "), "` | `\"", @get(k), "\"` |\n") for k in FewBodyDB.@keys]...)) +rows = map(dbkeys()) do key + entry = db(key) + "| `$(key)` | `$(entry.value)` | `$(entry.reference)` |\n" +end +Markdown.parse( + "| key | value | reference |\n" * + "| :-- | --: | :-- |\n" * + join(rows), +) ``` ## Bibliography -The database references are here. - ```@example using FewBodyDB # hide -for k in keys(FewBodyDB.REFS) # hide - println(@bib k) # hide +references = sort!(unique([db(key).reference for key in dbkeys()]); by = string) # hide +for key in references # hide + println(bib(key)) # hide end # hide ``` ## Citation -Please use [CITATION.bib](https://github.com/JuliaFewBody/FewBodyDB.jl/blob/main/CITATION.bib) if you need to cite this package. +Please use [CITATION.bib](https://github.com/JuliaFewBody/FewBodyDB.jl/blob/main/CITATION.bib) +if you need to cite this package. ```@example -file = open("../../CITATION.bib", "r") # hide -text = Base.read(file, String) # hide -close(file) # hide -println(text) # hide +println(read("../../CITATION.bib", String)) ``` ## API reference diff --git a/src/FewBodyDB.jl b/src/FewBodyDB.jl index 45793ca..636c7ad 100644 --- a/src/FewBodyDB.jl +++ b/src/FewBodyDB.jl @@ -1,106 +1,220 @@ module FewBodyDB -# import / export -import OrderedCollections: OrderedDict -export @get, @bib +export DatabaseEntry, db, dbkeys, bib, @get, @bib -# set dictionary -REFS = OrderedDict{String, String}() -DATA = OrderedDict{String, String}() +import Base: put! -# put new data (for development use only) -macro put(expr) - return DATA[join(string.(expr.args[begin:(end - 1)]), "/")] = string(expr.args[end]) -end - -# get data """ -Get data from `FewBodyDB.DATA`. -```repl -julia> @get Bubin2005Jan, HD⁺, energy, (J=0, v=0) -"-0.5978979685" -``` + DatabaseEntry(reference, system, observable, state, value) + +A benchmark result stored in the database. `reference` identifies its +bibliographic source, while `system`, `observable`, and `state` describe the +calculation. `value` retains the concrete numeric type used by the source. """ -macro get(expr) - if expr isa String - # @get "Bubin2005Jan/HD⁺/energy/(J = 0, v = 0)" - return :(FewBodyDB.get($expr)) - elseif expr isa Expr && expr.head == :string - # v = 0; @get "Bubin2005Jan/HD⁺/energy/(J = 0, v = $v)" - return :(FewBodyDB.get(string($(esc(expr))))) - elseif expr isa Symbol - # k = "Bubin2005Jan/HD⁺/energy/(J = 0, v = 0)"; @get k - return :(FewBodyDB.get(string($(esc(expr))))) - elseif expr isa Expr && expr.head == :tuple - # @get Bubin2005Jan, HD⁺, energy, (J=0, v=0) - return FewBodyDB.get(join(string.(expr.args), "/")) - else - # others - return :(FewBodyDB.get(string($(esc(expr))))) - end +struct DatabaseEntry{S,T<:Real} + reference::Symbol + system::Symbol + observable::Symbol + state::S + value::T end -function get(key) - return DATA[key] +# These registries are deliberately private. All lookup and registration goes +# through the functions below so validation and storage can evolve independently +# of the public API. +const _DATABASE = Dict{String,DatabaseEntry}() +const _REFERENCES = Dict{Symbol,String}() + +_symbol(value::Symbol) = value +_symbol(value::AbstractString) = Symbol(value) + +function _dbkey(reference, system, observable, state) + return join( + string.((_symbol(reference), _symbol(system), _symbol(observable), state)), + "/", + ) end -# put new bibliography (for development use only) -macro ref(expr) - return REFS[string(expr.args[begin])] = string(expr.args[end]) +function _available(values) + isempty(values) && return "none" + return join(repr.(values), ", ") +end + +# Register bundled bibliography while loading the package. +function _register_reference!(key::Union{Symbol,AbstractString}, bibtex::AbstractString) + reference = _symbol(key) + haskey(_REFERENCES, reference) && + throw(ArgumentError("reference $(repr(reference)) is already registered")) + + _REFERENCES[reference] = String(bibtex) + return _REFERENCES[reference] end -# get BibTeX entry """ -Get BibTeX entry from `FewBodyDB.DATA`. + put!(reference, system, observable, state, value) -> DatabaseEntry + +Add a benchmark result to the database. The first three arguments may be +symbols or strings, `reference` must already exist in the bundled bibliography, +and `value` must be real. Registering the same result twice throws an +`ArgumentError`. +""" +function put!( + reference::Union{Symbol,AbstractString}, + system::Union{Symbol,AbstractString}, + observable::Union{Symbol,AbstractString}, + state, + value::T, +) where {T<:Real} + reference_symbol = _symbol(reference) + haskey(_REFERENCES, reference_symbol) || throw( + ArgumentError( + "unknown reference $(repr(reference_symbol)); available references: " * + _available(_referencekeys()), + ), + ) + + key = _dbkey(reference_symbol, system, observable, state) + haskey(_DATABASE, key) && + throw(ArgumentError("database key $(repr(key)) is already registered")) + + entry = DatabaseEntry( + reference_symbol, + _symbol(system), + _symbol(observable), + deepcopy(state), + value, + ) + _DATABASE[key] = entry + return deepcopy(entry) +end + +""" + db(key::Union{Symbol,AbstractString}) -> DatabaseEntry + db(reference, system, observable, state) -> DatabaseEntry + +Return a benchmark result. A result can be addressed by its slash-separated +key or by its four structured key components. The returned entry is independent +of the stored value and can safely be modified if its `state` is mutable. + +# Examples + ```julia -julia> println(@bib Bubin2005Jan) -@article{Bubin2005Jan, - title = {Charge asymmetry in HD+}, - volume = {122}, - ISSN = {1089-7690}, - url = {http://dx.doi.org/10.1063/1.1850905}, - DOI = {10.1063/1.1850905}, - number = {4}, - journal = {The Journal of Chemical Physics}, - publisher = {AIP Publishing}, - author = {Bubin, Sergiy and Bednarz, Eugeniusz and Adamowicz, Ludwik}, - year = {2005}, - month = jan -} +entry = db(:Bubin2005Jan, Symbol("HD⁺"), :energy, (J = 0, v = 0)) +entry.value == -0.5978979685 + +db("Bubin2005Jan/HD⁺/energy/(J = 0, v = 0)").value ``` """ -macro bib(expr) - if expr isa String - # @bib "Bubin2005Jan" - return :(FewBodyDB.bib($expr)) - elseif expr isa Expr && expr.head == :string - # s = Bubin2005Jan; @bib "$s" - return :(FewBodyDB.bib(string($(esc(expr))))) - elseif expr isa Symbol - try - # @bib Bubin2005Jan - return FewBodyDB.bib(string(expr)) - catch - # k = "Bubin2005Jan"; @bib k - return :(FewBodyDB.bib(string($(esc(expr))))) - end - else - # others - return :(FewBodyDB.bib(string($(esc(expr))))) +function db(key::AbstractString) + haskey(_DATABASE, key) || throw( + ArgumentError( + "unknown database key $(repr(key)); available keys: " * + _available(dbkeys()), + ), + ) + + return deepcopy(_DATABASE[key]) +end + +db(key::Symbol) = db(string(key)) + +function db( + reference::Union{Symbol,AbstractString}, + system::Union{Symbol,AbstractString}, + observable::Union{Symbol,AbstractString}, + state, +) + return db(_dbkey(reference, system, observable, state)) +end + +""" + dbkeys() -> Vector{String} + +Return all database keys in deterministic order. +""" +dbkeys() = sort!(collect(keys(_DATABASE))) + +""" + bib(key::Union{Symbol,AbstractString}) -> String + bib(entry::DatabaseEntry) -> String + +Return the BibTeX source associated with a reference key or database entry. +""" +function bib(key::Union{Symbol,AbstractString}) + reference = _symbol(key) + haskey(_REFERENCES, reference) || throw( + ArgumentError( + "unknown reference $(repr(reference)); available references: " * + _available(_referencekeys()), + ), + ) + + return _REFERENCES[reference] +end + +bib(entry::DatabaseEntry) = bib(entry.reference) + +_referencekeys() = sort!(collect(keys(_REFERENCES)); by = string) + +# Compatibility wrappers for the original macro API. New code should prefer +# `db(...).value`, `bib(...)`, and `dbkeys()`. +macro get(expr) + if expr isa Expr && expr.head === :tuple && length(expr.args) == 4 + reference, system, observable, state = expr.args + quote_component = value -> value isa Symbol ? QuoteNode(value) : esc(value) + return :( + FewBodyDB.db( + $(quote_component(reference)), + $(quote_component(system)), + $(quote_component(observable)), + $(esc(state)), + ).value + ) end + + return :(FewBodyDB.db($(esc(expr))).value) end -function bib(key) - return REFS[key] +macro bib(expr) + key = expr isa Symbol ? QuoteNode(expr) : esc(expr) + return :(FewBodyDB.bib($key)) end -# list keys macro keys() - return collect(Base.keys(DATA)) + return :(FewBodyDB.dbkeys()) +end + +# Declarative registration helpers used only by the bundled source files. +macro ref(expr) + expr isa Expr && expr.head === :tuple && length(expr.args) == 2 || + throw(ArgumentError("@ref expects a reference key and a BibTeX string")) + reference, bibtex = expr.args + key = reference isa Symbol ? QuoteNode(reference) : esc(reference) + return :(FewBodyDB._register_reference!($key, $(esc(bibtex)))) +end + +macro put(expr) + expr isa Expr && expr.head === :tuple && length(expr.args) == 5 || throw( + ArgumentError( + "@put expects a reference, system, observable, state, and numeric value", + ), + ) + reference, system, observable, state, value = expr.args + quote_component = + component -> component isa Symbol ? QuoteNode(component) : esc(component) + return :( + FewBodyDB.put!( + $(quote_component(reference)), + $(quote_component(system)), + $(quote_component(observable)), + $(esc(state)), + $(esc(value)), + ) + ) end -# update bibliography & database include("refs/Bubin2005Jan.jl") include("refs/Karr2006Apr.jl") include("refs/Suzuki2003Jul.jl") + end diff --git a/test/runtests.jl b/test/runtests.jl index a85e065..9897a1e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,66 +2,63 @@ using FewBodyDB using Test @testset "FewBodyDB.jl" begin + @testset "lookup" begin + entry = db(:Bubin2005Jan, Symbol("HD⁺"), :energy, (J = 0, v = 0)) - # @put - FewBodyDB.@put Ohno2025Sep, H, energy, (n = 1, l = 0, m = 0), -0.5 - @test "-0.5" == (@get Ohno2025Sep, H, energy, (n = 1, l = 0, m = 0)) + @test entry isa DatabaseEntry + @test entry.reference === :Bubin2005Jan + @test entry.system === Symbol("HD⁺") + @test entry.observable === :energy + @test entry.state == (J = 0, v = 0) + @test entry.value === -0.5978979685 - # @bib - @test "@article{Bubin2005Jan" == (@bib Bubin2005Jan)[1:21] - @test "@article{Bubin2005Jan" == (@bib "Bubin2005Jan")[1:21] - # @test "@article{Bubin2005Jan" == (k = "Bubin2005Jan"; @bib k)[1:21] - @test "@article{Bubin2005Jan" == (k = "Bubin2005Jan"; @bib "$k")[1:21] - @test "@article{Bubin2005Jan" == FewBodyDB.bib("Bubin2005Jan")[1:21] + @test db("Bubin2005Jan/HD⁺/energy/(J = 0, v = 0)").value === entry.value + @test db(:Karr2006Apr, Symbol("HD⁺"), :energy, (J = 0, v = 0)).value === + -0.59789796860903 + @test db(:Suzuki2003Jul, Symbol("Ps⁻"), :energy, "¹Sᵉ").value === + -0.2620050702328 + @test db(:Suzuki2003Jul, Symbol("ttμ"), :energy, "¹Fᵒ").value === -101.43 - # @get - @test "-0.5978979685" == @get Bubin2005Jan, HD⁺, energy, (J = 0, v = 0) - @test "-0.5978979685" == @get "Bubin2005Jan/HD⁺/energy/(J = 0, v = 0)" - @test "-0.5978979685" == (k = "Bubin2005Jan/HD⁺/energy/(J = 0, v = 0)"; @get k) - @test "-0.5978979685" == (v = 0; @get "Bubin2005Jan/HD⁺/energy/(J = 0, v = $v)") + keys = dbkeys() + @test keys == sort(keys) + @test "Suzuki2003Jul/∞Li/energy/¹Sᵉ" in keys + @test_throws ArgumentError db(:unknown) + end - @test "-0.5891818291" == @get Bubin2005Jan, HD⁺, energy, (J = 0, v = 1) - @test "-0.5891818291" == @get "Bubin2005Jan/HD⁺/energy/(J = 0, v = 1)" - @test "-0.5891818291" == (k = "Bubin2005Jan/HD⁺/energy/(J = 0, v = 1)"; @get k) - @test "-0.5891818291" == (v = 1; @get "Bubin2005Jan/HD⁺/energy/(J = 0, v = $v)") + @testset "bibliography" begin + @test startswith(bib(:Bubin2005Jan), "@article{Bubin2005Jan") + @test bib(db(:Bubin2005Jan, Symbol("HD⁺"), :energy, (J = 0, v = 0))) == + bib("Bubin2005Jan") + @test_throws ArgumentError bib(:unknown) + end - @test "-0.59789796860903" == @get Karr2006Apr, HD⁺, energy, (J = 0, v = 0) - @test "-0.59789796860903" == @get "Karr2006Apr/HD⁺/energy/(J = 0, v = 0)" - @test "-0.59789796860903" == (k = "Karr2006Apr/HD⁺/energy/(J = 0, v = 0)"; @get k) - @test "-0.59789796860903" == (v = 0; @get "Karr2006Apr/HD⁺/energy/(J = 0, v = $v)") + @testset "registration" begin + state = [1, 2] + registered = put!(:Bubin2005Jan, :H, :test_observable, state, 1 // 2) + key = "Bubin2005Jan/H/test_observable/[1, 2]" - # Suzuki2003Jul entries (updated) - @test "-0.2620050702328" == @get Suzuki2003Jul, Ps⁻, energy, "¹Sᵉ" - @test "-0.2620050702328" == @get "Suzuki2003Jul/Ps⁻/energy/¹Sᵉ" - @test "-0.2620050702328" == (k = "Suzuki2003Jul/Ps⁻/energy/¹Sᵉ"; @get k) + @test registered isa DatabaseEntry{Vector{Int},Rational{Int}} + @test db(key).value === 1 // 2 + @test_throws ArgumentError put!( + :Bubin2005Jan, + :H, + :test_observable, + [1, 2], + 1 // 2, + ) + @test_throws ArgumentError put!(:UnknownReference, :H, :energy, :ground, -0.5) - @test "-0.527751016523" == @get Suzuki2003Jul, ∞H⁻, energy, "¹Sᵉ" - @test "-0.527751016523" == @get "Suzuki2003Jul/∞H⁻/energy/¹Sᵉ" - - @test "-0.1252865" == @get Suzuki2003Jul, H⁻, energy, "³Pᵉ" - @test "-0.1252865" == @get "Suzuki2003Jul/H⁻/energy/³Pᵉ" - - @test "-112.9730179" == @get Suzuki2003Jul, ttμ, energy, "¹Sᵉ" - @test "-110.2621165" == @get Suzuki2003Jul, ttμ, energy, "¹Pᵒ" - @test "-105.98293" == @get Suzuki2003Jul, ttμ, energy, "¹Dᵉ" - @test "-101.43" == @get Suzuki2003Jul, ttμ, energy, "¹Fᵒ" - - @test "-111.364511474" == @get Suzuki2003Jul, tdμ, energy, "¹Sᵉ" - @test "-108.179385" == @get Suzuki2003Jul, tdμ, energy, "¹Pᵒ" - @test "-103.408481" == @get Suzuki2003Jul, tdμ, energy, "¹Dᵉ" - - @test "-2.903724376984" == @get Suzuki2003Jul, ∞He, energy, "¹Sᵉ" - @test "-2.903724372437" == @get Suzuki2003Jul, He, energy, "¹Sᵉ" - @test "-2.175293782367" == @get Suzuki2003Jul, He, energy, "³Sᵉ" - @test "-2.123843086498" == @get Suzuki2003Jul, He, energy, "³Pᵒ" - @test "-2.055620732852" == @get Suzuki2003Jul, He, energy, "¹Dᵉ" - @test "-2.055338993337" == @get Suzuki2003Jul, ∞He, energy, "³Dᵉ" - @test "-2.031255144382" == @get Suzuki2003Jul, He, energy, "¹Fᵒ" - @test "-2.031255168403" == @get Suzuki2003Jul, He, energy, "³Fᵒ" - @test "-2.020000710898" == @get Suzuki2003Jul, He, energy, "¹Gᵉ" - @test "-2.020000710925" == @get Suzuki2003Jul, He, energy, "³Gᵉ" - - @test "-3.50763486" == @get Suzuki2003Jul, p̄He⁺, energy, "L=31" - @test "-7.279913" == @get Suzuki2003Jul, ∞Li, energy, "¹Sᵉ" + # Registration and lookup both isolate mutable state from the registry. + state[1] = 9 + @test db(key).state == [1, 2] + result = db(key) + result.state[1] = 8 + @test db(key).state == [1, 2] + end + @testset "legacy macro wrappers" begin + @test (@get Bubin2005Jan, HD⁺, energy, (J = 0, v = 1)) === -0.5891818291 + @test (@get "Suzuki2003Jul/∞H⁻/energy/¹Sᵉ") === -0.527751016523 + @test startswith(@bib(Bubin2005Jan), "@article{Bubin2005Jan") + end end