-
Notifications
You must be signed in to change notification settings - Fork 11
/
Misc.jl
216 lines (184 loc) · 7.41 KB
/
Misc.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
"""
atoms, x = read_xyz(filename)
Return the list of `atoms` (Array{Symbol, 1}) and their Cartesian coordinates
`x::Array{Float64, 2}` as stored in the .xyz file. `x[:, k]` will return Cartesian
coords of the kth atom.
# Arguments
- `filename::AbstractString`: The filename of the .xyz file
# Returns
- `atoms::Array{Symbol, 1}`: An array of atoms stored as symbols e.g. [:H, :H, :O] read
from the .xyz file.
- `x::Array{Float64, 2}`: The Cartesian coordinates of the atoms. `x[:, k]` will return cartesian coordinates of the k-th atom
"""
function read_xyz(filename::AbstractString)
f = open(filename)
lines = readlines(f)
if length(lines) == 0
return Symbol[], Float64[]
end
n = parse(Int, lines[1]) # get number of atoms
atoms = Symbol[]
x = zeros(Float64, 3, n)
for i = 1:n
push!(atoms, Symbol(split(lines[i + 2])[1]))
for j = 1:3
x[j, i] = parse(Float64, split(lines[i + 2])[1 + j])
end
end
close(f)
return atoms, x
end
"""
write_xyz(atoms, x, filename; comment="")
write_xyz(molecules, box, filename; comment="")
write_xyz(framework, filename; comment="", center=false)
Write a molecule, framework, or array of atoms & positions to an .xyz file.
# Arguments
- `atoms::Array{Symbol, 1}`: An array of atoms stored as symbols e.g. [:H, :H, :O]
- `x::Array{Float64, 2}`: The Cartesian coordinates of the atoms.
`x[:, k]` contains Cartesian coordinates of the k-th atom
- `molecules::Array{Molecule, 1}`: an array of molecules whose atoms to write to .xyz
- `framework::Framework`: a crystal structure whose atoms to write to .xyz
- `filename::AbstractString`: The filename of the .xyz file. (".xyz" appended automatically
if the extension is not provided.) (absolute path)
- `comment::AbstractString`: comment if you'd like to write to the file.
- `center::Bool`: shift atoms so that origin is the center of the `framework.box`
"""
function write_xyz(atoms::Array{Symbol, 1}, x::Array{Float64, 2},
filename::AbstractString; comment::AbstractString="")
if ! occursin(".xyz", filename)
filename *= ".xyz"
end
if length(atoms) != size(x)[2]
error("Number of atoms does not match number of coordinates provided!\n")
end
if size(x)[1] != 3
error("x should be 3 by number of atoms.\n")
end
xyzfile = open(filename, "w")
@printf(xyzfile, "%d\n%s\n", length(atoms), comment)
for i = 1:length(atoms)
@printf(xyzfile, "%s\t%.4f\t%.4f\t%.4f\n", atoms[i], x[1, i], x[2, i], x[3, i])
end
close(xyzfile)
return nothing
end
"""
atom_colors = read_cpk_colors()
Read in CPK color scheme for atoms. Return `atom_colors::Dict{Symbol, Tuple{Int, Int, Int}}` such that
`atom_colors[":C"]` gives RGB code for carbon as a tuple, `(144, 144, 144)`.
https://en.wikipedia.org/wiki/CPK_coloring
# Returns
- `atom_colors::Dict{Symbol, Tuple{Int, Int, Int}}`: A dictionary linking an element symbol to its' corresponding CPK color in RGB
"""
function read_cpk_colors()
atom_colors = Dict{Symbol, Tuple{Int, Int, Int}}()
df_colors = CSV.read(joinpath(PATH_TO_DATA, "cpk_atom_colors.csv"))
for row in eachrow(df_colors)
atom_colors[Symbol(row[:atom])] = (row[:R], row[:G], row[:B])
end
return atom_colors
end
"""
atomic_radii = read_atomic_radii()
Return `atomic_radii::Dict{Symbol, Float64}`, where `atom_masses[":C"]` gives
the atomic radii of carbon (10.87 Angstrom).
# Returns
- `atomic_radii::Dict{Symbol, Float64}`: A dictionary linking an element symbol to its' corresponding atomic radius
"""
function read_atomic_radii()
atomic_radii = Dict{Symbol, Float64}()
df_props = CSV.read(joinpath(PATH_TO_DATA, "atom_properties.csv"))
for row in eachrow(df_props)
if ! ismissing(row[Symbol("atomicradius[Angstrom]")])
atomic_radii[Symbol(row[:atom])] = row[Symbol("atomicradius[Angstrom]")]
end
end
return atomic_radii
end
"""
atomic_masses = read_atomic_masses()
Read the `data/atomicmasses.csv` file to construct a dictionary of atoms and their atomic
masses in amu.
# Returns
- `atomic_masses::Dict{Symbol, Float64}`: A dictionary containing the atomic masses of each atom stored in `data/atomicmasses.csv`
"""
function read_atomic_masses()
if ! isfile(joinpath(PATH_TO_DATA, "atomicmasses.csv"))
error("Cannot find atomicmasses.csv file in your data folder\n")
end
df_am = CSV.read(joinpath(PATH_TO_DATA, "atomicmasses.csv"))
atomic_masses = Dict{Symbol, Float64}()
for row in eachrow(df_am)
atomic_masses[Symbol(row[:atom])] = row[:mass]
end
return atomic_masses
end
function _guess(df::DataFrame, pressure_col_name::Symbol, loading_col_name::Symbol, model::Symbol)
n = df[loading_col_name]
p = df[pressure_col_name]
if model == :langmuir
M0 = n[end] * 1.1
if isapprox(n[1], 0.0)
K0 = n[2]/p[2]
else
K0 = n[1]/p[1]
end
return Dict("M0" => M0, "K0" => K0)
elseif model == :henry
if isapprox(n[1], 0.0)
K0 = n[2]/p[2]
else
K0 = n[1]/p[1]
end
return Dict("K0" => K0)
else
error("Model not available. Currently only `:langmuir` and `:henry` are available")
end
end
"""
params = fit_isotherm(df, pressure_col_name, loading_col_name, model)
Takes in a DataFrame `df` containing an isotherm. Will try to fit a model to the data.
Available models are `:henry` and `:langmuir`
The Langmuir model is in the following form:
N = (MKP)/(1+KP)
where N is the total adsorption, M is the maximum monolayer coverage, K is the Langmuir constant and P is the partial pressure of the gas.
`params` will contain a fit to M and K
The Henry model takes the following form:
N = KP
and `params` will contain K
# Arguments
- `df::DataFrame`: The DataFrame containing the pressure and adsorption data for the isotherm
- `pressure_col_name::Symbol`: The header of the pressure column. Can be found with `names(df)`
- `loading_col_name::Symbol`: The header of the loading/adsorption column. Can be found with `names(df)`
- `model::Symbol`: The model chosen to fit the isotherm
# Returns
- `params::Dict{AbstractString, Float64}`: A Dictionary with the parameters corresponding to each model along with the MSE of the fit. `:langmuir` contains "M" and "K". `:henry` contains "K".
"""
function fit_isotherm(df::DataFrame, pressure_col_name::Symbol, loading_col_name::Symbol, model::Symbol; henry_tol::Float64 = 0.25)
sort!(df, [pressure_col_name])
n = df[loading_col_name]
p = df[pressure_col_name]
if ! isapprox(p[1], 0.0, atol=0.01)
prepend!(n, 0.0)
prepend!(p, 0.0)
end
θ0 = _guess(df, pressure_col_name, loading_col_name, model)
if model == :langmuir
objective_function_langmuir(θ) = return sum([(n[i] - θ[1] * θ[2] * p[i]/(1 + θ[2] * p[i]))^2 for i = 1:length(n)])
res = optimize(objective_function_langmuir, [θ0["M0"], θ0["K0"]], LBFGS())
M, K = res.minimizer
mse = res.minimum / length(n)
return Dict("M" => M, "K" => K, "MSE" => mse)
elseif model == :henry
min_mse = Inf
objective_function_henry(θ) = return sum([(n[i] - θ[1] * p[i])^2 for i = 1:length(n)])
res = optimize(objective_function_henry, [θ0["K0"]] * 1.1, LBFGS())
K = res.minimizer
mse = res.minimum / length(n)
if mse < min_mse
min_mse = mse
end
return Dict("K" => K[1], "MSE" => mse)
end
end