Skip to content

Commit

Permalink
Key-based property interface (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
mfherbst authored Jan 19, 2023
1 parent 7b3de90 commit c820c36
Show file tree
Hide file tree
Showing 15 changed files with 297 additions and 114 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "AtomsBase"
uuid = "a963bdd2-2df7-4f54-a1ee-49d51e6be12a"
authors = ["JuliaMolSim community"]
version = "0.2.5"
version = "0.3.0"

[deps]
PeriodicTable = "7b2266bf-644c-5ea3-82d8-af4bbd25a884"
Expand Down
2 changes: 2 additions & 0 deletions docs/src/apireference.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ isinfinite
n_dimensions
periodicity
species_type
atomkeys
hasatomkey
```

## Species / atom properties
Expand Down
5 changes: 4 additions & 1 deletion docs/src/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ additional behavior depending on context.
## System state and properties
The only required properties to be specified of the system is the species
and implementations of standard functions accessing the properties of the species,
currently [`position`](@ref), [`velocity`](@ref), [`atomic_symbol`](@ref), [`atomic_mass`](@ref), [`atomic_number`](@ref), [`n_dimensions`](@ref), [`element`](@ref).
currently
- Geometric information: [`position`](@ref), [`velocity`](@ref), [`n_dimensions`](@ref)
- Atomic information: [`atomic_symbol`](@ref), [`atomic_mass`](@ref), [`atomic_number`](@ref), [`element`](@ref)
- Atomic and system property accessors: `getindex`, `haskey`, `getkey`, `keys`, `pairs`
Based on these methods respective equivalent methods acting
on an `AbstractSystem` will be automatically available, e.g. using the iteration
interface of the `AbstractSystem` (see above). Most of the property accessors on the
Expand Down
89 changes: 77 additions & 12 deletions docs/src/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ using Unitful, UnitfulAtomic, AtomsBase # hide
deuterium = Atom(1, atomic_symbol=:D, [0, 1, 2.]u"bohr")
````
An equivalent dict-like interface based on `keys`, `haskey`, `getkey` and `pairs`
is also available. For example
````@example atom
keys(atom)
````
````@example atom
atom[:atomic_symbol]
````
````@example atom
pairs(atom)
````
This interface seamlessly generalises to working with user-specific atomic properties
as will be discussed next.
### Optional atomic properties
Custom properties can be easily attached to an `Atom` by supplying arbitrary
keyword arguments upon construction. For example to attach a pseudopotential
Expand All @@ -88,11 +102,20 @@ for using the structure with [DFTK](https://dftk.org), construct the atom as
using Unitful, UnitfulAtomic, AtomsBase # hide
atom = Atom(:C, [0, 1, 2.]u"bohr", pseudopotential="hgh/lda/c-q4")
````
which will make the pseudopotential identifier available as `atom.pseudopotential`.
which will make the pseudopotential identifier available as
````@example atomprop
atom[:pseudopotential]
````
Notice that such custom properties are fully integrated with the standard atomic properties,
e.g. automatically available from the `keys`, `haskey` and `pairs` functions, e.g.:
````@example atomprop
@show haskey(atom, :pseudopotential)
pairs(atom)
````
Updating an atomic property proceeds similarly. E.g.
````@example atomprop
using Unitful, UnitfulAtomic, AtomsBase # hide
newatom = Atom(;atom=atom, atomic_mass=13u"u")
newatom = Atom(atom; atomic_mass=13u"u")
````
makes a new carbon atom with all properties identical to `atom` (including custom ones),
but setting the `atomic_mass` to 13 units.
Expand All @@ -112,31 +135,68 @@ Property name | Unit / Type | Description
`:magnetic_moments` | `Union{Float64,Vector{Float64}}` | Initial magnetic moment
`:pseudopotential` | `String` | Pseudopotential or PAW keyword or `""` if Coulomb potential employed
A convenient way to iterate over all data stored in an atom offers the `pairs` function:
````@example atomprop
for (k, v) in pairs(atom)
println("$k = $v")
end
````
## System interface and conventions
Once the atoms are constructed these can be assembled into a system.
For example to place a hydrogen molecule into a cubic box of `10Å` and periodic
boundary conditions, use:
````@example system
using Unitful, UnitfulAtomic, AtomsBase # hide
bounding_box = [[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]]u"Å"
box = [[10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]]u"Å"
boundary_conditions = [Periodic(), Periodic(), Periodic()]
hydrogen = FlexibleSystem([Atom(:H, [0, 0, 1.]u"bohr"),
Atom(:H, [0, 0, 3.]u"bohr")],
bounding_box, boundary_conditions)
box, boundary_conditions)
````
An update constructor for systems is supported as well (see [`AbstractSystem`](@ref)). For example
````@example system
AbstractSystem(hydrogen; bounding_box=[[5.0, 0.0, 0.0], [0.0, 5.0, 0.0], [0.0, 0.0, 5.0]]u"Å")
````
Note that `FlexibleSystem( ... )` would have worked as well in this example (since we are
To update the atomic composition of the system, this function supports an `atoms` (or `particles`)
keyword argument to supply the new set of atoms to be contained in the system.
Note that in this example `FlexibleSystem( ... )` would have worked as well (since we are
updating a `FlexibleSystem`). However, using the `AbstractSystem` constructor to update the system
is more general as it allows for type-specific dispatching when updating other data structures
implementing the `AbstractSystem` interface.
Oftentimes more convenient are the functions
[`atomic_system`](@ref), [`isolated_system`](@ref), [`periodic_system`](@ref),
which cover some standard atomic system setups.
Similar to the atoms, system objects similarly support a functional-style access to system properties
as well as a dict-style access:
````@example system
bounding_box(hydrogen)
````
````@example system
hydrogen[:boundary_conditions]
````
````@example system
pairs(hydrogen)
````
Moreover atomic properties of a specific atom or all atoms can be directly queried using
the indexing notation:
````@example system
hydrogen[1, :position] # Position of first atom
````
````@example system
hydrogen[:, :position] # All atomic symbols
````
Finally, supported keys of atomic properties can be directly queried at the system level
using [`atomkeys`](@ref) and [`hasatomkey`](@ref). Note that these functions only apply to atomic
properties which are supported by *all* atoms of a system. In other words if a custom atomic property is only
set in a few of the contained atoms, these functions will not consider it.
````@example system
atomkeys(hydrogen)
````
For constructing atomic systems the functions
[`atomic_system`](@ref), [`isolated_system`](@ref), [`periodic_system`](@ref)
are oftentimes more convenient as they provide specialisations
for some standard atomic system setups.
For example to setup a hydrogen system with periodic BCs, we can issue
````@example
using Unitful, UnitfulAtomic, AtomsBase # hide
Expand Down Expand Up @@ -165,11 +225,16 @@ hydrogen = isolated_system([:H => [0, 0, 1.]u"bohr",
### Optional system properties
Similar to atoms, systems also support storing arbitrary data, for example
```@example
````@example sysprop
using Unitful, UnitfulAtomic, AtomsBase # hide
hydrogen = isolated_system([:H => [0, 0, 1.]u"bohr", :H => [0, 0, 3.]u"bohr"]; extra_data=42)
```
Again to simplify interoperability some optional properties are reserved, namely:
system = isolated_system([:H => [0, 0, 1.]u"bohr", :H => [0, 0, 3.]u"bohr"]; extra_data=42)
````
Again these custom properties are fully integrated with `keys`, `haskey`, `pairs` and `getkey`.
````@example sysprop
@show keys(system)
````
Some property names are reserved and should be considered by all libraries
supporting `AtomsBase` if possible:
Property name | Unit / Type | Description
:-------------- | :----------------- | :---------------------
Expand Down
2 changes: 1 addition & 1 deletion src/AtomsBase.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ include("interface.jl")
include("properties.jl")
include("show.jl")
include("flexible_system.jl")
include("atom.jl")
include("atomview.jl")
include("atom.jl")
include("fast_system.jl")

end
40 changes: 16 additions & 24 deletions src/atom.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,20 @@ atomic_number(atom::Atom) = atom.atomic_number
element(atom::Atom) = element(atomic_number(atom))
n_dimensions(::Atom{D}) where {D} = D

Base.hasproperty(at::Atom, x::Symbol) = hasfield(Atom, x) || haskey(at.data, x)
Base.getproperty(at::Atom, x::Symbol) = hasfield(Atom, x) ? getfield(at, x) : getindex(at.data, x)
function Base.propertynames(at::Atom, private::Bool=false)
if private
(fieldnames(Atom)..., keys(at.data)...)
else
(filter(!isequal(:data), fieldnames(Atom))..., keys(at.data)...)
end
Base.getindex(at::Atom, x::Symbol) = hasfield(Atom, x) ? getfield(at, x) : getindex(at.data, x)
Base.haskey(at::Atom, x::Symbol) = hasfield(Atom, x) || haskey(at.data, x)
function Base.getkey(at::Atom, x::Symbol, default)
hasfield(Atom, x) ? getfield(at, x) : getkey(at.data, x, default)
end
function Base.keys(at::Atom)
(:position, :velocity, :atomic_symbol, :atomic_number, :atomic_mass, keys(at.data)...)
end
Base.pairs(at::Atom) = (k => at[k] for k in keys(at))

"""
Atom(identifier::AtomId, position::AbstractVector; kwargs...)
Atom(identifier::AtomId, position::AbstractVector, velocity::AbstractVector; kwargs...)
Atom(; atomic_number, position, velocity=zeros(D)u"bohr/s", kwargs...)
Construct an atomic located at the cartesian coordinates `position` with (optionally)
the given cartesian `velocity`. Note that `AtomId = Union{Symbol,AbstractString,Integer}`.
Expand All @@ -53,18 +54,17 @@ function Atom(identifier::AtomId,
atomic_number, atomic_mass, Dict(kwargs...))
end
function Atom(id::AtomId, position::AbstractVector, velocity::Missing; kwargs...)
Atom(id, position; kwargs...)
Atom(id, position, zeros(length(position))u"bohr/s"; kwargs...)
end
function Atom(; atomic_symbol, position, velocity=zeros(length(position))u"bohr/s", kwargs...)
Atom(atomic_symbol, position, velocity; atomic_symbol, kwargs...)
end

"""
Atom(atom::Atom; kwargs...)
Atom(; atom, kwargs...)
Update constructor. Construct a new `Atom`, by amending the data contained
in the passed `atom` object. Note that the first version only works if `atom` is an `Atom`,
while the second version works on arbitrary species adhering to the `AtomsBase` conventions.
In library code the second version should therefore be preferred.
in the passed `atom` object.
Supported `kwargs` include `atomic_symbol`, `atomic_number`, `atomic_mass`, `charge`,
`multiplicity` as well as user-specific custom properties.
Expand All @@ -75,18 +75,10 @@ julia> hydrogen = Atom(:H, zeros(3)u"Å")
```
and now amend its charge and atomic mass
```julia-repl
julia> Atom(; atom, atomic_mass=1.0u"u", charge=-1.0u"e_au")
julia> Atom(atom; atomic_mass=1.0u"u", charge=-1.0u"e_au")
```
"""
function Atom(;atom, kwargs...)
extra = atom isa Atom ? atom.data : (; )
Atom(atomic_number(atom), position(atom), velocity(atom);
atomic_symbol=atomic_symbol(atom),
atomic_number=atomic_number(atom),
atomic_mass=atomic_mass(atom),
extra..., kwargs...)
end
Atom(atom::Atom; kwargs...) = Atom(; atom, kwargs...)
Atom(atom::Atom; kwargs...) = Atom(; pairs(atom)..., kwargs...)

function Base.convert(::Type{Atom}, id_pos::Pair{<:AtomId,<:AbstractVector{<:Unitful.Length}})
Atom(id_pos...)
Expand Down
8 changes: 8 additions & 0 deletions src/atomview.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,11 @@ element(atom::AtomView) = element(atomic_number(atom))

Base.show(io::IO, at::AtomView) = show_atom(io, at)
Base.show(io::IO, mime::MIME"text/plain", at::AtomView) = show_atom(io, mime, at)

Base.getindex(v::AtomView, x::Symbol) = getindex(v.system, v.index, x)
Base.haskey(v::AtomView, x::Symbol) = hasatomkey(v.system, x)
function Base.getkey(v::AtomView, x::Symbol, default)
hasatomkey(v.system, x) ? v[x] : default
end
Base.keys(v::AtomView) = atomkeys(v.system)
Base.pairs(at::AtomView) = (k => at[k] for k in keys(at))
63 changes: 40 additions & 23 deletions src/fast_system.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
export FastSystem

struct FastSystem{D, L <: Unitful.Length, M <: Unitful.Mass} <: AbstractSystem{D}
box::SVector{D, SVector{D, L}}
bounding_box::SVector{D, SVector{D, L}}
boundary_conditions::SVector{D, BoundaryCondition}
positions::Vector{SVector{D, L}}
atomic_symbols::Vector{Symbol}
atomic_numbers::Vector{Int}
atomic_masses::Vector{M}
position::Vector{SVector{D, L}}
atomic_symbol::Vector{Symbol}
atomic_number::Vector{Int}
atomic_mass::Vector{M}
end

# Constructor to fetch the types
Expand Down Expand Up @@ -41,22 +41,39 @@ function FastSystem(particles, box, boundary_conditions)
atomic_number.(particles), atomic_mass.(particles))
end

bounding_box(sys::FastSystem) = sys.box
bounding_box(sys::FastSystem) = sys.bounding_box
boundary_conditions(sys::FastSystem) = sys.boundary_conditions
Base.length(sys::FastSystem) = length(sys.positions)
Base.size(sys::FastSystem) = size(sys.positions)

species_type(sys::FS) where {FS <: FastSystem} = AtomView{FS}
Base.getindex(sys::FastSystem, index::Int) = AtomView(sys, index)

position(s::FastSystem) = s.positions
atomic_symbol(s::FastSystem) = s.atomic_symbols
atomic_number(s::FastSystem) = s.atomic_numbers
atomic_mass(s::FastSystem) = s.atomic_masses
velocity(s::FastSystem) = missing

position(s::FastSystem, i) = s.positions[i]
atomic_symbol(s::FastSystem, i) = s.atomic_symbols[i]
atomic_number(s::FastSystem, i) = s.atomic_numbers[i]
atomic_mass(s::FastSystem, i) = s.atomic_masses[i]
velocity(s::FastSystem, i) = missing
Base.length(sys::FastSystem) = length(sys.position)
Base.size(sys::FastSystem) = size(sys.position)

species_type(::FS) where {FS <: FastSystem} = AtomView{FS}
Base.getindex(sys::FastSystem, i::Integer) = AtomView(sys, i)

position(s::FastSystem) = s.position
atomic_symbol(s::FastSystem) = s.atomic_symbol
atomic_number(s::FastSystem) = s.atomic_number
atomic_mass(s::FastSystem) = s.atomic_mass
velocity(::FastSystem) = missing

position(s::FastSystem, i) = s.position[i]
atomic_symbol(s::FastSystem, i) = s.atomic_symbol[i]
atomic_number(s::FastSystem, i) = s.atomic_number[i]
atomic_mass(s::FastSystem, i) = s.atomic_mass[i]
velocity(::FastSystem, i) = missing

# System property access
function Base.getindex(system::FastSystem, x::Symbol)
if x in (:bounding_box, :boundary_conditions)
getfield(system, x)
else
throw(KeyError("Key $x not found"))
end
end
Base.haskey(::FastSystem, x::Symbol) = x in (:bounding_box, :boundary_conditions)
Base.keys(::FastSystem) = (:bounding_box, :boundary_conditions)

# Atom and atom property access
atomkeys(::FastSystem) = (:position, :atomic_symbol, :atomic_number, :atomic_mass)
hasatomkey(system::FastSystem, x::Symbol) = x in atomkeys(system)
Base.getindex(system::FastSystem, i::Integer, x::Symbol) = getfield(system, x)[i]
Base.getindex(system::FastSystem, ::Colon, x::Symbol) = getfield(system, x)
Loading

2 comments on commit c820c36

@mfherbst
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/76000

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.3.0 -m "<description of version>" c820c36341b609ecfd4318c2a71c35d593c4c5bb
git push origin v0.3.0

Please sign in to comment.