module UnitCells using Random using Printf import ..CrystFEL: libcrystfel export UnitCell, LatticeType, CenteringType, UniqueAxis export TriclinicLattice, MonoclinicLattice, OrthorhombicLattice export TetragonalLattice, HexagonalLattice, RhombohedralLattice, CubicLattice export PrimitiveCell, ACenteredCell, BCenteredCell, CCenteredCell export BodyCenteredCell, FaceCenteredCell, RhombohedralCell, RhombohedralCellOnHexagonalAxes export NoUniqueAxis, UnknownUniqueAxis, UniqueAxisA, UniqueAxisB, UniqueAxisC export rotatecell # Represents the real C-side (opaque) structure. mutable struct InternalUnitCell end # The Julia-side structure, needed to house the pointer to the C structure # Without this, we would only ever have a Ptr{DataTemplate}, not a DataTemplate. mutable struct UnitCell internalptr::Ptr{InternalUnitCell} end """ Enumeration of the seven Bravais lattice types: `TriclinicLattice`, `MonoclinicLattice`, `OrthorhombicLattice`, `TetragonalLattice`, `RhombohedralLattice`, `HexagonalLattice`, `CubicLattice`. """ @enum LatticeType begin TriclinicLattice MonoclinicLattice OrthorhombicLattice TetragonalLattice RhombohedralLattice HexagonalLattice CubicLattice end """ Enumeration of unit cell centering possibilities: `PrimitiveCell`, `ACenteredCell`, `BCenteredCell`, `CCenteredCell`, `BodyCenteredCell` (I-centering), `FaceCenteredCell` (F-centering), and `RhombohedralCell` (R-centering, primitive rhombohedral cell). `RhombohedralCellOnHexagonalAxes` indicates "H-centering" as used by the protein data bank, which is different from the "triple hexagonal cell" described in the International Tables. """ @enum CenteringType begin PrimitiveCell = Int('P') ACenteredCell = Int('A') BCenteredCell = Int('B') CCenteredCell = Int('C') BodyCenteredCell = Int('I') FaceCenteredCell = Int('F') RhombohedralCell = Int('R') RhombohedralCellOnHexagonalAxes = Int('H') end """ Enumeration of unique axis possibilities. The possibilities are `UniqueAxisA`, `UniqueAxisB` and `UniqueAxisC`. Alternatively, use `NoUniqueAxis` if the type of unit cell does not have a unique axis (triclinic, orthorhombic, cubic or rhombohedral). `UnknownUniqueAxis` means that the unique axis is not known. """ @enum UniqueAxis begin NoUniqueAxis = Int('*') UnknownUniqueAxis = Int('?') UniqueAxisA = Int('a') UniqueAxisB = Int('b') UniqueAxisC = Int('c') end """ UnitCell(latticetype, centering, uniqueaxis, a, b, c, α, β, γ) Creates a CrystFEL UnitCell, in an undefined orientation, from the given parameters. The axis lengths (a, b, c) should be in *Ångstroms*, and the angles (α, β, γ) should be in *degrees* - note that this is different to the equivalent function in CrystFEL's C API. See the documentation for `LatticeType`, `CenteringType` and `UniqueAxis` for possible values. You can also use the characters `'a'`, `'b'` and `'c'` for `uniqueaxis`, or `'P'`, `'A'`, `'B'`, `'C'`, `'I'`, `'F'`, `'R'` and `'H`' for `centering. Corresponds to CrystFEL C API function `cell_new_from_parameters` with follow-up calls to `cell_set_centering`, `cell_set_lattice_type` and `cell_set_unique_axis`. """ function UnitCell(latticetype, centering, uniqueaxis, a, b, c, α, β, γ) ninety(a) = isapprox(a, 90, atol=0.1) if latticetype == OrthorhombicLattice if !(ninety(α) && ninety(β) && ninety(γ)) throw(ArgumentError("All angles must be 90°")) end elseif latticetype == TetragonalLattice if !(ninety(α) && ninety(β) && ninety(γ)) throw(ArgumentError("All angles must be 90°")) end elseif latticetype == CubicLattice if !(ninety(α) && ninety(β) && ninety(γ)) throw(ArgumentError("All angles must be 90°")) end elseif latticetype == HexagonalLattice if uniqueaxis == UniqueAxisA if !isapprox(b, c, rtol=0.01) throw(ArgumentError("b and c lengths should be equal")) end elseif uniqueaxis == UniqueAxisB if !isapprox(a, c, rtol=0.01) throw(ArgumentError("a and c lengths should be equal")) end elseif uniqueaxis == UniqueAxisC if !isapprox(a, b, rtol=0.01) throw(ArgumentError("a and b lengths should be equal")) end else throw(ArgumentError("Hexagonal cell requires a unique axis")) end end out = ccall((:cell_new_from_parameters, libcrystfel), Ptr{InternalUnitCell}, (Cdouble,Cdouble,Cdouble,Cdouble,Cdouble,Cdouble), a/1e10, b/1e10, c/1e10, deg2rad(α), deg2rad(β), deg2rad(γ)) if out == C_NULL throw(ArgumentError("Failed to create unit cell")) end ccall((:cell_set_centering, libcrystfel), Cvoid, (Ptr{InternalUnitCell},Cchar), out, centering) ccall((:cell_set_unique_axis, libcrystfel), Cvoid, (Ptr{InternalUnitCell},Cchar), out, uniqueaxis) ccall((:cell_set_lattice_type, libcrystfel), Cvoid, (Ptr{InternalUnitCell},Cint), out, latticetype) cell = UnitCell(out) finalizer(cell) do x ccall((:cell_free, libcrystfel), Cvoid, (Ptr{InternalUnitCell},), x.internalptr) end return cell end """ UnitCell(latticetype, centering, a, b, c, α, β, γ) A convenience constructor which attempts to determine the unique axis automatically from the cell parameters. If the unique axis is not obvious, an `ArgumentError` will be thrown. """ function UnitCell(latticetype, centering, a, b, c, α, β, γ) notninety(a) = !isapprox(a, 90, atol=0.5) ninety(a) = isapprox(a, 90, atol=0.1) onetwenty(a) = isapprox(a, 120, atol=0.1) if latticetype == TriclinicLattice ua = NoUniqueAxis elseif latticetype == OrthorhombicLattice ua = NoUniqueAxis elseif latticetype == RhombohedralLattice ua = NoUniqueAxis elseif latticetype == CubicLattice ua = NoUniqueAxis elseif latticetype == MonoclinicLattice if notninety(α) && ninety(β) && ninety(γ) ua = UniqueAxisA elseif ninety(α) && notninety(β) && ninety(γ) ua = UniqueAxisB elseif ninety(α) && ninety(β) && notninety(γ) ua = UniqueAxisC else throw(ArgumentError("Can't determine unique axis")) end elseif latticetype == TetragonalLattice if isapprox(b, c, rtol=0.01) && !isapprox(a, b, rtol=0.05) ua = UniqueAxisA elseif isapprox(a, c, rtol=0.01) && !isapprox(a, b, rtol=0.05) ua = UniqueAxisB elseif isapprox(a, b, rtol=0.01) && !isapprox(c, b, rtol=0.05) ua = UniqueAxisC else throw(ArgumentError("Can't determine unique axis")) end elseif latticetype == HexagonalLattice if onetwenty(α) && ninety(β) && ninety(γ) ua = UniqueAxisA elseif ninety(α) && onetwenty(β) && ninety(γ) ua = UniqueAxisB elseif ninety(α) && ninety(β) && onetwenty(γ) ua = UniqueAxisC else throw(ArgumentError("Can't determine unique axis")) end end UnitCell(latticetype, centering, ua, a, b, c, α, β, γ) end """ UnitCell(latticetype, centering, a, b, c) Construct a `UnitCell` for an `OrthorhombicLattice`, `TetragonalLattice` or `CubicLattice`. """ function UnitCell(latticetype::LatticeType, centering::CenteringType, a::Real, b::Real, c::Real) if latticetype in (OrthorhombicLattice, TetragonalLattice, CubicLattice) UnitCell(latticetype, centering, a, b, c, 90, 90, 90) else throw(ArgumentError("More parameters needed for this type of lattice")) end end """ UnitCell(CubicLattice, centering, a) Construct a `UnitCell` for a `CubicLattice`. """ function UnitCell(latticetype::LatticeType, centering::CenteringType, a::Real) if latticetype == CubicLattice UnitCell(latticetype, centering, a, a, a, 90, 90, 90) else throw(ArgumentError("More parameters needed for this type of lattice")) end end """ UnitCell(RhombohedralLattice, a, α) Construct a `UnitCell` for a `RhombohedralLattice`. """ function UnitCell(latticetype::LatticeType, a::Real, α::Real) if latticetype == RhombohedralLattice UnitCell(latticetype, RhombohedralCell, a, a, a, α, α, α) else throw(ArgumentError("More parameters needed for this type of lattice")) end end function getlatticetype(cell) lt = ccall((:cell_get_lattice_type, libcrystfel), Cint, (Ptr{InternalUnitCell},), cell.internalptr) cen = ccall((:cell_get_centering, libcrystfel), Cchar, (Ptr{InternalUnitCell},), cell.internalptr) ua = ccall((:cell_get_unique_axis, libcrystfel), Cchar, (Ptr{InternalUnitCell},), cell.internalptr) return LatticeType(lt),CenteringType(cen),UniqueAxis(ua) end function getcellparams(cell) let a=Ref{Cdouble}(0), b=Ref{Cdouble}(0), c=Ref{Cdouble}(0), α=Ref{Cdouble}(0), β=Ref{Cdouble}(0), γ=Ref{Cdouble}(0) ccall((:cell_get_parameters, libcrystfel), Cvoid, (Ptr{InternalUnitCell}, Ref{Cdouble},Ref{Cdouble},Ref{Cdouble}, Ref{Cdouble},Ref{Cdouble},Ref{Cdouble}), cell.internalptr, a, b, c, α, β, γ) return (a=a[], b=b[], c=c[], α=α[], β=β[], γ=γ[]) end end # Returns the direct-space basis vectors as a Julia matrix # See matrix-notation.pdf for information. This returns an "M-matrix". function directcartesianmatrix(uc) ax = Ref{Cdouble}(0) ay = Ref{Cdouble}(0) az = Ref{Cdouble}(0) bx = Ref{Cdouble}(0) by = Ref{Cdouble}(0) bz = Ref{Cdouble}(0) cx = Ref{Cdouble}(0) cy = Ref{Cdouble}(0) cz = Ref{Cdouble}(0) out = @ccall libcrystfel.cell_get_cartesian(uc.internalptr::Ptr{InternalUnitCell}, ax::Ref{Cdouble}, ay::Ref{Cdouble}, az::Ref{Cdouble}, bx::Ref{Cdouble}, by::Ref{Cdouble}, bz::Ref{Cdouble}, cx::Ref{Cdouble}, cy::Ref{Cdouble}, cz::Ref{Cdouble})::Cint if out != 0 throw(ErrorException("Failed to convert cell parameters")) end return [ax[] bx[] cx[]; ay[] by[] cy[]; az[] bz[] cz[]] end # Returns the reciprocal-space basis vectors as a Julia matrix # See matrix-notation.pdf for information. This returns an "R-matrix". function reciprocalcartesianmatrix(uc) ax = Ref{Cdouble}(0) ay = Ref{Cdouble}(0) az = Ref{Cdouble}(0) bx = Ref{Cdouble}(0) by = Ref{Cdouble}(0) bz = Ref{Cdouble}(0) cx = Ref{Cdouble}(0) cy = Ref{Cdouble}(0) cz = Ref{Cdouble}(0) out = @ccall libcrystfel.cell_get_reciprocal(uc.internalptr::Ptr{InternalUnitCell}, ax::Ref{Cdouble}, ay::Ref{Cdouble}, az::Ref{Cdouble}, bx::Ref{Cdouble}, by::Ref{Cdouble}, bz::Ref{Cdouble}, cx::Ref{Cdouble}, cy::Ref{Cdouble}, cz::Ref{Cdouble})::Cint if out != 0 throw(ErrorException("Failed to convert cell parameters")) end return [ax[] ay[] az[]; bx[] by[] bz[]; cx[] cy[] cz[]] end function Base.propertynames(uc::UnitCell; private=false) (:a, :b, :c, :α, :β, :γ, :latticetype, :cellparams, :directcartesian, :reciprocalcartesian, :internalptr) end function Base.getproperty(uc::UnitCell, name::Symbol) if name === :internalptr getfield(uc, :internalptr) elseif name === :cellparams return getcellparams(uc) elseif name === :latticetype return getlatticetype(uc) elseif name === :a return getcellparams(uc).a elseif name === :b return getcellparams(uc).b elseif name === :c return getcellparams(uc).c elseif name === :α return getcellparams(uc).α elseif name === :β return getcellparams(uc).β elseif name === :γ return getcellparams(uc).γ elseif name === :directcartesian return directcartesianmatrix(uc) elseif name === :reciprocalcartesian return reciprocalcartesianmatrix(uc) end end function Base.show(io::IO, uc::UnitCell) write(io, "UnitCell(") lt,cen,ua = uc.latticetype show(io, lt); write(io, ", ") show(io, cen); write(io, ", ") show(io, ua); write(io, ",\n ") @printf(io, "%.3f Å, %.3f Å, %.3f Å, %.3f°, %.3f°, %.3f°", uc.a*1e10, uc.b*1e10, uc.c*1e10, rad2deg(uc.α), rad2deg(uc.β), rad2deg(uc.γ)) write(io, ")") end # This type is for talking to libcrystfel, and is named to avoid conflicting # with other quaternion libraries. mutable struct CrystFELQuaternion w::Cdouble x::Cdouble y::Cdouble z::Cdouble end function randomquat() r = ()->2.0*rand(Float64)-1.0 q = [r(), r(), r(), r()] q ./ √(sum(q.^2)) end """ rotatecell(uc::UnitCell, quaternion) Rotate a unit cell according to a quaternion (represented as a vector of 4 floats). """ function rotatecell(uc, quat) q = CrystFELQuaternion(quat...) out = @ccall libcrystfel.cell_rotate(uc.internalptr::Ptr{InternalUnitCell}, q::CrystFELQuaternion)::Ptr{InternalUnitCell} if out == C_NULL throw(ErrorException("Failed to rotate unit cell")) end UnitCell(out) end """ rotatecell(uc::UnitCell) Rotate a unit cell at random in three dimensions. Use this routine for simulating serial crystallography datasets. Equivalent to CrystFEL routine `cell_rotate(uc, random_quaternion())`. """ rotatecell(uc) = rotatecell(uc, randomquat()) end # of module