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,7 +1,7 @@
name = "AtomsBase"
uuid = "a963bdd2-2df7-4f54-a1ee-49d51e6be12a"
authors = ["JuliaMolSim community"]
version = "0.5.2"
version = "0.5.3"

[deps]
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Expand Down
4 changes: 4 additions & 0 deletions docs/src/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ The function [`AtomsBase.species(sys, i)`](@ref) return the particle species of

- [`ChemicalSpecies`](@ref) : a prototype implementation and recommended default for the species of an atom.

!!! note "`==` versus `isequal` for `ChemicalSpecies`"
`ChemicalSpecies` distinguishes two notions of comparison. `==` is a *matching* relation: an unspecified isotope or atom name acts as a wildcard, so e.g. `ChemicalSpecies(:C) == ChemicalSpecies(:C13)` is `true`. This is handy for queries (e.g. "is this atom carbon?"), but it is not a strict equality.
In contrast `isequal` (with the corresponding `hash`) is *strict* equality of all fields, and `isless` provides a strict total order. These are the well-behaved counterparts used by `sort`, `Set`, `Dict` and `unique`, so `isequal(ChemicalSpecies(:C), ChemicalSpecies(:C13))` is `false`.

## Convenience functions

AtomsBase provides a number of convenience utilities that should work for any system that implements the AtomsBase interface. If they not work as expected this is likely a bug and should be reported as an issue.
Expand Down
37 changes: 35 additions & 2 deletions src/utils/chemspecies.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import PeriodicTable
using Unitful

import Base: ==, convert, show, length
import Base: ==, convert, show, length, isless, isequal, hash

export ChemicalSpecies

Expand Down Expand Up @@ -74,6 +74,23 @@ ChemicalSpecies(:C12; atom_name=:MyC) == ChemicalSpecies(:C)
ChemicalSpecies(:C; atom_name=:MyC) == ChemicalSpecies(:C12; atom_name=:MyC)
```

`==` versus `isequal`

`==` is a *matching* relation: an unspecified isotope (`n_neutrons < 0`) or
unspecified name (`name == 0`) acts as a wildcard that matches anything, as in
the examples above. This is convenient for queries (e.g. "is this atom carbon?")
but not a strict equality.

In contrast `isequal` is *strict* equality of the raw
fields `(atomic_number, n_neutrons, name)`, and `isless` is a strict total order
on those same fields. These are what `sort`, `Set`, `Dict` and `unique` rely on,
so they are provided as the well-behaved counterparts to `==`.
```julia
ChemicalSpecies(:C) == ChemicalSpecies(:C13) # true (wildcard match)
isequal(ChemicalSpecies(:C), ChemicalSpecies(:C13)) # false (strict identity)
sort(ChemicalSpecies.([:O, :H, :C])) # -> [H, C, O]
```

"""
struct ChemicalSpecies
atomic_number::Int16 # = Z = number of protons
Expand Down Expand Up @@ -173,7 +190,23 @@ function ==(cs1::ChemicalSpecies, cs2::ChemicalSpecies)
end
end

# -------- fast access to the periodic table
# `isless`/`isequal`/`hash` impose a strict total order on the RAW fields
# (atomic_number, n_neutrons, name), which is what enables sorting and correct
# use in `Set`/`Dict`/`unique`. This is deliberately different from `==`, which
# is a wildcard *matching* relation (e.g. `:C == :C13` but `!isequal(:C, :C13)`).
# Consequently, an unspecified isotope (`n_neutrons == -1`) sorts before any
# specified isotope of the same element, and an unspecified name (`name == 0`)
# sorts first. The `name` tie-break is deterministic but not alphabetical, since
# `name` is a byte-packed `UInt32`.
_key(cs::ChemicalSpecies) = (cs.atomic_number, cs.n_neutrons, cs.name)

isless(a::ChemicalSpecies, b::ChemicalSpecies) = isless(_key(a), _key(b))

isequal(a::ChemicalSpecies, b::ChemicalSpecies) = isequal(_key(a), _key(b))

hash(cs::ChemicalSpecies, h::UInt) = hash(_key(cs), hash(:ChemicalSpecies, h))

# -------- fast access to the periodic table

const _sym2z = Dict{Symbol, UInt8}(
Symbol(el.symbol) => el.number for el in PeriodicTable.elements
Expand Down
30 changes: 30 additions & 0 deletions test/species.jl
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,36 @@ tmp = ChemicalSpecies(:C12; atom_name=:MyC)
@test element_symbol(ChemicalSpecies(:C)) == :C
@test element_symbol(ChemicalSpecies(:C13)) == :C

@testset "ordering / identity" begin
# sort by atomic number
@test sort( ChemicalSpecies.([:O, :H, :C, :N]) ) == ChemicalSpecies.([:H, :C, :N, :O])
@test issorted( ChemicalSpecies.([:H, :C, :N, :O]) )
@test !issorted( ChemicalSpecies.([:O, :H]) )

# pairwise isless across elements
@test isless( ChemicalSpecies(:H), ChemicalSpecies(:C) )
@test !isless( ChemicalSpecies(:C), ChemicalSpecies(:H) )

# isotope ordering: unspecified isotope sorts first, then by neutron count
@test isless( ChemicalSpecies(:C), ChemicalSpecies(:C12) )
@test isless( ChemicalSpecies(:C12), ChemicalSpecies(:C13) )
@test sort( ChemicalSpecies.([:C13, :C, :C12]) ) == ChemicalSpecies.([:C, :C12, :C13])

# isequal / hash are strict, unlike the wildcard ==
@test ChemicalSpecies(:C) == ChemicalSpecies(:C13) # wildcard match
@test !isequal( ChemicalSpecies(:C), ChemicalSpecies(:C13) )
@test !isequal( ChemicalSpecies(:C; atom_name=:MyC), ChemicalSpecies(:C) )
@test isequal( ChemicalSpecies(:C12), ChemicalSpecies(:C12) )
@test hash( ChemicalSpecies(:C12) ) == hash( ChemicalSpecies(:C12) )

# Set / Dict / unique behave correctly (rely on isequal + hash)
@test length( Set( ChemicalSpecies.([:C, :C, :C13, :C13]) ) ) == 2
@test length( unique( ChemicalSpecies.([:C, :C, :O]) ) ) == 2
d = Dict( ChemicalSpecies(:C12) => 1 )
@test d[ ChemicalSpecies(:C12) ] == 1
@test !haskey( d, ChemicalSpecies(:C13) )
end

@testset "ChemicalSpecies in Atom and FastSystem" begin
box = ([1, 0, 0]u"m", [0, 1, 0]u"m", [0, 0, 1]u"m")
pbcs = (true, true, false)
Expand Down
Loading