diff --git a/.github/workflows/CI_Build.yml b/.github/workflows/CI_Build.yml index 60ff74dfb..ec7f26abe 100644 --- a/.github/workflows/CI_Build.yml +++ b/.github/workflows/CI_Build.yml @@ -33,7 +33,7 @@ jobs: uses: julia-actions/julia-buildpkg@latest - name: install dependencies - run: julia --project --color=yes -e 'import Pkg; Pkg.add.(["FIGlet", "Coverage"])' + run: julia --project --color=yes -e 'import Pkg; Pkg.add.(["Coverage"])' - name: run tests run: julia --project --color=yes --code-coverage -e 'import Pkg; Pkg.test(coverage=true)' diff --git a/.github/workflows/formatter.yml b/.github/workflows/formatter.yml new file mode 100644 index 000000000..bcf2550d4 --- /dev/null +++ b/.github/workflows/formatter.yml @@ -0,0 +1,87 @@ +name: formatter + +on: + pull_request: + branches: [ master ] + +concurrency: # only allow the most recent workflow to execute + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: # environment variables + julia_version: '1.8.2' # julia version to use on all runners (except cross-platform-julia) + +jobs: + formatting: + runs-on: ubuntu-latest + steps: + - uses: mdecoleman/pr-branch-name@1.2.0 + id: vars + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - uses: julia-actions/setup-julia@latest + with: + version: ${{ env.julia_version }} + + - uses: actions/checkout@main + + - name: Install JuliaFormatter + run: julia -e 'using Pkg; Pkg.add("JuliaFormatter")' + + - name: Run JuliaFormatter + run: | + julia -e ' + using JuliaFormatter; + format( + ".", + verbose=true, + always_for_in=true, + whitespace_typedefs=true, + whitespace_ops_in_indices=true, + remove_extra_newlines=true, + short_to_long_function_def=true, + long_to_short_function_def=true, + always_use_return=true, + whitespace_in_kwargs=false, + format_docstrings=true, + conditional_to_if=true, + normalize_line_endings="unix", + trailing_comma=false, + separate_kwargs_with_semicolon=true, + format_markdown=true + )' + + - name: Formatting Check + run: | + julia -e ' + out = Cmd(`git diff --name-only`) |> read |> String + if out == "" + exit(0) + else + @error "Some files have not been formatted !!!" + write(stdout, out) + exit(1) + end' + + - if: ${{ failure() }} + id: cpr + name: Formatting PR + uses: peter-evans/create-pull-request@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + base: ${{ steps.vars.outputs.branch }} + commit-message: JuliaFormatter Action Bot + title: 'Automatic JuliaFormatter.jl run' + branch: auto-juliaformatter-pr + delete-branch: true + labels: formatting, automated pr, no changelog + + - if: ${{ failure() }} + name: comment on PR + uses: thollander/actions-comment-pull-request@v1 + with: + message: 'Code formatting requirements not met. See PR #${{ steps.cpr.outputs.pull-request-number }}' + pr_number: ${{ github.event.issue.number }} + comment_includes: "Code formatting requirements not met. See PR #" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/PMlogo.pdf b/PMlogo.pdf deleted file mode 100644 index 9c3ac2eee..000000000 Binary files a/PMlogo.pdf and /dev/null differ diff --git a/PorousMaterialsLogo.png b/PorousMaterialsLogo.png deleted file mode 100644 index 13e020746..000000000 Binary files a/PorousMaterialsLogo.png and /dev/null differ diff --git a/Project.toml b/Project.toml index e333af4c7..eadd3dd9a 100644 --- a/Project.toml +++ b/Project.toml @@ -1,21 +1,19 @@ name = "PorousMaterials" uuid = "68953c7c-a3c7-538e-83d3-73516288599e" authors = ["SimonEnsemble "] -version = "0.4.2" +version = "0.5.0" [deps] -Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" -FIGlet = "3064a664-84fe-4d92-92c7-ed492f3d8fae" -FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" Optim = "429524aa-4258-5aef-a3af-852621145aeb" Polynomials = "f27b6e38-b328-58d1-80ce-0feddd5e7a45" +PrecompileSignatures = "91cefc8d-f054-46dc-8f8c-26e11d7c5411" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" @@ -24,31 +22,25 @@ Roots = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Xtals = "ede5f01d-793e-4c47-9885-c447d1f18d6d" [compat] -Aqua = "0.5.5" -CSV = "0.10.4" +Aqua = "0.5" +CSV = "0.10" DataFrames = "1" -FIGlet = "0.2.1" -FileIO = "1" Graphs = "1" -JLD2 = "0.4.22" +JLD2 = "0.4" OffsetArrays = "1" Optim = "1" Polynomials = "3" +PrecompileSignatures = "3" ProgressMeter = "1" Reexport = "1" Roots = "2" SpecialFunctions = "2" -StatsBase = "0.33.16" -Xtals = "0.4" -julia = "1.7, 1.8" +StatsBase = "0.34" +Xtals = "0.5" +julia = "1.6" [extras] -Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[targets] -test = ["Test", "Documenter"] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" diff --git a/README.md b/README.md index 3bebff6de..6647fe67a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -![PorousMaterials.jl](PMlogo.png) +![PorousMaterials.jl](logo.png) -| **Documentation** | **DOI** | **Build Status** | **Test Coverage** | -|:---:|:---:|:---:|:---:| +| **Documentation** | **DOI** | **Build Status** | **Test Coverage** | +|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:----------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| | [![](https://img.shields.io/badge/docs-dev-blue.svg)](https://simonensemble.github.io/PorousMaterials.jl/dev) [![](https://img.shields.io/badge/docs-latest-blue.svg)](https://simonensemble.github.io/PorousMaterials.jl/stable) | [![DOI](https://zenodo.org/badge/102691401.svg)](https://zenodo.org/badge/latestdoi/102691401) | [![Build](https://github.com/SimonEnsemble/PorousMaterials.jl/actions/workflows/ci_testing.yml/badge.svg)](https://github.com/SimonEnsemble/PorousMaterials.jl/actions/workflows/ci_testing.yml) [![Docs](https://github.com/SimonEnsemble/PorousMaterials.jl/actions/workflows/doc_deployment.yml/badge.svg)](https://github.com/SimonEnsemble/PorousMaterials.jl/actions/workflows/doc_deployment.yml) [![Weekly](https://github.com/SimonEnsemble/PorousMaterials.jl/actions/workflows/weekly.yml/badge.svg)](https://github.com/SimonEnsemble/PorousMaterials.jl/actions/workflows/weekly.yml) | [![codecov](https://codecov.io/gh/SimonEnsemble/PorousMaterials.jl/branch/master/graph/badge.svg?token=PWsgNnxfZI)](https://codecov.io/gh/SimonEnsemble/PorousMaterials.jl) [![Aqua QA](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl) | A pure-[Julia](https://julialang.org/) package for classical molecular modeling of adsorption in porous crystals such as metal-organic frameworks (MOFs). diff --git a/docs/README.md b/docs/README.md index f82c0e123..709151593 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,7 +1,9 @@ notes: -- all documentation files are in `docs/src` -- `make.jl` automatically pulls doc strings from code in `src` -to deploy locally: -- run `julia make.jl` from `docs` directory. -- open `build/index.html` in your browswer + - all documentation files are in `docs/src` + - `make.jl` automatically pulls doc strings from code in `src` + +to deploy locally: + + - run `julia make.jl` from `docs` directory. + - open `build/index.html` in your browswer diff --git a/docs/make.jl b/docs/make.jl index a1747e44a..90cd8fb7c 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,27 +1,27 @@ using Documenter using PorousMaterials -makedocs( - root = joinpath(dirname(pathof(PorousMaterials)), "..", "docs"), - modules = [PorousMaterials], - sitename = "PorousMaterials.jl", - clean = true, - pages = [ - "PorousMaterials" => "index.md", - "paths" => "paths.md", - "matter" => "matter.md", - "boxes" => "box.md", - "crystals" => "crystal.md", - "molecules" => "molecule.md", - "computing distances" => "distance.md", - "forcefields" => "force_field.md", - "equations of state" => "eos.md", - "Henry coefficients" => "henry.md", - "grand-canonical Monte Carlo simulations" => "gcmc.md", - "energy minimum" => "energy_min.md", - "grids" => "grid.md" - ], - format = Documenter.HTML(assets = ["assets/flux.css"]) +makedocs(; + root=joinpath(dirname(pathof(PorousMaterials)), "..", "docs"), + modules=[PorousMaterials], + sitename="PorousMaterials.jl", + clean=true, + pages=[ + "PorousMaterials" => "index.md", + "paths" => "paths.md", + "matter" => "matter.md", + "boxes" => "box.md", + "crystals" => "crystal.md", + "molecules" => "molecule.md", + "computing distances" => "distance.md", + "forcefields" => "force_field.md", + "equations of state" => "eos.md", + "Henry coefficients" => "henry.md", + "grand-canonical Monte Carlo simulations" => "gcmc.md", + "energy minimum" => "energy_min.md", + "grids" => "grid.md" + ], + format=Documenter.HTML(; assets=["assets/flux.css"]) ) -deploydocs(repo="github.com/SimonEnsemble/PorousMaterials.jl.git") +deploydocs(; repo="github.com/SimonEnsemble/PorousMaterials.jl.git") diff --git a/docs/src/box.md b/docs/src/box.md index 5cd4a6580..02c08a8b1 100644 --- a/docs/src/box.md +++ b/docs/src/box.md @@ -1,6 +1,6 @@ ```@meta DocTestSetup = quote - using Xtals + using PorousMaterials end ``` @@ -22,39 +22,46 @@ A `Box` is most conveniently constructed from its basic spatial data (`a` `b` `c a = 26.13173 # Å b = 26.13173 c = 6.722028 -α = π/2 # radians -β = π/2 -γ = 2*π/3 +α = π / 2 # radians +β = π / 2 +γ = 2 * π / 3 box = Box(a, b, c, α, β, γ) + # output + Bravais unit cell of a crystal. - Unit cell angles α = 90.000000 deg. β = 90.000000 deg. γ = 120.000000 deg. - Unit cell dimensions a = 26.131730 Å. b = 26.131730 Å, c = 6.722028 Å - Volume of unit cell: 3975.275878 ų + Unit cell angles α = 90.000000 deg. β = 90.000000 deg. γ = 120.000000 deg. + Unit cell dimensions a = 26.131730 Å. b = 26.131730 Å, c = 6.722028 Å + Volume of unit cell: 3975.275878 ų ``` A `Box` may also be defined by providing only the `Frac`tional-to-`Cart`esian conversion matrix: + ```jldoctest box box = Box([26.1317 -13.0659 0; 0 22.6307 0; 0 0 6.72203]) + # output + Bravais unit cell of a crystal. - Unit cell angles α = 90.000000 deg. β = 90.000000 deg. γ = 120.000113 deg. - Unit cell dimensions a = 26.131700 Å. b = 26.131711 Å, c = 6.722030 Å - Volume of unit cell: 3975.265115 ų + Unit cell angles α = 90.000000 deg. β = 90.000000 deg. γ = 120.000113 deg. + Unit cell dimensions a = 26.131700 Å. b = 26.131711 Å, c = 6.722030 Å + Volume of unit cell: 3975.265115 ų ``` To quickly get a simple unit-cubic `Box`, use the `unit_cube` function. + ```jldoctest unit_cube() + # output + Bravais unit cell of a crystal. - Unit cell angles α = 90.000000 deg. β = 90.000000 deg. γ = 90.000000 deg. - Unit cell dimensions a = 1.000000 Å. b = 1.000000 Å, c = 1.000000 Å - Volume of unit cell: 1.000000 ų + Unit cell angles α = 90.000000 deg. β = 90.000000 deg. γ = 90.000000 deg. + Unit cell dimensions a = 1.000000 Å. b = 1.000000 Å, c = 1.000000 Å + Volume of unit cell: 1.000000 ų ``` - ## transforming coordinates Conversions are provided for switching between `Frac`tional and `Cart`esian `Coords` @@ -63,26 +70,28 @@ using the `Box` (works for `Atoms` and `Charges`, too) ```jldoctest box xtal = Crystal("SBMOF-1.cif") Cart(xtal.atoms.coords, xtal.box) + # output + Cart([4.594867082350715 -0.952720283971488 … 0.8392490029633858 -1.5321086078257065; 1.4395486200000005 4.2228986200000005 … 1.4289162230000012 4.212266223; 5.89964228469024 5.359217037237699 … 17.537474811394276 16.239103154389543]) ``` - ## replicating a box For simulations in larger volumes than a single crystallograhic unit cell, the `Box` may be replicated along each or any of the three crystallographic axes. ```jldoctest box -replicated_box = replicate(box, (2,2,2)) +replicated_box = replicate(box, (2, 2, 2)) + # output + Bravais unit cell of a crystal. - Unit cell angles α = 90.000000 deg. β = 90.000000 deg. γ = 120.000113 deg. - Unit cell dimensions a = 52.263400 Å. b = 52.263422 Å, c = 13.444060 Å - Volume of unit cell: 31802.120923 ų + Unit cell angles α = 90.000000 deg. β = 90.000000 deg. γ = 120.000113 deg. + Unit cell dimensions a = 52.263400 Å. b = 52.263422 Å, c = 13.444060 Å + Volume of unit cell: 31802.120923 ų ``` - ## exporting a box For visualization of the unit cell boundaries, the `Box` may be written out to a @@ -90,11 +99,11 @@ For visualization of the unit cell boundaries, the `Box` may be written out to a ```jldoctest box write_vtk(box, "box.vtk") + # output ``` - # detailed docs ```@docs diff --git a/docs/src/crystal.md b/docs/src/crystal.md index 0d96ade51..845567bf3 100644 --- a/docs/src/crystal.md +++ b/docs/src/crystal.md @@ -20,10 +20,12 @@ xtal.box # The unit cell information xtal.atoms # The atom coordinates (in fractional space) and the atom identities xtal.charges # The charge magnitude and coordinates (in fractional space) xtal.bonds # Bonding information in the structure. By default this is an empty graph, - # but use `read_bonds_from_file=true` argument in `Crystal` to read from crystal structure file +# but use `read_bonds_from_file=true` argument in `Crystal` to read from crystal structure file xtal.symmetry # Symmetry information of the crystal. By default converts the symmetry to P1 symmetry. - # Use `convert_to_p1=false` argument in `Crystal` to keep original symmetry +# Use `convert_to_p1=false` argument in `Crystal` to keep original symmetry + # output + Xtals.SymmetryInfo(["x"; "y"; "z";;], "P1", true) ``` @@ -37,6 +39,7 @@ It is important to use this function prior to GCMC or Henry coefficient calculat strip_numbers_from_atom_labels!(xtal) # output + ``` ## converting the coordinates to cartesian space @@ -47,7 +50,9 @@ that can be done by using the unit cell information of the crystal. ```jldoctest crystal xtal.atoms.coords.xf # array of fractional coordinates cart_coords = xtal.box.f_to_c * xtal.atoms.coords.xf # array of cartesian coordinates + # output + 3×120 Matrix{Float64}: 4.59487 -0.95272 2.68943 8.23701 … 8.8164 0.839249 -1.53211 1.43955 4.2229 4.12715 1.3438 1.35443 1.42892 4.21227 @@ -59,23 +64,14 @@ cart_coords = xtal.box.f_to_c * xtal.atoms.coords.xf # array of cartesian coo For many simulations, one needs to replicate the unit cell multiple times to create a bigger super cell. ```jldoctest crystal -super_xtal = replicate(xtal, (2,2,2)) # Replicates the original unit cell once in each dimension +super_xtal = replicate(xtal, (2, 2, 2)) # Replicates the original unit cell once in each dimension + # output -Name: SBMOF-1.cif -Bravais unit cell of a crystal. - Unit cell angles α = 90.000000 deg. β = 100.897000 deg. γ = 90.000000 deg. - Unit cell dimensions a = 23.238600 Å. b = 11.133400 Å, c = 45.862400 Å - Volume of unit cell: 11651.776815 ų - - # atoms = 960 - # charges = 960 - chemical formula: Ca₃₂C₄₄₈H₂₅₆O₁₉₂S₃₂ - space Group: P1 - symmetry Operations: - 'x, y, z' - bonding graph: - # vertices = 960 - # edges = 0 + +Crystal(Ca₃₂C₄₄₈H₂₅₆O₁₉₂S₃₂, periodic = TTT): + bounding_box : [ 23.2386 0 0; + 6.81724e-16 11.1334 0; + -8.67001 3.33915e-15 45.0354]u"Å" ``` ## finding other properties @@ -84,7 +80,9 @@ Bravais unit cell of a crystal. rho = crystal_density(xtal) # Crystal density of the crystal in kg/m^2 mw = molecular_weight(xtal) # The molecular weight of the unit cell in amu formula = chemical_formula(xtal) # The irreducible chemical formula of the crystal + # output + "Ca₄C₅₆H₃₂O₂₄S₄" ``` @@ -95,11 +93,17 @@ If the crystal structure file does not contains partial charges, we provide meth ```julia species_to_charges = Dict(:Ca => 2.0, :C => 1.0, :H => -1.0) # This method assigns a static charge to atom species charged_xtal = assign_charges(xtal, species_to_charges, 1e-5) # This function creates a new charged `Crystal` object. - # The function checks for charge neutrality with a tolerance of 1e-5 +# The function checks for charge neutrality with a tolerance of 1e-5 new_charges = Charges([2.0, 1.0, -1.0, -1.0, ...], xtal.atoms.coords) -other_charged_xtal = Crystal(xtal.name, xtal.box, xtal.atoms, # Here we create a new `Charges` object using an array of new charges. - new_charges, xtal.bonds, xtal.symmetry) # The number of charges in the array has to be equal to the number of atoms - # and finally a new `Crystal` object is manually created +other_charged_xtal = Crystal( + xtal.name, + xtal.box, + xtal.atoms, # Here we create a new `Charges` object using an array of new charges. + new_charges, + xtal.bonds, + xtal.symmetry +) # The number of charges in the array has to be equal to the number of atoms +# and finally a new `Crystal` object is manually created ``` ## writing crystal files @@ -109,11 +113,11 @@ We provide methods to write both `.xyz` and `.cif` files ```jldoctest crystal; output=false write_cif(xtal, "my_new_cif_file.cif") # Stored in the current directory write_xyz(xtal, "my_new_xyz_file.xyz") # stored in the current directory + # output ``` - # detailed docs ```@docs diff --git a/docs/src/distance.md b/docs/src/distance.md index 6553786c5..1a2f258aa 100644 --- a/docs/src/distance.md +++ b/docs/src/distance.md @@ -14,7 +14,9 @@ within a given `box`. ```jldoctest distance xtal = Crystal("SBMOF-1.cif") distance(xtal.atoms.coords, xtal.box, 1, 10, false) # Cartesian distance within the unit cell + # output + 4.962373067546231 ``` @@ -23,7 +25,9 @@ across the periodic boundaries of the `box`. ```jldoctest distance distance(xtal.atoms.coords, xtal.box, 1, 10, true) # Cartesian distance accounting for periodic boundary + # output + 4.143597209982431 ``` @@ -31,7 +35,9 @@ distance(xtal.atoms.coords, xtal.box, 1, 10, true) # Cartesian distance accounti ```jldoctest distance distance(xtal.atoms, xtal.box, 3, 5, true) + # output + 10.244292605252747 ``` diff --git a/docs/src/energy_min.md b/docs/src/energy_min.md index 71c33a3f3..bd2ff60a3 100644 --- a/docs/src/energy_min.md +++ b/docs/src/energy_min.md @@ -10,23 +10,26 @@ Here we show how to find the minimum energy (acc. to a force field) position of ### Example -For example, we wish to find the minimum energy (acc. to the UFF) position of a xenon adsorbate in SBMOF-1. +For example, we wish to find the minimum energy (acc. to the UFF) position of a xenon adsorbate in SBMOF-1. ```jldoctest xtal = Crystal("SBMOF-1.cif") -molecule = Molecule("Xe") +molecule = Molecule("Xe") ljff = LJForceField("UFF") # grid search to find min energy position. # gives good starting guess for optimization algorithm to fine tune. resolution = 1.0 # resolution of grid points in Å -minimized_molecule, min_E = find_energy_minimum_gridsearch(xtal, molecule, ljff, resolution=resolution) +minimized_molecule, min_E = + find_energy_minimum_gridsearch(xtal, molecule, ljff; resolution=resolution) # minimized_molecule: xenon at its min energy position # min_E: associated minimum energy of xenon (kJ/mol) # fine tune the minimum energy position according to the grid search. minimized_molecule, min_E = find_energy_minimum(xtal, minimized_molecule, ljff) + # output + Computing energy grid of Xe in SBMOF-1.cif Regular grid (in fractional space) of 13 by 7 by 24 points superimposed over the unit cell. (Molecule species: Xe @@ -37,6 +40,7 @@ Atoms: ``` # detailed docs + ```@docs find_energy_minimum find_energy_minimum_gridsearch diff --git a/docs/src/eos.md b/docs/src/eos.md index 9d7573ec7..8a282077b 100644 --- a/docs/src/eos.md +++ b/docs/src/eos.md @@ -12,7 +12,7 @@ end The Peng-Robinson equation of state can be written as: -![PREOS](https://latex.codecogs.com/gif.latex?P%20%3D%5Cfrac%7BRT%7D%7BV%7Bm%7D-b%7D-%5Cfrac%7Ba%5Calpha%7D%7BV%7B%7Bm%7D%7D%5E%7B2%7D+2bV%7Bm%7D-b%5E%7B2%7D%7D) +![PREOS](https://latex.codecogs.com/gif.latex?P%20%3D%5Cfrac%7BRT%7D%7BV%7Bm%7D-b%7D-%5Cfrac%7Ba%5Calpha%7D%7BV%7B%7Bm%7D%7D%5E%7B2%7D+2bV%7Bm%7D-b%5E%7B2%7D%7D) where $V_{m}$ is the molar volume, $R$ is the gas constant, and $T$ is temperature. @@ -28,7 +28,7 @@ and $\alpha$ can be calculated using acentric factor $\omega$ and critical tempe where -![PREOS_kappa](https://latex.codecogs.com/gif.latex?%5Ckappa%20%5Capprox%200.37464+1.54226%5Comega-0.26992%5Comega%5E%7B2%7D) +![PREOS_kappa](https://latex.codecogs.com/gif.latex?%5Ckappa%20%5Capprox%200.37464+1.54226%5Comega-0.26992%5Comega%5E%7B2%7D) and @@ -62,7 +62,9 @@ fluid.fluid # The name of the fluid fluid.Pc # The critical pressure of the fluid fluid.Tc # The critical temperature of the fluid fluid.ω # The acentric factor of the fluid + # output + 0.00363 ``` @@ -73,17 +75,23 @@ fluid = VdWFluid(:Xe) # Input fluid as a symbol. The fluids reade fluid.fluid # The name of the fluid fluid.a # The van der Waals constant a of the fluid fluid.b # The van der Waals constant b of the fluid + # output + 5.105e-5 ``` ## calculating density, fugacity, and molar volume + Using a given temperature and pressure, `PorousMaterials.jl` the equation of state can be used to calculate the dnesity, fugacity, and molar volume of a real fluid, stored as a dictionary. + ```jldoctest eos T = 298.0 # K # The temperature in Kelvin of interest type Float64. P = 1.0 # bar # The pressure in bar of interest type Float64. -props = calculate_properties(fluid, T, P, verbose=true) # verbose::Bool will print results if `true` +props = calculate_properties(fluid, T, P; verbose=true) # verbose::Bool will print results if `true` + # output + Xe properties at T = 298.000000 K, P = 1.000000 bar: compressibility factor: 0.995117906779058 fugacity coefficient: 0.9951492697826048 @@ -99,13 +107,16 @@ Dict{String, Float64} with 5 entries: ``` The output is a dictionary containing the following keys: + ```jldoctest eos; output=false props["compressibility factor"] # the compressibility factor props["density (mol/m³)"] # fluid density in mol/m³ props["fugacity (bar)"] # the fugacity in bar props["fugacity coefficient"] # the fugacity coefficient props["molar volume (L/mol)"] # the molar volume in L/mol + # output + 24.65612613988038 ``` @@ -116,4 +127,3 @@ props["molar volume (L/mol)"] # the molar volume in L/mol VdWFluid calculate_properties ``` - diff --git a/docs/src/force_field.md b/docs/src/force_field.md index 3b7bea175..36bdef06d 100644 --- a/docs/src/force_field.md +++ b/docs/src/force_field.md @@ -22,8 +22,10 @@ Reading in Lennard-Jones force field parameters is made easy with the [`LJForceF ```jldoctest force_field # read in Lennard-Jones force field parameters from the Universal Force Field -ljforcefield = LJForceField("UFF", r_cutoff=14.0, mixing_rules="Lorentz-Berthelot") +ljforcefield = LJForceField("UFF"; r_cutoff=14.0, mixing_rules="Lorentz-Berthelot") + # output + Force field: UFF Number of atoms included: 108 Cut-off radius (Å) = 14.0 @@ -137,7 +139,7 @@ O_CO2-O_CO2 ϵ = 79.00000 K, σ = 3.05000 Å He- He ϵ = 28.18319 K, σ = 2.10430 Å ``` -This also prints all of the atoms included in the loaded forcefield with their given ϵ and σ. This was excluded because it would use too much space on this page. +This also prints all of the atoms included in the loaded forcefield with their given ϵ and σ. This was excluded because it would use too much space on this page. We can access attributes `LJForceField` such as `pure_σ`, `pure_ϵ`, and interaction values: @@ -149,7 +151,9 @@ ljforcefield.pure_σ[:Xe] # Å # access the Lennard-Jones epsilon & sigma for Xe-C interactions ljforcefield.ϵ[:Xe][:C] # K ljforcefield.σ²[:Xe][:C] # Å (store σ² for faster computation) + # output + 13.521685546424905 ``` @@ -165,7 +169,9 @@ forcefield_coverage(xtal, ljforcefield) # check if the atoms in a molecule are covered molecule = Molecule("CO2") forcefield_coverage(molecule, ljforcefield) + # output + true ``` @@ -175,8 +181,10 @@ Find the replication factors needed to make a supercell big enough to fit a sphe ```jldoctest force_field r_cutoff = 14.0 # Å -repfactors = replication_factors(xtal.box, r_cutoff) +repfactors = replication_factors(xtal.box, r_cutoff) + # output + (3, 6, 2) ``` @@ -187,16 +195,19 @@ What is the van der Waals potential energy of a Xe adsorbate inside SBMOF-1 at C ```jldoctest force_field # load molecule and convert it to fractional molecule = Molecule("Xe") -molecule = Frac(molecule, xtal.box) +molecule = Frac(molecule, xtal.box) translate_to!(molecule, Cart([0.0, 1.0, 3.0]), xtal.box) # need box b/c we're in Cartesian energy = vdw_energy(xtal, molecule, ljforcefield) # K + # output + 5.73882798944654e6 ``` # detailed docs ## Forcefields + ```@docs LJForceField replication_factors @@ -204,17 +215,20 @@ energy = vdw_energy(xtal, molecule, ljforcefield) # K ``` ## Potential Energy + ```@docs PotentialEnergy SystemPotentialEnergy ``` ## Nearest Image Conventions + ```@docs nearest_image! ``` ## Electrostatics Energy + ```@docs Eikr total @@ -226,6 +240,7 @@ energy = vdw_energy(xtal, molecule, ljforcefield) # K ``` ## Van der Waals Energy + ```@docs lennard_jones vdw_energy diff --git a/docs/src/gcmc.md b/docs/src/gcmc.md index 598adabea..dde5fe7a7 100644 --- a/docs/src/gcmc.md +++ b/docs/src/gcmc.md @@ -5,13 +5,20 @@ Simulate the adsorption of CO$_2$ in FIQCEN\_clean\_min\_charges (CuBTC) at 298 ```julia xtal = Crystal("FIQCEN_clean.cif") # load a crystal structure strip_numbers_from_atom_labels!(xtal) # clean up the atom labels -ljforcefield = LJForceField("UFF", r_cutoff=12.8) # load the UFF forcefield +ljforcefield = LJForceField("UFF"; r_cutoff=12.8) # load the UFF forcefield molecule = Molecule("CO2") # load the CO2 molecule temperature = 298.0 # K pressure = 1.0 # bar # conduct Grand-Canonical Monte Carlo simulation (VERY short, should use thousands of cycles!) -results, molecules = μVT_sim(xtal, molecule, temperature, pressure, ljforcefield, - n_burn_cycles=50, n_sample_cycles=50) +results, molecules = μVT_sim( + xtal, + molecule, + temperature, + pressure, + ljforcefield; + n_burn_cycles=50, + n_sample_cycles=50 +) ``` Or, compute the entire adsorption isotherm at once, parallelized across many cores (this works by cleverly queuing a [`μVT_sim`](@ref) for each pressue across the specified number of cores for optimal efficiency): @@ -19,20 +26,35 @@ Or, compute the entire adsorption isotherm at once, parallelized across many cor ```julia pressures = [0.2, 0.6, 0.8, 1.0] # bar # loop over all pressures and compute entire adsorption isotherm in parallel -results = adsorption_isotherm(xtal, molecule, temperature, pressures, ljforcefield, - n_burn_cycles=50, n_sample_cycles=50) +results = adsorption_isotherm( + xtal, + molecule, + temperature, + pressures, + ljforcefield; + n_burn_cycles=50, + n_sample_cycles=50 +) ``` - + Or, compute the adsorption isotherm in a step-wise manner, loading the molecules from the previous simulation to save on burn cycles: ```julia -results = stepwise_adsorption_isotherm(xtal, molecule, temperature, pressures, forcefield, - n_burn_cycles=50, n_sample_cycles=50) +results = stepwise_adsorption_isotherm( + xtal, + molecule, + temperature, + pressures, + forcefield; + n_burn_cycles=50, + n_sample_cycles=50 +) ``` # detailed docs ## Grand-Canonical Monte Carlo Simulations + ```@docs μVT_sim adsorption_isotherm @@ -40,4 +62,3 @@ results = stepwise_adsorption_isotherm(xtal, molecule, temperature, pressures, f μVT_output_filename isotherm_sim_results_to_dataframe ``` - diff --git a/docs/src/grid.md b/docs/src/grid.md index c89a6e623..ae371849d 100644 --- a/docs/src/grid.md +++ b/docs/src/grid.md @@ -17,9 +17,10 @@ xtal = Crystal("SBMOF-1.cif") strip_numbers_from_atom_labels!(xtal) molecule = Molecule("Xe") ljforcefield = LJForceField("UFF") -grid = energy_grid(xtal, molecule, ljforcefield, - resolution=0.5, units=:kJ_mol) +grid = energy_grid(xtal, molecule, ljforcefield; resolution=0.5, units=:kJ_mol) + # output + Computing energy grid of Xe in SBMOF-1.cif Regular grid (in fractional space) of 25 by 13 by 47 points superimposed over the unit cell. Regular grid of 25 by 13 by 47 points superimposed over a unit cell and associated data. @@ -35,9 +36,11 @@ grid.data # 3 dim array containing data for each point grid.n_pts # number of grid points in x, y, z grid.origin # the origin of the grid grid.units # units associated with each data point + # output + :kJ_mol -``` +``` ### Saving and Retrieving Grids @@ -55,7 +58,9 @@ grid = read_cube(filename) ``` # detailed docs + ## Grids + ```@docs Grid energy_grid diff --git a/docs/src/henry.md b/docs/src/henry.md index fc4c95232..c9eb3ae81 100644 --- a/docs/src/henry.md +++ b/docs/src/henry.md @@ -5,9 +5,10 @@ ## Preparing the Henry coefficient simulation The simulation requires the following `PorousMaterials.jl` objects: -* `Crystal` structure -* `Molecule` adsorbate -* `LJForceField` forcefield + + - `Crystal` structure + - `Molecule` adsorbate + - `LJForceField` forcefield In addition the the list above, one has to specify the temperature (in K) and the number of Widom insertions per unit volume (in Angstrom). @@ -19,7 +20,8 @@ ljff = LJForceField("UFF") # We will use the Universal Force Field temp = 298.0 # Standard temperature (K) widom_insertions = 2000 # Number of insertions per unit volume -results = henry_coefficient(xtal, methane, temp, ljff, insertions_per_volume=widom_insertions) +results = + henry_coefficient(xtal, methane, temp, ljff; insertions_per_volume=widom_insertions) ``` The results are also saved to `rc[:paths][:simulations]` as a `.jld2` file that can be read using the `JLD2` package. @@ -28,7 +30,9 @@ The output (and saved file) is a dictionary: ```julia results + # output + Dict{String, Any} with 16 entries: "⟨U⟩ (K)" => -3623.51 "err Qst (kJ/mol)" => 0.0917643 @@ -51,6 +55,7 @@ Dict{String, Any} with 16 entries: ## locating the saved results The name of the result filenames follow a convention outlined in `henry_result_savename`. + ```julia using JLD2 # determine the canonical filename for the simulation diff --git a/docs/src/index.md b/docs/src/index.md index 7ca23dd31..b3f86b6bc 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,35 +1,37 @@ -![PorousMaterials.jl](assets/PMlogo.png) -A pure-[Julia](https://julialang.org/) package for classical molecular modeling of adsorption in porous crystals such as metal-organic frameworks (MOFs). - -🔨 Compute the potential energy of a molecule at particular position and orientation inside of a porous crystal - -🔨 Write a potential energy grid of a molecule inside a porous material to visualize binding sites - -🔨 Compute the Henry coefficient of a gas in a porous crystal - -🔨 Run grand-canonical Monte Carlo simulations of gas adsorption in a porous crystal - -Designed for high-throughput computations to minimize input files and querying results from output files. User-friendly. Instructive error messages thrown when they should be. Well-documented. Easy to install. - -*In development, please contribute, post issues 🐛, and improve!* - -## Installation - -1. Download and install the [Julia programming language](https://julialang.org/), v1.5 or higher. - -2. In Julia, open the package manager (using `]`) and enter the following: - -```julia -pkg> add PorousMaterials -``` - -3. In Julia, load all functions in `PorousMaterials.jl` into the namespace: - -```julia -julia> using PorousMaterials # that's it -``` - -## Tests -Run the tests in the script `tests/runtests.jl` manually or by `] test PorousMaterials` in the Julia REPL. - -Direct tests for Henry coefficients and grand-canonical Monte Carlo simulations take much longer and must be run separately; they are found in `tests/henry.jl` and `tests/gcmc_long.jl`. +![PorousMaterials.jl](assets/PMlogo.png) +A pure-[Julia](https://julialang.org/) package for classical molecular modeling of adsorption in porous crystals such as metal-organic frameworks (MOFs). + +🔨 Compute the potential energy of a molecule at particular position and orientation inside of a porous crystal + +🔨 Write a potential energy grid of a molecule inside a porous material to visualize binding sites + +🔨 Compute the Henry coefficient of a gas in a porous crystal + +🔨 Run grand-canonical Monte Carlo simulations of gas adsorption in a porous crystal + +Designed for high-throughput computations to minimize input files and querying results from output files. User-friendly. Instructive error messages thrown when they should be. Well-documented. Easy to install. + +*In development, please contribute, post issues 🐛, and improve!* + +## Installation + + 1. Download and install the [Julia programming language](https://julialang.org/), v1.5 or higher. + + 2. Open the REPL and enter the following: + +```julia +import Pkg +Pkg.add(PorousMaterials) +``` + + 3. In Julia, load all functions in `PorousMaterials.jl` into the namespace: + +```julia +using PorousMaterials # that's it +``` + +## Tests + +Run the tests in the script `tests/runtests.jl` manually or by `] test PorousMaterials` in the Julia REPL. + +Direct tests for Henry coefficients and grand-canonical Monte Carlo simulations take much longer and must be run separately; they are found in `tests/henry.jl` and `tests/gcmc_long.jl`. diff --git a/docs/src/matter.md b/docs/src/matter.md index d8757b822..088f715cf 100644 --- a/docs/src/matter.md +++ b/docs/src/matter.md @@ -13,7 +13,7 @@ end we store coordinates as an abstract `Coords` type that has two subtypes: `Cart` and `Frac` for Cartesian and Fractional, respectively. see the [Wikipedia](https://en.wikipedia.org/wiki/Fractional_coordinates) page on fractional coordinates, which are defined in the context of a periodic system, e.g. within a crystal. -construct coordinates of `n` particles by passing a `n` by `3` array +construct coordinates of `n` particles by passing a `n` by `3` array ```jldoctest; output=false coord = Cart([1.0, 2.0, 5.0]) # construct cartesian coordinate of a particle @@ -21,7 +21,9 @@ coord.x # 3 x 1 array, [1, 2, 3] coord = Frac([0.1, 0.2, 0.5]) # construct fractional coordinate of a particle coord.xf # 3 x 1 array, [0.1, 0.2, 0.3] + # output + 3×1 Matrix{Float64}: 0.1 0.2 @@ -31,12 +33,16 @@ coord.xf # 3 x 1 array, [0.1, 0.2, 0.3] the coordinates of multiple particles are stored column-wise: ```jldoctest matter; output=false -coords = Cart([ - 0.651027 0.274176 0.386178 0.371651 0.619131; - 0.0681196 0.0267313 0.836004 0.819681 0.585807; - 0.667704 0.825569 0.780142 0.606194 0.572355 -]) # five particles at uniform random coordinates +coords = Cart( + [ + 0.651027 0.274176 0.386178 0.371651 0.619131 + 0.0681196 0.0267313 0.836004 0.819681 0.585807 + 0.667704 0.825569 0.780142 0.606194 0.572355 + ] +) # five particles at uniform random coordinates + # output + Cart([0.651027 0.274176 … 0.371651 0.619131; 0.0681196 0.0267313 … 0.819681 0.585807; 0.667704 0.825569 … 0.606194 0.572355]) ``` @@ -48,7 +54,9 @@ coords[2:3] # (slicing by index) coords of particles 2 and 3 coords[[1, 2, 5]] # (slicing by index) coords of particles 1, 2, and 5 coords[rand(Bool, 5)] # (boolean slicing) coords, selected at random length(coords) # number of particles, (5) + # output + 5 ``` @@ -57,20 +65,30 @@ length(coords) # number of particles, (5) `Coords` are immutable: ```jldoctest matter -coords.x = rand(3, 5) +try + coords.x = rand(3, 5) +catch ex + @error "Immutable!" +end + # output -ERROR: setfield!: immutable struct of type Cart cannot be changed + +┌ Error: Immutable! +└ @ Main none:4 ``` + but we can manipulate the values of `Array{Float64, 2}` where coordinates (through `coords.x` or `coords.xf`) are stored: ```jldoctest matter; output=false coords.x[2, 3] = 100.0 coords.x[:] = [ # need the [:] to say "overwrite all of the elements" - 0.496997 0.560528 0.496615 0.213062 0.21751; - 0.772372 0.697443 0.133055 0.211073 0.02676; - 0.230555 0.0988727 0.592699 0.193649 0.16536 + 0.496997 0.560528 0.496615 0.213062 0.21751 + 0.772372 0.697443 0.133055 0.211073 0.02676 + 0.230555 0.0988727 0.592699 0.193649 0.16536 ] + # output + 3×5 Matrix{Float64}: 0.496997 0.560528 0.496615 0.213062 0.21751 0.772372 0.697443 0.133055 0.211073 0.02676 @@ -82,7 +100,9 @@ fractional coordinates can be wrapped to be inside the unit cell box: ```jldoctest coords = Frac([1.2, -0.3, 0.9]) wrap!(coords) + # output + 3×1 Matrix{Float64}: 0.19999999999999996 0.7 @@ -93,9 +113,11 @@ we can translate coordinates by a vector `dx`: ```jldoctest dx = Cart([1.0, 2.0, 3.0]) -coords = Cart([1.0, 0.0, 0.0]) +coords = Cart([1.0, 0.0, 0.0]) translate_by!(coords, dx) + # output + 3×1 Matrix{Float64}: 2.0 2.0 @@ -109,7 +131,9 @@ dx = Frac([0.1, 0.2, 0.3]) box = unit_cube() coords = Cart([1.0, 0.0, 0.0]) translate_by!(coords, dx, box) + # output + 3×1 Matrix{Float64}: 1.1 0.20000000000000004 @@ -122,16 +146,19 @@ an atom is specified by its coordinates and atomic species. we can construct a s ```jldoctest matter species = [:O, :H, :H] # atomic species are represnted with Symbols -coords = Cart([0.0 0.757 -0.757; # coordinates of each - 0.0 0.586 0.586; - 0.0 0.0 0.0 ] - ) +coords = Cart([ + 0.0 0.757 -0.757 # coordinates of each + 0.0 0.586 0.586 + 0.0 0.0 0.0 +]) atoms = Atoms(species, coords) # 3 atoms comprising water atoms.n # number of atoms, 3 atoms.coords # coordinates; atoms.coords.x gives the array of coords atoms.species # array of species atoms::Atoms{Cart} # successful type assertion, as opposed to atoms::Atoms{Frac} + # output + Atoms{Cart}(3, [:O, :H, :H], Cart([0.0 0.757 -0.757; 0.0 0.586 0.586; 0.0 0.0 0.0])) ``` @@ -141,7 +168,9 @@ we can slice atoms, such as: ```jldoctest matter atoms[2:3] + # output + Atoms{Cart}(2, [:H, :H], Cart([0.757 -0.757; 0.586 0.586; 0.0 0.0])) ``` @@ -150,7 +179,9 @@ and combine them: ```jldoctest matter atoms_combined = atoms[1] + atoms[2:3] isapprox(atoms, atoms_combined) + # output + true ``` @@ -160,24 +191,30 @@ true ```jldoctest matter q = [-1.0, 0.5, 0.5] # values of point charges, units: electrons -coords = Cart([0.0 0.757 -0.757; # coordinates of the point charges - 0.0 0.586 0.586; - 0.0 0.0 0.0 ] - ) +coords = Cart([ + 0.0 0.757 -0.757 # coordinates of the point charges + 0.0 0.586 0.586 + 0.0 0.0 0.0 +]) charges = Charges(q, coords) # 3 point charges charges.n # number of charges, 3 charges.coords # retreive coords charges.q # retreive q charges::Charges{Cart} # successful type assertion, as opposed to charges::Charges{Frac} + # output + Charges{Cart}(3, [-1.0, 0.5, 0.5], Cart([0.0 0.757 -0.757; 0.0 0.586 0.586; 0.0 0.0 0.0])) ``` we can determine if the set of point charges comprise a charge-neutral system by: + ```jldoctest matter; output=false net_charge(charges) # 0.0 neutral(charges) # true + # output + true ``` diff --git a/docs/src/molecule.md b/docs/src/molecule.md index 84ae7fc6c..afb27af3d 100644 --- a/docs/src/molecule.md +++ b/docs/src/molecule.md @@ -8,11 +8,19 @@ end ## Loading Molecule Files -Molecule input files are stored in `PorousMaterials.PATH_TO_MOLECULES`. Each molecule possesses its own directory containing two files: `charges.csv` and `atoms.csv`, comma-separated-value files, which describe the point charges and Lennard Jones spheres, respectively, that compose the molecule. Only rigid molecules are currently supported. Units of length are in Angstroms ($\AA$); units of charges are electrons. +Molecule input files are stored in the path indicated by the `Xtals`/`PorousMaterials` global dictionary `rc`. + +```julia +rc[:paths][:molecules] # absolute or relative path +``` + +Each molecule possesses its own directory containing two files: `charges.csv` and `atoms.csv`, comma-separated-value files, which describe the point charges and Lennard Jones spheres, respectively, that compose the molecule. Only rigid molecules are currently supported. Units of length are in Angstroms ($\AA$); units of charges are electrons. ```jldoctest molecule molecule = Molecule("CO2") + # output + Molecule species: CO2 Center of mass (fractional coords): Cart([0.0; 0.0; 0.0;;]) Atoms: @@ -34,13 +42,68 @@ molecule.species # molecule species molecule.com # center-of-mass molecule.atoms # Lennard-Jones spheres molecule.charges # point charges + # output + Charges{Cart}(3, [0.7, -0.35, -0.35], Cart([0.0 -1.16 1.16; 0.0 0.0 0.0; 0.0 0.0 0.0])) ``` To see specific information about the atoms and charges attributes of the molecule see [`Atoms`](@ref) and [`Charges`](@ref). +## Pseudo-Atoms + +Sometimes, [e.g. modeling quadrupolar molecules](https://github.com/SimonEnsemble/PorousMaterials.jl/issues/236), it is desirable to add a point charge in a location outside of any atomic nucleus. +In order to do this, a pseudo-atom label must be chosen (consistent with input data) and added to the global list of atomic masses. + +Take the example of using [TraPPE](https://doi.org/10.1002/aic.690470719)-derived charges for the dinitrogen molecule. +Partial negative charges are assigned to the nitrogen atoms and a corresponding positive charge is placed at a pseudo-atom "dummy site" at the center of mass. +The atoms and charges input data for this "N2_TraPPE" molecule are like so: + +```jldoctest molecule +dir = joinpath(rc[:paths][:molecules], "N2_TraPPE") +for file in readdir(dir) + @info file data = String(read(joinpath(dir, file))) +end + +# output + +┌ Info: atoms.csv +└ data = "atom,x,y,z\nN_in_N2,0,0,0.55\nPSEUDOATOM_LABEL,0,0,0\nN_in_N2,0,0,-0.55" +┌ Info: charges.csv +└ data = "q,x,y,z\n-0.482,0,0,0.55\n0.964,0,0,0\n-0.482,0,0,-0.55" +``` + +**atoms.csv** + +``` +atom,x,y,z +N_in_N2,0,0,0.55 +PSEUDOATOM_LABEL,0,0,0 +N_in_N2,0,0,-0.55 +``` + +**charges.csv** + +``` +q,x,y,z +-0.482,0,0,0.55 +0.964,0,0,0 +-0.482,0,0,-0.55 +``` + +In this example, `PSEUDOATOM_LABEL` is the label for the pseudo-atom; there are several common choices, so make sure you know which one is used in your particular data! + +Before loading these data by calling `Molecule`, we need to add our (massless) pseudo-atom to the mass dictionary: + +```juliadoctest +rc[:atomic_masses][:PSEUDOATOM_LABEL] = 0.0 +Molecule("N2_TraPPE") +# ouput +asdf +``` + ## Moving Molecules + We can translate and roatate a molecule: ```julia @@ -56,12 +119,13 @@ dx = Cart([0.1, 0.0, 0.0]) translate_by!(molecule, dx, unit_cube()) # conduct a uniform random rotation about the center-of-mass -random_rotation!(molecule, unit_cube()) +random_rotation!(molecule, unit_cube()) ``` # detailed docs ## Molecules + ```@docs Molecule translate_to! @@ -72,6 +136,7 @@ random_rotation!(molecule, unit_cube()) ``` ## Molecular Movement + ```@docs apply_periodic_boundary_condition! random_insertion! diff --git a/docs/src/paths.md b/docs/src/paths.md index af1e1e723..a2744aede 100644 --- a/docs/src/paths.md +++ b/docs/src/paths.md @@ -11,7 +11,9 @@ variable dictionary `rc` is part of the `PorousMaterials` namespace. This is wh ```jldoctest keys(rc) + # output + KeySet for a Dict{Symbol, Any} with 5 entries. Keys: :covalent_radii :bonding_rules @@ -25,7 +27,9 @@ the present working directory when the module is imported. All the other paths ```jldoctest keys(rc[:paths]) + # output + KeySet for a Dict{Symbol, String} with 6 entries. Keys: :forcefields :grids diff --git a/high_throughput_helpers/collect_results.jl b/high_throughput_helpers/collect_results.jl index e55a0dcd3..79c00baf9 100644 --- a/high_throughput_helpers/collect_results.jl +++ b/high_throughput_helpers/collect_results.jl @@ -1,6 +1,6 @@ using PorousMaterials using JLD - # using PyPlot +# using PyPlot include("jobs_to_run.jl") @@ -14,10 +14,19 @@ for (structure, conditions) in jobs_to_run # store results per adsorption isotherm results = [] for pressure in conditions["pressures"] - - save_results_filename = joinpath(PorousMaterials.PATH_TO_DATA, "gcmc_sims", PorousMaterials.root_save_filename( - structure, Symbol(gas), ljff.name, temperature, pressure, - n_burn_cycles, n_sample_cycles) * ".jld") + save_results_filename = joinpath( + PorousMaterials.PATH_TO_DATA, + "gcmc_sims", + PorousMaterials.root_save_filename( + structure, + Symbol(gas), + ljff.name, + temperature, + pressure, + n_burn_cycles, + n_sample_cycles + ) * ".jld" + ) result = JLD.load(save_results_filename)["results"] push!(results, result) diff --git a/high_throughput_helpers/jobs_to_run.jl b/high_throughput_helpers/jobs_to_run.jl index faccec143..9f03cb3f4 100644 --- a/high_throughput_helpers/jobs_to_run.jl +++ b/high_throughput_helpers/jobs_to_run.jl @@ -1,13 +1,17 @@ using PorousMaterials using JLD -jobs_to_run = Dict("zif71_bogus_charges" => Dict("gases" => ["CO2EPM2"], - "temperatures" => [298.0], - "pressures" => vcat(collect(linspace(0.0, 1.0, 11))[2:end], - collect(linspace(1.0, 20.0, 20))[2:end]), - # this applies to all pressures/temperatures - "forcefield" => LJForceField("Greg_bogus_ZIF71.csv", cutoffradius=12.8), - "n_burn_cycles" => 5, - "n_sample_cycles" => 5 - ), - ) +jobs_to_run = Dict( + "zif71_bogus_charges" => Dict( + "gases" => ["CO2EPM2"], + "temperatures" => [298.0], + "pressures" => vcat( + collect(linspace(0.0, 1.0, 11))[2:end], + collect(linspace(1.0, 20.0, 20))[2:end] + ), + # this applies to all pressures/temperatures + "forcefield" => LJForceField("Greg_bogus_ZIF71.csv"; cutoffradius=12.8), + "n_burn_cycles" => 5, + "n_sample_cycles" => 5 + ) +) diff --git a/high_throughput_helpers/run_gcmc_simulation.jl b/high_throughput_helpers/run_gcmc_simulation.jl index b123665d7..0a2159aad 100644 --- a/high_throughput_helpers/run_gcmc_simulation.jl +++ b/high_throughput_helpers/run_gcmc_simulation.jl @@ -18,13 +18,23 @@ ljff = jobs_to_run[structurename]["forcefield"] n_burn_cycles = jobs_to_run[structurename]["n_burn_cycles"] n_sample_cycles = jobs_to_run[structurename]["n_sample_cycles"] -xtal = Framework(structurename * ".cif") +xtal = Crystal(structurename * ".cif") strip_numbers_from_atom_labels!(xtal) adsorbate = Molecule(gasname) # run the simulation -results = gcmc_simulation(xtal, temperature, pressure, adsorbate, ljff, - n_burn_cycles=n_burn_cycles, n_sample_cycles=n_sample_cycles, - verbose=true, sample_frequency=1, eos=:PengRobinson, autosave=true) +results = muVT_sim( + xtal, + temperature, + pressure, + adsorbate, + ljff; + n_burn_cycles=n_burn_cycles, + n_sample_cycles=n_sample_cycles, + verbose=true, + sample_frequency=1, + eos=:PengRobinson, + autosave=true +) # results dictionary autosaved in data/gcmc_sims diff --git a/high_throughput_helpers/submit_jobs_for_isotherms.jl b/high_throughput_helpers/submit_jobs_for_isotherms.jl index 8df295efe..034fd1d1c 100644 --- a/high_throughput_helpers/submit_jobs_for_isotherms.jl +++ b/high_throughput_helpers/submit_jobs_for_isotherms.jl @@ -2,47 +2,68 @@ Write a job submission script to submit for a μVT simulation of `gas` in `structurename` at `temperature` K and `pressure` bar. """ -function write_gcmc_submit_script(structurename::AbstractString, gas::AbstractString, - temperature::Float64, pressure::Float64) +function write_gcmc_submit_script( + structurename::AbstractString, + gas::AbstractString, + temperature::Float64, + pressure::Float64 +) jobscriptdir = "jobz" - if ~ isdir(jobscriptdir) + if ~isdir(jobscriptdir) mkdir(jobscriptdir) end - @printf("Writing submission script for μVT sim of %s in %s at %f bar and %f K.\n", - gas, structurename, temperature, pressure) + @printf( + "Writing submission script for μVT sim of %s in %s at %f bar and %f K.\n", + gas, + structurename, + temperature, + pressure + ) ## build gcmc_submit.sh script gcmc_submit = open("gcmc_submit.sh", "w") - @printf(gcmc_submit, - """ - #!/bin/bash - - # use current working directory for input and output - # default is to use the users home directory - #\$ -cwd - - # name this job - #\$ -N %s - - # send stdout and stderror to this file - #\$ -o %s.o - #\$ -e %s.e - - ## select queue - if needed; mime5 is SimonEnsemble priority queue but is restrictive. - ##\$ -q mime5 - - # print date and time - date + @printf( + gcmc_submit, + """ + #!/bin/bash + + # use current working directory for input and output + # default is to use the users home directory + #\$ -cwd + + # name this job + #\$ -N %s + + # send stdout and stderror to this file + #\$ -o %s.o + #\$ -e %s.e + + ## select queue - if needed; mime5 is SimonEnsemble priority queue but is restrictive. + ##\$ -q mime5 + ##\$ -A simongrp + + # print date and time + date - julia run_gcmc_simulation.jl %s %s %f %f - """, - structurename, - joinpath(jobscriptdir, structurename * "_" * string(temperature) * "_" * string(pressure)), - joinpath(jobscriptdir, structurename * "_" * string(temperature) * "_" * string(pressure)), - structurename, gas, temperature, pressure) + julia run_gcmc_simulation.jl %s %s %f %f + """, + structurename, + joinpath( + jobscriptdir, + structurename * "_" * string(temperature) * "_" * string(pressure) + ), + joinpath( + jobscriptdir, + structurename * "_" * string(temperature) * "_" * string(pressure) + ), + structurename, + gas, + temperature, + pressure + ) - close(gcmc_submit) + return close(gcmc_submit) end # import list of materials diff --git a/PMlogo.png b/logo.png old mode 100755 new mode 100644 similarity index 100% rename from PMlogo.png rename to logo.png diff --git a/src/PorousMaterials.jl b/src/PorousMaterials.jl index 888c1607d..294623e0c 100644 --- a/src/PorousMaterials.jl +++ b/src/PorousMaterials.jl @@ -1,12 +1,26 @@ module PorousMaterials -using CSV, DataFrames, Distributed, FIGlet, Graphs, JLD2, LinearAlgebra, OffsetArrays, Optim, - Polynomials, Printf, ProgressMeter, Roots, SpecialFunctions, Statistics, StatsBase +using CSV, + DataFrames, + Distributed, + Graphs, + JLD2, + LinearAlgebra, + OffsetArrays, + Optim, + Polynomials, + Printf, + ProgressMeter, + Roots, + SpecialFunctions, + Statistics, + StatsBase # extend Xtals using Reexport @reexport using Xtals import Xtals.Cart, Xtals.Frac, Xtals.write_xyz +using PrecompileSignatures: @precompile_signatures # physical constants const UNIV_GAS_CONST = 8.3144598e-5 # m³-bar/(K-mol) @@ -26,62 +40,106 @@ include("henry.jl") include("gcmc.jl") include("energy_min.jl") include("atomic_masses.jl") +include("banner.jl") function __init__() rc[:paths][:forcefields] = "" rc[:paths][:molecules] = "" rc[:paths][:grids] = "" rc[:paths][:simulations] = "" - set_paths(joinpath(pwd(), "data"), no_warn=true) - append_atomic_masses() -end - -function banner() - font_num = 579 - FIGlet.render("Porous", FIGlet.availablefonts()[font_num]) - FIGlet.render("Materials", FIGlet.availablefonts()[font_num]) + set_paths(joinpath(pwd(), "data"); no_warn=true) + return append_atomic_masses() end +# overload unicode names w/ pure-ASCII forms +muVT_sim = μVT_sim +muVT_output_filename = μVT_output_filename export # isotherm_fitting.jl fit_adsorption_isotherm, # molecule.jl - Molecule, translate_by!, translate_to!, random_rotation!, random_rotation_matrix, ion, distortion, + Molecule, + translate_by!, + translate_to!, + random_rotation!, + random_rotation_matrix, + ion, + distortion, # forcefield.jl - LJForceField, replication_factors, forcefield_coverage, + LJForceField, + replication_factors, + forcefield_coverage, # energy_utilities.jl - PotentialEnergy, SystemPotentialEnergy, + PotentialEnergy, + SystemPotentialEnergy, # vdw_energetics.jl - lennard_jones, vdw_energy, vdw_energy_no_PBC, + lennard_jones, + vdw_energy, + vdw_energy_no_PBC, # electrostatics.jl - electrostatic_potential_energy, precompute_kvec_wts, - setup_Ewald_sum, total, Eikr, total_electrostatic_potential_energy, + electrostatic_potential_energy, + precompute_kvec_wts, + setup_Ewald_sum, + total, + Eikr, + total_electrostatic_potential_energy, # mc_helpers.jl - random_insertion!, remove_molecule!, random_translation!, random_reinsertion!, needs_rotations, + random_insertion!, + remove_molecule!, + random_translation!, + random_reinsertion!, + needs_rotations, AdaptiveTranslationStepSize, # Grid.jl apply_periodic_boundary_condition!, - Grid, write_cube, read_cube, energy_grid, compute_accessibility_grid, accessible, - required_n_pts, xf_to_id, id_to_xf, update_density!, find_energy_minimum, origin, - + Grid, + write_cube, + read_cube, + energy_grid, + compute_accessibility_grid, + accessible, + required_n_pts, + xf_to_id, + id_to_xf, + update_density!, + find_energy_minimum, + origin, + # EOS.jl - calculate_properties, PengRobinsonFluid, VdWFluid, + calculate_properties, + PengRobinsonFluid, + VdWFluid, # gcmc.jl - μVT_sim, adsorption_isotherm, stepwise_adsorption_isotherm, - μVT_output_filename, GCMCstats, MarkovCounts, isotherm_sim_results_to_dataframe, - + μVT_sim, + muVT_sim, + adsorption_isotherm, + stepwise_adsorption_isotherm, + μVT_output_filename, + muVT_output_filename, + GCMCstats, + MarkovCounts, + isotherm_sim_results_to_dataframe, + # henry.jl - henry_coefficient, henry_result_savename, + henry_coefficient, + henry_result_savename, # energy_min.jl - find_energy_minimum, find_energy_minimum_gridsearch + find_energy_minimum, + find_energy_minimum_gridsearch, + + # nvt.jl + NVT_sim + +@precompile_signatures(PorousMaterials) + end diff --git a/src/atomic_masses.jl b/src/atomic_masses.jl index 516cc2da0..92f6af5da 100644 --- a/src/atomic_masses.jl +++ b/src/atomic_masses.jl @@ -1,25 +1,28 @@ # add atom types to rc[:atomic_masses] for contextual LJ potentials function append_atomic_masses() - rc[:atomic_masses] = merge(rc[:atomic_masses], Dict( - :N_in_N2 => 14.0067, - :CH2 => 14.025, - :CH3 => 15.035, - :CH4 => 16.04, - :C_b => 12.0107, - :C_tol => 12.0107, - :C_ac => 12.0107, - :C_RCOO => 12.0107, - :C_sp2 => 12.0107, - :C_sp3 => 12.0107, - :C_CO2 => 12.0107, - :H_H2S => 1.00794, - :H_b => 1.00794, - :O_RCOO => 15.9994, - :O_CO2 => 15.9994, - :O_zeo => 15.9994, - :S_H2S => 32.065, - :Si_zeo => 28.0855, - :ig => 1.0, - :X => 1.0 - )) + return rc[:atomic_masses] = merge( + rc[:atomic_masses], + Dict( + :N_in_N2 => 14.0067, + :CH2 => 14.025, + :CH3 => 15.035, + :CH4 => 16.04, + :C_b => 12.0107, + :C_tol => 12.0107, + :C_ac => 12.0107, + :C_RCOO => 12.0107, + :C_sp2 => 12.0107, + :C_sp3 => 12.0107, + :C_CO2 => 12.0107, + :H_H2S => 1.00794, + :H_b => 1.00794, + :O_RCOO => 15.9994, + :O_CO2 => 15.9994, + :O_zeo => 15.9994, + :S_H2S => 32.065, + :Si_zeo => 28.0855, + :ig => 1.0, + :X => 1.0 + ) + ) end diff --git a/src/banner.jl b/src/banner.jl new file mode 100644 index 000000000..bef1f002f --- /dev/null +++ b/src/banner.jl @@ -0,0 +1,17 @@ +const BANNER = raw""" +________ +___ __ \______________________ _________ +__ /_/ / __ \_ ___/ __ \ / / /_ ___/ +_ ____// /_/ / / / /_/ / /_/ /_(__ ) +/_/ \____//_/ \____/\__,_/ /____/ + + +______ ___ _____ _____ ______ +___ |/ /_____ __ /_______________(_)_____ ___ /_______ +__ /|_/ /_ __ `/ __/ _ \_ ___/_ /_ __ `/_ /__ ___/ +_ / / / / /_/ // /_ / __/ / _ / / /_/ /_ / _(__ ) +/_/ /_/ \__,_/ \__/ \___//_/ /_/ \__,_/ /_/ /____/ + +""" + +banner() = print(BANNER) diff --git a/src/electrostatics.jl b/src/electrostatics.jl index f24b7cba5..7924edc13 100644 --- a/src/electrostatics.jl +++ b/src/electrostatics.jl @@ -3,7 +3,7 @@ import Base: +, * # Vacuum permittivity eps0 = 8.854187817e-12 # C^2/(J-m) # 1 m = 1e10 A, 1 e = 1.602176e-19 C, kb = 1.3806488e-23 J/K # 8.854187817e-12 C^2/(J-m) [1 m / 1e10 A] [1 e / 1.602176e-19 C]^2 [kb = 1.3806488e-23 J/K] -const ϵ₀ = 8.854187817e-12 / (1.602176565e-19 ^ 2) / 1.0e10 * 1.3806488e-23 # \epsilon_0 vacuum permittivity units: electron charge^2 /(A - K) +const ϵ₀ = 8.854187817e-12 / (1.602176565e-19^2) / 1.0e10 * 1.3806488e-23 # \epsilon_0 vacuum permittivity units: electron charge^2 /(A - K) const FOUR_π_ϵ₀ = 4.0 * π * ϵ₀ mutable struct EwaldSum @@ -22,17 +22,27 @@ mutable struct GGEwaldSum # for guest-guest interactions self::Float64 # spurious self-interaction intra::Float64 # intramolecular interactions end -total(ews::GGEwaldSum) = ews.sr + ews.lr_excluding_own_images + ews.lr_own_images + ews.self + ews.intra -+(e1::GGEwaldSum, e2::GGEwaldSum) = GGEwaldSum(e1.sr + e2.sr, - e1.lr_excluding_own_images + e2.lr_excluding_own_images, - e1.lr_own_images + e2.lr_own_images, - e1.self + e2.self, - e1.intra + e2.intra) -*(a::Float64, ews::GGEwaldSum) = GGEwaldSum(a * ews.sr, - a * ews.lr_excluding_own_images, - a * ews.lr_own_images, - a * ews.self, - a * ews.intra) +function total(ews::GGEwaldSum) + return ews.sr + ews.lr_excluding_own_images + ews.lr_own_images + ews.self + ews.intra +end +function +(e1::GGEwaldSum, e2::GGEwaldSum) + return GGEwaldSum( + e1.sr + e2.sr, + e1.lr_excluding_own_images + e2.lr_excluding_own_images, + e1.lr_own_images + e2.lr_own_images, + e1.self + e2.self, + e1.intra + e2.intra + ) +end +function *(a::Float64, ews::GGEwaldSum) + return GGEwaldSum( + a * ews.sr, + a * ews.lr_excluding_own_images, + a * ews.lr_own_images, + a * ews.self, + a * ews.intra + ) +end GGEwaldSum() = GGEwaldSum(0.0, 0.0, 0.0, 0.0, 0.0) """ @@ -50,7 +60,9 @@ struct Kvector wt::Float64 end -"Data structure for storing Ewald summation settings" +""" +Data structure for storing Ewald summation settings +""" struct EwaldParams "number of replications in k-space in a, b, c directions" kreps::Tuple{Int, Int, Int} @@ -64,7 +76,13 @@ end function Base.print(io::IO, eparams::EwaldParams) @printf(io, "\tEwald summation parameters:\n") - @printf(io, "\t\tk-replication factors: %d %d %d\n", eparams.kreps[1], eparams.kreps[2], eparams.kreps[3]) + @printf( + io, + "\t\tk-replication factors: %d %d %d\n", + eparams.kreps[1], + eparams.kreps[2], + eparams.kreps[3] + ) @printf(io, "\t\tEwald convergence param. α = %f\n", eparams.α) @printf(io, "\t\tshort-range cutoff radius (Å): %f\n", eparams.sr_cutoff_r) @printf(io, "\t\t%d kvectors\n", length(eparams.kvecs)) @@ -80,11 +98,12 @@ end mutable struct for holding the eikr vectors # Attributes -- `eikar::OffsetArray{Complex{Float64}}`: array for storing e^{i * ka ⋅ r}; has indices + + - `eikar::OffsetArray{Complex{Float64}}`: array for storing e^{i * ka ⋅ r}; has indices 0:kreps[1] and corresponds to recip. vectors in a-direction -- `eikbr::OffsetArray{Complex{Float64}}`: array for storing e^{i * kb ⋅ r}; has indices + - `eikbr::OffsetArray{Complex{Float64}}`: array for storing e^{i * kb ⋅ r}; has indices -kreps[2]:kreps[2] and corresponds to recip. vectors in b-direction -- `eikcr::OffsetArray{Complex{Float64}}`: array for storing e^{i * kc ⋅ r}; has indices + - `eikcr::OffsetArray{Complex{Float64}}`: array for storing e^{i * kc ⋅ r}; has indices -kreps[2]:kreps[1] and corresponds to recip. vectors in c-direction """ mutable struct Eikr @@ -92,11 +111,13 @@ mutable struct Eikr eikbr::OffsetArray{Complex{Float64}, 2, Array{Complex{Float64}, 2}} eikcr::OffsetArray{Complex{Float64}, 2, Array{Complex{Float64}, 2}} end -Eikr(n_charges::Int, kreps::Tuple{Int, Int, Int}) = Eikr( +function Eikr(n_charges::Int, kreps::Tuple{Int, Int, Int}) + return Eikr( OffsetArray{Complex{Float64}}(undef, 1:n_charges, 0:kreps[1]), # remove negative kreps[1] and take advantage of symmetry - OffsetArray{Complex{Float64}}(undef, 1:n_charges, -kreps[2]:kreps[2]), - OffsetArray{Complex{Float64}}(undef, 1:n_charges, -kreps[3]:kreps[3]) + OffsetArray{Complex{Float64}}(undef, 1:n_charges, (-kreps[2]):kreps[2]), + OffsetArray{Complex{Float64}}(undef, 1:n_charges, (-kreps[3]):kreps[3]) ) +end Eikr(crystal::Crystal, eparams::EwaldParams) = Eikr(crystal.charges.n, eparams.kreps) Eikr(molecule::Molecule, eparams::EwaldParams) = Eikr(molecule.charges.n, eparams.kreps) @@ -107,18 +128,20 @@ Compute the Ewald summation convergence parameter α and the cutoff value for |k reciprocal space. # Arguments -- `sr_cutoff_r::Float64`: cutoff-radius (units: Å) for short-range contributions to Ewald -sum. This must be consistent with the number of replication factors used to apply the -nearest image convention, so typically this is chosen to be the same as for short-range -van-der Waals interactions. -- `ϵ::Float64`: desired level of precision. Typical value is 1e-6, but this does not -guarentee this precision technically since that depends on the charges in the system, but -it is very helpful to think of this as the weight on contributions near the edge of the -short-range cutoff radius or max |k|². + + - `sr_cutoff_r::Float64`: cutoff-radius (units: Å) for short-range contributions to Ewald + sum. This must be consistent with the number of replication factors used to apply the + nearest image convention, so typically this is chosen to be the same as for short-range + van-der Waals interactions. + - `ϵ::Float64`: desired level of precision. Typical value is 1e-6, but this does not + guarentee this precision technically since that depends on the charges in the system, but + it is very helpful to think of this as the weight on contributions near the edge of the + short-range cutoff radius or max |k|². # Returns -- `α::Float64`: Ewald sum convergence parameter (units: inverse Å) -- `max_mag_k_sqrd::Float64`: cutoff for |k|², where k is a k-vector, in the Fourier sum. + + - `α::Float64`: Ewald sum convergence parameter (units: inverse Å) + - `max_mag_k_sqrd::Float64`: cutoff for |k|², where k is a k-vector, in the Fourier sum. """ function determine_ewald_params(sr_cutoff_r::Float64, ϵ::Float64) # α is Ewald sum convergence parameter. It determines how fast the long- and short- @@ -135,7 +158,7 @@ function determine_ewald_params(sr_cutoff_r::Float64, ϵ::Float64) α = fzero(sr_error, 0.0, 25.0) # 25.0 should be safe bracket. # solve for the max squre norm of the k-vectors required to allow long-range # interactions in Fourier space to decay beyond for k-vectors longer than this. - lr_error(mag_k_sqrd) = 2 * exp(- mag_k_sqrd / (4.0 * α ^ 2)) / mag_k_sqrd - ϵ + lr_error(mag_k_sqrd) = 2 * exp(-mag_k_sqrd / (4.0 * α^2)) / mag_k_sqrd - ϵ max_mag_k_sqrd = fzero(lr_error, 0.0, 100.0) return α, max_mag_k_sqrd end @@ -147,17 +170,19 @@ Determine replications of k-vectors in Fourier sum, `kreps`, a Tuple of Ints, re assert all k-vectors with square magnitude less than |k|² (`max_mag_k_sqrd`) are included. # Arguments -- `box::Box`: the simulation box containing the reciprocal lattice. -- `max_mag_k_sqrd::Float64`: cutoff value for |k|² in reciprocal space sum. + + - `box::Box`: the simulation box containing the reciprocal lattice. + - `max_mag_k_sqrd::Float64`: cutoff value for |k|² in reciprocal space sum. # Returns -- `kreps::Tuple{Int, Int, Int}`: number of k-vector replications required in a, b, c -directions. + + - `kreps::Tuple{Int, Int, Int}`: number of k-vector replications required in a, b, c + directions. """ function required_kreps(box::Box, max_mag_k_sqrd::Float64) # fill in later kreps = [0, 0, 0] - for abc = 1:3 + for abc in 1:3 n = [0, 0, 0] while true n[abc] += 1 @@ -179,29 +204,35 @@ end For speed, pre-compute the weights for each reciprocal lattice vector for the Ewald sum in Fourier space. This function takes advantage of the symmetry: - cos(-k⋅(x-xᵢ)) + cos(k⋅(x-xᵢ)) = 2 cos(k⋅(x-xᵢ)) +cos(-k⋅(x-xᵢ)) + cos(k⋅(x-xᵢ)) = 2 cos(k⋅(x-xᵢ)) If `max_mag_k_sqrd` is passed, k-vectors with a magnitude greater than `max_mag_k_sqrd` are not included. # Arguments -- `kreps::Tuple{Int, Int, Int}`: number of k-vector replications required in a, b, c -- `box::Box`: the simulation box containing the reciprocal lattice. -- `α::Float64`: Ewald sum convergence parameter (units: inverse Å) -- `max_mag_k_sqrd::Float64`: cutoff for |k|² in Fourier sum; if passed, do not include -k-vectors with magnitude squared greater than this. + + - `kreps::Tuple{Int, Int, Int}`: number of k-vector replications required in a, b, c + - `box::Box`: the simulation box containing the reciprocal lattice. + - `α::Float64`: Ewald sum convergence parameter (units: inverse Å) + - `max_mag_k_sqrd::Float64`: cutoff for |k|² in Fourier sum; if passed, do not include + k-vectors with magnitude squared greater than this. # Returns -- `kvectors::Array{Kvector, 1}`: array of k-vectors to include in the Fourier sum and their -corresponding weights indicating the contribution to the Fourier sum. + + - `kvectors::Array{Kvector, 1}`: array of k-vectors to include in the Fourier sum and their + corresponding weights indicating the contribution to the Fourier sum. """ -function precompute_kvec_wts(kreps::Tuple{Int, Int, Int}, box::Box, α::Float64, - max_mag_k_sqrd::Float64=Inf) +function precompute_kvec_wts( + kreps::Tuple{Int, Int, Int}, + box::Box, + α::Float64, + max_mag_k_sqrd::Float64=Inf +) kvecs = Kvector[] # take advantage of symmetry. cos(k ⋅ dx) = cos( (-k) ⋅ dx) # don't include both [ka kb kc] [-ka -kb -kc] for all kb, kc # hence ka goes from 0:k_repfactors[1] - for ka = 0:kreps[1], kb = -kreps[2]:kreps[2], kc=-kreps[3]:kreps[3] + for ka in 0:kreps[1], kb in (-kreps[2]):kreps[2], kc in (-kreps[3]):kreps[3] # don't include the home unit cell if (ka == 0) && (kb == 0) && (kc == 0) continue @@ -224,7 +255,7 @@ function precompute_kvec_wts(kreps::Tuple{Int, Int, Int}, box::Box, α::Float64, # factor of 2 from cos(-k⋅(x-xᵢ)) + cos(k⋅(x-xᵢ)) = 2 cos(k⋅(x-xᵢ)) # and how we include ka>=0 only and the two if statements above if mag_k_sqrd < max_mag_k_sqrd - wt = 2.0 * exp(-mag_k_sqrd / (4.0 * α ^ 2)) / mag_k_sqrd / ϵ₀ / box.Ω + wt = 2.0 * exp(-mag_k_sqrd / (4.0 * α^2)) / mag_k_sqrd / ϵ₀ / box.Ω push!(kvecs, Kvector(ka, kb, kc, wt)) end end @@ -244,17 +275,23 @@ Also, allocate OffsetArrays for storing e^{i * k ⋅ r} where r = x - xⱼ and k lattice vector. # Arguments -- `box::Box`: the simulation box containing the reciprocal lattice. -- `sr_cutoff_r::Float64`: cutoff-radius (units: Å) for short-range contributions to Ewald -- `ϵ::Float64`: desired level of precision. Typical value is 1e-6, but this does not -- `verbose::Bool`: If `true` will print results + + - `box::Box`: the simulation box containing the reciprocal lattice. + - `sr_cutoff_r::Float64`: cutoff-radius (units: Å) for short-range contributions to Ewald + - `ϵ::Float64`: desired level of precision. Typical value is 1e-6, but this does not + - `verbose::Bool`: If `true` will print results # Returns -- `eparams::EwaldParams`: data structure containing Ewald summation settings -corresponding weights indicating the contribution to the Fourier sum. + + - `eparams::EwaldParams`: data structure containing Ewald summation settings + corresponding weights indicating the contribution to the Fourier sum. """ -function setup_Ewald_sum(box::Box, sr_cutoff_r::Float64; - ϵ::Float64=1e-6, verbose::Bool=false) +function setup_Ewald_sum( + box::Box, + sr_cutoff_r::Float64; + ϵ::Float64=1e-6, + verbose::Bool=false +) # determine Ewald convergence parameter and cutoff for |k|² for sum in Fourier space α, max_mag_k_sqrd = determine_ewald_params(sr_cutoff_r, ϵ) # determine kreps required to assert all k-vectors with magnitude less than |k|² are included. @@ -280,25 +317,30 @@ Given k ⋅ r, where r = x - xⱼ, compute e^{i n k ⋅ r} for n = 0:krep to fil columns of eikr correspond to different k's; rows are charges # Arguments -- `eikr::OffsetArray{Complex{Float64}, 2, Array{Complex{Float64}, 2}}: An offset array containing charges, then kreps, then abc (direction) -- `k_dot_dc::Array{Float64, 1}`: k ⋅ r, where r = x - xⱼ -- `krep::Int`: number of k-vector replications required in a, b or c -- `include_neg_reps::Bool`: If `true` will include negative replications + + - `eikr::OffsetArray{Complex{Float64}, 2, Array{Complex{Float64}, 2}}: An offset array containing charges, then kreps, then abc (direction) + - `k_dot_dc::Array{Float64, 1}`: k ⋅ r, where r = x - xⱼ + - `krep::Int`: number of k-vector replications required in a, b or c + - `include_neg_reps::Bool`: If `true` will include negative replications """ -@inline function fill_eikr!(eikr::OffsetArray{Complex{Float64}, 2, Array{Complex{Float64}, 2}}, - k_dot_dx::Array{Float64, 1}, krep::Int, incl_neg_reps::Bool) +@inline function fill_eikr!( + eikr::OffsetArray{Complex{Float64}, 2, Array{Complex{Float64}, 2}}, + k_dot_dx::Array{Float64, 1}, + krep::Int, + incl_neg_reps::Bool +) # explicitly compute for k = 1, k = 0 @inbounds eikr[:, 0] .= exp(0.0 * im) @inbounds eikr[:, 1] .= exp.(im * k_dot_dx) # recursion relation for higher frequencies to avoid expensive computing of cosine. # e^{3 * i * k_dot_r} = e^{2 * i * k_dot_r} * e^{ i * k_dot_r} - for k = 2:krep + for k in 2:krep @inbounds eikr[:, k] .= eikr[:, k - 1] .* eikr[:, 1] end # negative kreps are complex conjugate of positive ones. # e^{2 * i * k_dot_r} = conj(e^{-2 * i * k_dot_dr}) if incl_neg_reps - for k = -krep:-1 + for k in (-krep):-1 @inbounds eikr[:, k] .= conj.(eikr[:, -k]) end end @@ -307,7 +349,6 @@ end """ ϕ = electrostatic_potential_energy(crystal, molecule, eparams, eikr) - Compute the electrostatic potential energy of a molecule inside a crystal. The electrostatic potential is created by the point charges assigned to the crystal @@ -320,23 +361,29 @@ image convention can be applied for the short-range cutoff radius supplied in `eparams.sr_cutoff_r`. # Arguments -- `crystal::Crystal`: Crystal structure (see `crystal.charges` for charges) -- `molecule::Molecule`: The molecule being compared to the atoms in the crystal. -- `eparams::EwaldParams`: data structure containing Ewald summation settings -- `eikr::Eikr`: Stores the eikar, eikbr, and eikcr OffsetArrays used in this calculation. + + - `crystal::Crystal`: Crystal structure (see `crystal.charges` for charges) + - `molecule::Molecule`: The molecule being compared to the atoms in the crystal. + - `eparams::EwaldParams`: data structure containing Ewald summation settings + - `eikr::Eikr`: Stores the eikar, eikbr, and eikcr OffsetArrays used in this calculation. # Returns -- `pot::EwaldSum`: Electrostatic potential between `crystal` and `molecule` (units: K) + + - `pot::EwaldSum`: Electrostatic potential between `crystal` and `molecule` (units: K) """ -function electrostatic_potential_energy(crystal::Crystal, - molecule::Molecule{Frac}, - eparams::EwaldParams, - eikr::Eikr) +function electrostatic_potential_energy( + crystal::Crystal, + molecule::Molecule{Frac}, + eparams::EwaldParams, + eikr::Eikr +) pot = EwaldSum() # loop over charges of the molecule - for c = 1:molecule.charges.n + for c in 1:(molecule.charges.n) # compute electrostatic potential generated by crystal at molecule's charge location - @inbounds pot += molecule.charges.q[c] * _ϕ(crystal.charges, molecule.charges.coords[c], eparams, crystal.box, eikr) + @inbounds pot += + molecule.charges.q[c] * + _ϕ(crystal.charges, molecule.charges.coords[c], eparams, crystal.box, eikr) end return pot::EwaldSum end @@ -344,7 +391,13 @@ end """ Electrostatic potential at point x due to charges """ -@inline function _ϕ(charges::Charges{Frac}, xf::Frac, eparams::EwaldParams, box::Box, eikr::Eikr) +@inline function _ϕ( + charges::Charges{Frac}, + xf::Frac, + eparams::EwaldParams, + box::Box, + eikr::Eikr +) pot = EwaldSum() @inbounds dxf = broadcast(-, charges.coords.xf, xf.xf) @@ -365,8 +418,13 @@ Electrostatic potential at point x due to charges # e^{i kb r * vec(kb)} * # e^{i kb r * vec(kc)} where r = x - xᵢ # and eikar[ka], eikbr[kb], eikcr[kc] contain exactly the above components. - @simd for c = 1:charges.n - @inbounds kvec_pot += charges.q[c] * real(eikr.eikar[c, kvec.ka] * eikr.eikbr[c, kvec.kb] * eikr.eikcr[c, kvec.kc]) + @simd for c in 1:(charges.n) + @inbounds kvec_pot += + charges.q[c] * real( + eikr.eikar[c, kvec.ka] * + eikr.eikbr[c, kvec.kb] * + eikr.eikcr[c, kvec.kc] + ) end pot.lr += kvec.wt * kvec_pot end @@ -378,7 +436,7 @@ Electrostatic potential at point x due to charges dxf .= box.f_to_c * dxf dxf .= dxf .^ 2 # convert to Cartesian coords - for c = 1:charges.n + for c in 1:(charges.n) @fastmath @inbounds r = sqrt(dxf[1, c] + dxf[2, c] + dxf[3, c]) if r < eparams.sr_cutoff_r @inbounds @fastmath pot.sr += charges.q[c] / r * erfc(r * eparams.α) / FOUR_π_ϵ₀ @@ -396,13 +454,14 @@ end # allows molecule to be split accross periodic boundary by applying nearest image convention function _intramolecular_energy(molecule::Molecule{Frac}, eparams::EwaldParams, box::Box) ϕ_intra = 0.0 - for i = 1:molecule.charges.n - for j = (i + 1):molecule.charges.n + for i in 1:(molecule.charges.n) + for j in (i + 1):(molecule.charges.n) dxf = molecule.charges.coords.xf[:, i] - molecule.charges.coords.xf[:, j] nearest_image!(dxf) dx = box.f_to_c * dxf r = sqrt(dx[1] * dx[1] + dx[2] * dx[2] + dx[3] * dx[3]) - ϕ_intra -= molecule.charges.q[i] * molecule.charges.q[j] * erf(eparams.α * r) / r + ϕ_intra -= + molecule.charges.q[i] * molecule.charges.q[j] * erf(eparams.α * r) / r end end return ϕ_intra / FOUR_π_ϵ₀ @@ -423,24 +482,35 @@ sum as well as the spurious self-interaction and intramolecular interactions. Ac Units of energy: Kelvin # Arguments -- `molecules::Array{Molecules, 1}`: array of molecules comprising the system. -- `eparams::EwaldParams`: data structure containing Ewald summation settings -- `box::Box`: the box the energy is being computed in -- `eikr::Eikr`: Stores the eikar, eikbr, and eikcr OffsetArrays used in this calculation. + + - `molecules::Array{Molecules, 1}`: array of molecules comprising the system. + - `eparams::EwaldParams`: data structure containing Ewald summation settings + - `box::Box`: the box the energy is being computed in + - `eikr::Eikr`: Stores the eikar, eikbr, and eikcr OffsetArrays used in this calculation. # Returns -- `ϕ::GGEwaldSum`: The total electrostatic potential energy + + - `ϕ::GGEwaldSum`: The total electrostatic potential energy """ -function electrostatic_potential_energy(molecules::Array{Molecule{Frac}, 1}, - eparams::EwaldParams, box::Box, - eikr::Eikr) +function electrostatic_potential_energy( + molecules::Array{Molecule{Frac}, 1}, + eparams::EwaldParams, + box::Box, + eikr::Eikr +) ϕ = GGEwaldSum() - for i = eachindex(molecules) - for j = eachindex(molecules) - for c = 1:molecules[j].charges.n + for i in eachindex(molecules) + for j in eachindex(molecules) + for c in 1:(molecules[j].charges.n) # view here is that charges in molecules[i] create a potential field for charges in molecules[j] - ϕ_with_i = molecules[j].charges.q[c] * _ϕ(molecules[i].charges, - molecules[j].charges.coords[c], eparams, box, eikr) + ϕ_with_i = + molecules[j].charges.q[c] * _ϕ( + molecules[i].charges, + molecules[j].charges.coords[c], + eparams, + box, + eikr + ) ### # Long-range contribution ### @@ -481,19 +551,26 @@ end # for use in MC simulations # V(entire system (incl. molecule i)) - V(entire system (excl. molecule i)) -function electrostatic_potential_energy(molecules::Array{Molecule{Frac}, 1}, - molecule_id::Int, - eparams::EwaldParams, - box::Box, - eikr::Eikr) +function electrostatic_potential_energy( + molecules::Array{Molecule{Frac}, 1}, + molecule_id::Int, + eparams::EwaldParams, + box::Box, + eikr::Eikr +) ϕ = GGEwaldSum() - for c = 1:molecules[molecule_id].charges.n - for other_molecule_id = eachindex(molecules) + for c in 1:(molecules[molecule_id].charges.n) + for other_molecule_id in eachindex(molecules) # view here is the molecules[other_molecule_id] creates potential field for # charge cof molecules[molecule_id] - ϕ_with_other_molecule_id = molecules[molecule_id].charges.q[c] * _ϕ( - molecules[other_molecule_id].charges, molecules[molecule_id].charges.coords[c], - eparams, box, eikr) + ϕ_with_other_molecule_id = + molecules[molecule_id].charges.q[c] * _ϕ( + molecules[other_molecule_id].charges, + molecules[molecule_id].charges.coords[c], + eparams, + box, + eikr + ) if other_molecule_id != molecule_id ϕ.sr += ϕ_with_other_molecule_id.sr @@ -516,25 +593,28 @@ end """ total_ϕ = total_electrostatic_potential_energy(molecules, eparams, box, eikr) - Calculates the total electrostatic potential energy of an array of `Molecule`s using a Grand Canonical Monte Carlo (GCMC) algorithm. #TODO add to this # Arguments -- `molecules::Array{Molecule, 1}`: The molecules comprising the system. -- `eparams::EwaldParams`: data structure containing Ewald summation settings -- `box::Box`: The box the energy is being computed in. -- `eikr::Eikr`: Stores the eikar, eikbr, and eikcr OffsetArrays used in this calculation. + + - `molecules::Array{Molecule, 1}`: The molecules comprising the system. + - `eparams::EwaldParams`: data structure containing Ewald summation settings + - `box::Box`: The box the energy is being computed in. + - `eikr::Eikr`: Stores the eikar, eikbr, and eikcr OffsetArrays used in this calculation. # Returns -- `ϕ::GGEwaldSum`: The total electrostatic potential energy + + - `ϕ::GGEwaldSum`: The total electrostatic potential energy """ -function total_electrostatic_potential_energy(molecules::Array{Molecule{Frac}, 1}, - eparams::EwaldParams, - box::Box, - eikr::Eikr) +function total_electrostatic_potential_energy( + molecules::Array{Molecule{Frac}, 1}, + eparams::EwaldParams, + box::Box, + eikr::Eikr +) ϕ = GGEwaldSum() - for i = eachindex(molecules) + for i in eachindex(molecules) ϕ += electrostatic_potential_energy(molecules, i, eparams, box, eikr) end ϕ.sr /= 2.0 # avoid double-counting @@ -546,23 +626,24 @@ end """ total_ϕ = total_electrostatic_potential_energy(crystal, molecules, eparams, eikr) - Explanation of total_electrostatic_potential_energy that uses crystal # Arguments -- `crystal::Crystal`: Crystal structure (see `crystal.charges` for charges) -- `molecules::Array{Molecule, 1}`: The molecules comprising the system. -- `eparams::EwaldParams`: data structure containing Ewald summation settings -- `eikr::Eikr`: Stores the eikar, eikbr, and eikcr OffsetArrays used in this calculation. + + - `crystal::Crystal`: Crystal structure (see `crystal.charges` for charges) + - `molecules::Array{Molecule, 1}`: The molecules comprising the system. + - `eparams::EwaldParams`: data structure containing Ewald summation settings + - `eikr::Eikr`: Stores the eikar, eikbr, and eikcr OffsetArrays used in this calculation. """ -function total_electrostatic_potential_energy(crystal::Crystal, - molecules::Array{Molecule{Frac}, 1}, - eparams::EwaldParams, - eikr::Eikr) +function total_electrostatic_potential_energy( + crystal::Crystal, + molecules::Array{Molecule{Frac}, 1}, + eparams::EwaldParams, + eikr::Eikr +) ϕ = EwaldSum() for molecule in molecules - ϕ += electrostatic_potential_energy(crystal, molecule, eparams, - eikr) + ϕ += electrostatic_potential_energy(crystal, molecule, eparams, eikr) end return ϕ::EwaldSum end diff --git a/src/energy_min.jl b/src/energy_min.jl index a9cbe6535..85d93ba74 100644 --- a/src/energy_min.jl +++ b/src/energy_min.jl @@ -3,41 +3,40 @@ find the minimum energy position, and associated minimum energy, of a molecule in a crystal. n.b. if molecule has more than one atom, it will *not* minimize over the orientation (rotations). -the optimizer needs an initial estimate of the minimum energy position. +the optimizer needs an initial estimate of the minimum energy position. pass molecule with good initial position. if you don't have a good initial position, use [`find_energy_minimum_gridsearch`](@ref). # Arguments -- `xtal::Crystal`: the crystal -- `molecule::Molecule`: the molecule, whose position we seek to tune until we reach a local minimum. must start at a good initial position close to the minimum. -- `ljff::LJForceField`: the force field used to calculate crystal-molecule interaction energies + + - `xtal::Crystal`: the crystal + - `molecule::Molecule`: the molecule, whose position we seek to tune until we reach a local minimum. must start at a good initial position close to the minimum. + - `ljff::LJForceField`: the force field used to calculate crystal-molecule interaction energies # Returns -- `minimized_molecule::Molecule{Frac}`: the molecule at its minimum energy position -- `min_energy::Float64`: the associated minimum molecule-crystal interaciton energy (kJ/mol) + + - `minimized_molecule::Molecule{Frac}`: the molecule at its minimum energy position + - `min_energy::Float64`: the associated minimum molecule-crystal interaciton energy (kJ/mol) """ -function find_energy_minimum(xtal::Crystal, - molecule::Molecule, - ljff::LJForceField - ) +function find_energy_minimum(xtal::Crystal, molecule::Molecule, ljff::LJForceField) if needs_rotations(molecule) @warn "needs rotations. does not optimize over configurations, only over center of mass" end if has_charges(molecule) && has_charges(xtal) error("cannot handle electrostatics") end - + # make sure replication factors sufficient rep_factors = replication_factors(xtal, sqrt(ljff.r²_cutoff)) xtal = replicate(xtal, rep_factors) - + # make sure molecule is in fractional coords if isa(molecule, Molecule{Cart}) molecule = Frac(molecule, xtal.box) else translate_to!(molecule, Frac(molecule.com.xf ./ rep_factors)) end - + # xf::Array{Float64, 1} function energy(xf) # make sure the coords are fractional @@ -53,7 +52,7 @@ function find_energy_minimum(xtal::Crystal, xf_min = mod.(mod.(res.minimizer, 1.0) .* rep_factors, 1.0) # translate molecule to min energy position to return it. translate_to!(molecule, Frac(xf_min)) - + return molecule, res.minimum end @@ -63,17 +62,23 @@ end perform an [`energy_grid`](@ref) calculation and, via a grid search, find the minimum energy position of a molecule. # Arguments -- `xtal::Crystal`: The crystal being investigated -- `molecule::Molecule{Cart}`: The molecule used to probe energy surface -- `ljff::LJForceField`: The force field used to calculate interaction energies -- `resolution::Union{Float64, Tuple{Int, Int, Int}}=1.0`: maximum distance between grid points, in Å, or a tuple specifying the number of grid points in each dimension. + + - `xtal::Crystal`: The crystal being investigated + - `molecule::Molecule{Cart}`: The molecule used to probe energy surface + - `ljff::LJForceField`: The force field used to calculate interaction energies + - `resolution::Union{Float64, Tuple{Int, Int, Int}}=1.0`: maximum distance between grid points, in Å, or a tuple specifying the number of grid points in each dimension. # Returns -- `minimized_molecule::Molecule{Frac}`: the molecule at its minimum energy position -- `min_energy::Float64`: the associated minimum molecule-crystal interaciton energy (kJ/mol) + + - `minimized_molecule::Molecule{Frac}`: the molecule at its minimum energy position + - `min_energy::Float64`: the associated minimum molecule-crystal interaciton energy (kJ/mol) """ -function find_energy_minimum_gridsearch(xtal::Crystal, molecule::Molecule{Cart}, ljff::LJForceField; - resolution::Union{Float64, Tuple{Int, Int, Int}}=1.0)::Tuple{Molecule{Frac}, Float64} +function find_energy_minimum_gridsearch( + xtal::Crystal, + molecule::Molecule{Cart}, + ljff::LJForceField; + resolution::Union{Float64, Tuple{Int, Int, Int}}=1.0 +)::Tuple{Molecule{Frac}, Float64} # Perform an energy grid calculation on a course grid to get initial estimate. grid = energy_grid(xtal, molecule, ljff; resolution=resolution) E_min, idx_min = findmin(grid.data) diff --git a/src/energy_utilities.jl b/src/energy_utilities.jl index 0659fae64..bef3d865e 100644 --- a/src/energy_utilities.jl +++ b/src/energy_utilities.jl @@ -15,20 +15,28 @@ Base.sum(energy::PotentialEnergy) = energy.vdw + energy.es *(energy::PotentialEnergy, a::Float64) = *(a, energy) /(energy::PotentialEnergy, a::Float64) = PotentialEnergy(energy.vdw / a, energy.es / a) /(energy::PotentialEnergy, a::Int) = PotentialEnergy(energy.vdw / a, energy.es / a) -square(u::PotentialEnergy) = PotentialEnergy(u.vdw ^ 2, u.es ^ 2) +square(u::PotentialEnergy) = PotentialEnergy(u.vdw^2, u.es^2) Base.sqrt(u::PotentialEnergy) = PotentialEnergy(sqrt(u.vdw), sqrt(u.es)) -std(energies::Array{PotentialEnergy, 1}) = PotentialEnergy(std([en.vdw for en in energies]), - std([en.es for en in energies]) - ) +function std(energies::Array{PotentialEnergy, 1}) + return PotentialEnergy( + std([en.vdw for en in energies]), + std([en.es for en in energies]) + ) +end -function Base.isapprox(u::PotentialEnergy, v::PotentialEnergy; verbose::Bool=true, atol::Float64=1e-6) - if ! isapprox(u.vdw, v.vdw, atol=atol) +function Base.isapprox( + u::PotentialEnergy, + v::PotentialEnergy; + verbose::Bool=true, + atol::Float64=1e-6 +) + if !isapprox(u.vdw, v.vdw; atol=atol) if verbose @warn "vdw energy mismatch" end return false end - if ! isapprox(u.es, v.es, atol=atol) + if !isapprox(u.es, v.es; atol=atol) if verbose @warn "es energy mismatch" end @@ -42,29 +50,34 @@ mutable struct SystemPotentialEnergy gg::PotentialEnergy end SystemPotentialEnergy() = SystemPotentialEnergy(PotentialEnergy(), PotentialEnergy()) # constructor -Base.sum(v::SystemPotentialEnergy) = v.gg.vdw + v.gg.es + - v.gh.vdw + v.gh.es -+(u::SystemPotentialEnergy, v::SystemPotentialEnergy) = SystemPotentialEnergy(u.gh + v.gh, - u.gg + v.gg) --(u::SystemPotentialEnergy, v::SystemPotentialEnergy) = SystemPotentialEnergy(u.gh - v.gh, - u.gg - v.gg) +Base.sum(v::SystemPotentialEnergy) = v.gg.vdw + v.gg.es + v.gh.vdw + v.gh.es +function +(u::SystemPotentialEnergy, v::SystemPotentialEnergy) + return SystemPotentialEnergy(u.gh + v.gh, u.gg + v.gg) +end +function -(u::SystemPotentialEnergy, v::SystemPotentialEnergy) + return SystemPotentialEnergy(u.gh - v.gh, u.gg - v.gg) +end *(u::SystemPotentialEnergy, a::Float64) = SystemPotentialEnergy(a * u.gh, a * u.gg) *(a::Float64, u::SystemPotentialEnergy) = *(u::SystemPotentialEnergy, a::Float64) /(u::SystemPotentialEnergy, a::Float64) = SystemPotentialEnergy(u.gh / a, u.gg / a) /(u::SystemPotentialEnergy, a::Int) = SystemPotentialEnergy(u.gh / a, u.gg / a) Base.sqrt(u::SystemPotentialEnergy) = SystemPotentialEnergy(sqrt(u.gh), sqrt(u.gg)) square(u::SystemPotentialEnergy) = SystemPotentialEnergy(square(u.gh), square(u.gg)) -std(us::Array{SystemPotentialEnergy, 1}) = SystemPotentialEnergy(std([u.gh for u in us]), - std([u.gg for u in us]) - ) +function std(us::Array{SystemPotentialEnergy, 1}) + return SystemPotentialEnergy(std([u.gh for u in us]), std([u.gg for u in us])) +end -function Base.isapprox(u::SystemPotentialEnergy, v::SystemPotentialEnergy; - verbose::Bool=true, atol::Float64=1e-6) - if ! isapprox(u.gh, v.gh, verbose=verbose, atol=atol) +function Base.isapprox( + u::SystemPotentialEnergy, + v::SystemPotentialEnergy; + verbose::Bool=true, + atol::Float64=1e-6 +) + if !isapprox(u.gh, v.gh; verbose=verbose, atol=atol) @warn "(guest-host mismatch)" return false end - if ! isapprox(u.gg, v.gg, verbose=verbose, atol=atol) + if !isapprox(u.gg, v.gg; verbose=verbose, atol=atol) @warn "(guest-guest mismatch)" return false end diff --git a/src/eos.jl b/src/eos.jl index 8304f38a2..244747c7d 100644 --- a/src/eos.jl +++ b/src/eos.jl @@ -13,26 +13,28 @@ struct PengRobinsonFluid ω::Float64 end - # Parameters in the Peng-Robinson Equation of State # T in Kelvin, P in bar -a(fluid::PengRobinsonFluid) = (0.457235 * UNIV_GAS_CONST ^ 2 * fluid.Tc ^ 2) / fluid.Pc +a(fluid::PengRobinsonFluid) = (0.457235 * UNIV_GAS_CONST^2 * fluid.Tc^2) / fluid.Pc b(fluid::PengRobinsonFluid) = (0.0777961 * UNIV_GAS_CONST * fluid.Tc) / fluid.Pc -κ(fluid::PengRobinsonFluid) = 0.37464 + (1.54226 * fluid.ω) - (0.26992 * fluid.ω ^ 2) -α(κ::Float64, Tr::Float64) = (1 + κ * (1 - √Tr)) ^ 2 -A(T::Float64, P::Float64, fluid::PengRobinsonFluid) = α(κ(fluid), T / fluid.Tc) * a(fluid) * P / (UNIV_GAS_CONST ^ 2 * T ^ 2) +κ(fluid::PengRobinsonFluid) = 0.37464 + (1.54226 * fluid.ω) - (0.26992 * fluid.ω^2) +α(κ::Float64, Tr::Float64) = (1 + κ * (1 - √Tr))^2 +function A(T::Float64, P::Float64, fluid::PengRobinsonFluid) + return α(κ(fluid), T / fluid.Tc) * a(fluid) * P / (UNIV_GAS_CONST^2 * T^2) +end B(T::Float64, P::Float64, fluid::PengRobinsonFluid) = b(fluid) * P / (UNIV_GAS_CONST * T) - # Calculates three outputs for compressibility factor using the polynomial form of # the Peng-Robinson Equation of State. Filters for only real roots and returns the # root closest to unity. function compressibility_factor(fluid::PengRobinsonFluid, T::Float64, P::Float64) # construct cubic polynomial in z - p = Polynomial([-(A(T, P, fluid) * B(T, P, fluid) - B(T, P, fluid) ^ 2 - B(T, P, fluid) ^ 3), - A(T, P, fluid) - 2 * B(T, P, fluid) - 3 * B(T, P, fluid) ^ 2, - -(1.0 - B(T, P, fluid)), - 1.0]) + p = Polynomial([ + -(A(T, P, fluid) * B(T, P, fluid) - B(T, P, fluid)^2 - B(T, P, fluid)^3), + A(T, P, fluid) - 2 * B(T, P, fluid) - 3 * B(T, P, fluid)^2, + -(1.0 - B(T, P, fluid)), + 1.0 + ]) # solve for the roots of the cubic polynomial z_roots = roots(p) # select real roots only. @@ -43,17 +45,16 @@ function compressibility_factor(fluid::PengRobinsonFluid, T::Float64, P::Float64 return real(z_factor[id_closest_to_unity]) end - # Calculating for fugacity coefficient from an integration (bar). function calculate_ϕ(fluid::PengRobinsonFluid, T::Float64, P::Float64) z = compressibility_factor(fluid, T, P) - log_ϕ = z - 1.0 - log(z - B(T, P, fluid)) + - - A(T, P, fluid) / (√8 * B(T, P, fluid)) * log( - (z + (1 + √2) * B(T, P, fluid)) / (z + (1 - √(2)) * B(T, P, fluid))) + log_ϕ = + z - 1.0 - log(z - B(T, P, fluid)) + + -A(T, P, fluid) / (√8 * B(T, P, fluid)) * + log((z + (1 + √2) * B(T, P, fluid)) / (z + (1 - √(2)) * B(T, P, fluid))) return exp(log_ϕ) end - """ fluid = PengRobinsonFluid(fluid) @@ -63,16 +64,29 @@ and returns a complete `PengRobinsonFluid` data structure. **NOTE: Do not delete the last three comment lines in PengRobinson_fluid_props.csv # Arguments -- `fluid::Symbol`: The fluid molecule you wish to construct a PengRobinsonFluid struct for + + - `fluid::Symbol`: The fluid molecule you wish to construct a PengRobinsonFluid struct for # Returns -- `PengRobinsonFluid::struct`: Data structure containing Peng-Robinson fluid parameters. + + - `PengRobinsonFluid::struct`: Data structure containing Peng-Robinson fluid parameters. """ function PengRobinsonFluid(fluid::Symbol) - df = CSV.read(joinpath(rc[:paths][:data], "PengRobinson_fluid_props.csv"), DataFrame, copycols=true, comment="#") + df = CSV.read( + joinpath(rc[:paths][:data], "PengRobinson_fluid_props.csv"), + DataFrame; + copycols=true, + comment="#" + ) filter!(row -> row[:fluid] == string(fluid), df) if nrow(df) == 0 - error(@sprintf("fluid %s properties not found in %sPengRobinson_fluid_props.csv", fluid, rc[:paths][:data])) + error( + @sprintf( + "fluid %s properties not found in %sPengRobinson_fluid_props.csv", + fluid, + rc[:paths][:data] + ) + ) end Tc = df[1, Symbol("Tc(K)")] Pc = df[1, Symbol("Pc(bar)")] @@ -80,16 +94,14 @@ function PengRobinsonFluid(fluid::Symbol) return PengRobinsonFluid(fluid, Tc, Pc, ω) end - # Prints resulting values for Peng-Robinson fluid properties function Base.show(io::IO, fluid::PengRobinsonFluid) println(io, "fluid species: ", fluid.fluid) println(io, "\tCritical temperature (K): ", fluid.Tc) println(io, "\tCritical pressure (bar): ", fluid.Pc) - println(io, "\tAcenteric factor: ", fluid.ω) + return println(io, "\tAcenteric factor: ", fluid.ω) end - # Data structure stating characteristics of a van der Waals fluid struct VdWFluid "van der Waals Fluid species. e.g. :CO2" @@ -100,7 +112,6 @@ struct VdWFluid b::Float64 end - # Calculates the compressibility factor Z for fluids function compressibility_factor(fluid::VdWFluid, T::Float64, P::Float64) # build polynomial in ρ: D ρ³ + C ρ² + B ρ + A = 0 @@ -108,9 +119,9 @@ function compressibility_factor(fluid::VdWFluid, T::Float64, P::Float64) # McQuarrie, Donald A., and John D. Simon. Molecular Thermodynamics. # University Science Books, 1999. pg. 57 example 2-2 - D = - fluid.a * fluid.b + D = -fluid.a * fluid.b C = fluid.a - B = - (P * fluid.b + UNIV_GAS_CONST * T) + B = -(P * fluid.b + UNIV_GAS_CONST * T) A = P # Creates polynomial in ρ the VdW cubic function @@ -127,7 +138,6 @@ function compressibility_factor(fluid::VdWFluid, T::Float64, P::Float64) return z end - # Calculates for fugacity using derivation of van der Waals EOS function calculate_ϕ(fluid::VdWFluid, T::Float64, P::Float64) log_f = log(P) + (fluid.b - fluid.a / (UNIV_GAS_CONST * T)) * (P / (UNIV_GAS_CONST * T)) @@ -136,7 +146,6 @@ function calculate_ϕ(fluid::VdWFluid, T::Float64, P::Float64) return ϕ end - """ fluid = VdWFluid(fluid) @@ -146,31 +155,42 @@ and returns a complete `VdWFluid` data structure. ***NOTE: Do not delete the last three comment lines in VdW_fluid_props.csv # Arguments -- `fluid::Symbol`: The fluid you wish to construct a VdWFluid struct for + + - `fluid::Symbol`: The fluid you wish to construct a VdWFluid struct for # Returns -- `VdWFluid::struct`: Data structure containing van der Waals constants + + - `VdWFluid::struct`: Data structure containing van der Waals constants """ function VdWFluid(fluid::Symbol) - df = CSV.read(joinpath(rc[:paths][:data], "VdW_fluid_props.csv"), DataFrame, copycols=true, comment="#") + df = CSV.read( + joinpath(rc[:paths][:data], "VdW_fluid_props.csv"), + DataFrame; + copycols=true, + comment="#" + ) filter!(row -> row[:fluid] == string(fluid), df) if nrow(df) == 0 - error(@sprintf("Fluid %s constants not found in %sVdW_fluidops.csv", fluid, rc[:paths][:data])) + error( + @sprintf( + "Fluid %s constants not found in %sVdW_fluidops.csv", + fluid, + rc[:paths][:data] + ) + ) end a = df[1, Symbol("a(bar*m^6/mol^2)")] b = df[1, Symbol("b(m^3/mol)")] return VdWFluid(fluid, a, b) end - # Prints resulting values for van der Waals constants function Base.show(io::IO, fluid::VdWFluid) println(io, "Fluid species: ", fluid.fluid) println(io, "Constant a (bar*m⁶/mol²): ", fluid.a) - println(io, "Constant b (m³/mol): ", fluid.b) + return println(io, "Constant b (m³/mol): ", fluid.b) end - """ props = calculate_properties(fluid, T, P, verbose=true) @@ -178,15 +198,22 @@ Use equation of state to calculate density, fugacity, and molar volume of a real given temperature and pressure. # Arguments -- `fluid::Union{PengRobinsonFluid, VdWFluid}`: Peng-Robinson/ van der Waals fluid data structure -- `T::Float64`: Temperature (units: Kelvin) -- `P::Float64`: Pressure (units: bar) -- `verbose::Bool`: will print results if `true` + + - `fluid::Union{PengRobinsonFluid, VdWFluid}`: Peng-Robinson/ van der Waals fluid data structure + - `T::Float64`: Temperature (units: Kelvin) + - `P::Float64`: Pressure (units: bar) + - `verbose::Bool`: will print results if `true` # Returns -- `prop_dict::Dict`: Dictionary of Peng-Robinson/ van der Waals fluid properties + + - `prop_dict::Dict`: Dictionary of Peng-Robinson/ van der Waals fluid properties """ -function calculate_properties(fluid::Union{PengRobinsonFluid, VdWFluid}, T::Float64, P::Float64; verbose::Bool=true) +function calculate_properties( + fluid::Union{PengRobinsonFluid, VdWFluid}, + T::Float64, + P::Float64; + verbose::Bool=true +) # Compressbility factor (unitless) z = compressibility_factor(fluid, T, P) # Density (mol/m^3) @@ -198,9 +225,13 @@ function calculate_properties(fluid::Union{PengRobinsonFluid, VdWFluid}, T::Floa f = ϕ * P # Prints a dictionary holding values for compressibility factor, molar volume, # density, and fugacity. - prop_dict = Dict("compressibility factor" => z, "molar volume (L/mol)"=> Vm , - "density (mol/m³)" => ρ, "fugacity (bar)" => f, - "fugacity coefficient" => ϕ) + prop_dict = Dict( + "compressibility factor" => z, + "molar volume (L/mol)" => Vm, + "density (mol/m³)" => ρ, + "fugacity (bar)" => f, + "fugacity coefficient" => ϕ + ) if verbose @printf("%s properties at T = %f K, P = %f bar:\n", fluid.fluid, T, P) for (property, value) in prop_dict diff --git a/src/forcefield.jl b/src/forcefield.jl index b4f139675..d44e1570c 100644 --- a/src/forcefield.jl +++ b/src/forcefield.jl @@ -2,106 +2,115 @@ Data structure for a Lennard Jones forcefield. # Attributes -- `name::String`: name of forcefield; correponds to filename -- `pure_σ::Dict{Symbol, Float64}`: Dictionary that returns Lennard-Jones σ of an X-X interaction, where X is an atom. (units: Angstrom) -- `pure_ϵ::Dict{Symbol, Float64}`: Dictionary that returns Lennard-Jones ϵ of an X-X interaction, where X is an atom. (units: K) -- `σ²::Dict{Symbol, Dict{Symbol, Float64}}`: Lennard Jones σ² (units: Angstrom²) for cross-interactions. Example use is `sigmas_squared[:He][:C]` -- `ϵ::Dict{Symbol, Dict{Symbol, Float64}}`: Lennard Jones ϵ (units: K) for cross-interactions. Example use is `epsilons[:He][:C]` -- `r²_cutoff::Float64`: The square of the cut-off radius beyond which we define the potential energy to be zero (units: Angstrom²). We store σ² to speed up computations, which involve σ², not σ. + + - `name::String`: name of forcefield; correponds to filename + - `pure_σ::Dict{Symbol, Float64}`: Dictionary that returns Lennard-Jones σ of an X-X interaction, where X is an atom. (units: Angstrom) + - `pure_ϵ::Dict{Symbol, Float64}`: Dictionary that returns Lennard-Jones ϵ of an X-X interaction, where X is an atom. (units: K) + - `σ²::Dict{Symbol, Dict{Symbol, Float64}}`: Lennard Jones σ² (units: Angstrom²) for cross-interactions. Example use is `sigmas_squared[:He][:C]` + - `ϵ::Dict{Symbol, Dict{Symbol, Float64}}`: Lennard Jones ϵ (units: K) for cross-interactions. Example use is `epsilons[:He][:C]` + - `r²_cutoff::Float64`: The square of the cut-off radius beyond which we define the potential energy to be zero (units: Angstrom²). We store σ² to speed up computations, which involve σ², not σ. """ struct LJForceField name::String - pure_σ::Dict{Symbol, Float64} - pure_ϵ::Dict{Symbol, Float64} + pure_σ::Dict{Symbol, Float64} + pure_ϵ::Dict{Symbol, Float64} - σ²::Dict{Symbol, Dict{Symbol, Float64}} - ϵ::Dict{Symbol, Dict{Symbol, Float64}} + σ²::Dict{Symbol, Dict{Symbol, Float64}} + ϵ::Dict{Symbol, Dict{Symbol, Float64}} - r²_cutoff::Float64 + r²_cutoff::Float64 end - Base.broadcastable(ljff::LJForceField) = Ref(ljff) - """ - ljforcefield = LJForceField(forcefield; r_cutoff=14.0, mixing_rules="Lorentz-Berthelot") + ljforcefield = LJForceField(forcefield; r_cutoff=14.0, mixing_rules="Lorentz-Berthelot") Read a .csv file containing Lennard Jones parameters (with the following columns: `atom,sigma,epsilon` and constructs a LJForceField object. The following mixing rules are implemented: -* Kong mixing rules: DOI 10.1063/1.1680358 -* Lorentz-Berthelot: https://en.wikipedia.org/wiki/Combining_rules#Lorentz-Berthelot_rules -* Geometric + + - Kong mixing rules: DOI 10.1063/1.1680358 + - Lorentz-Berthelot: https://en.wikipedia.org/wiki/Combining_rules#Lorentz-Berthelot_rules + - Geometric # Arguments -- `forcefield::String`: name of the forcefield. -- `r_cutoff::Float64`: cutoff radius beyond which we define the potential energy to be zero (units: Angstrom) -- `mixing_rules::String`: The mixing rules used to compute the cross-interaction terms of the forcefield + + - `forcefield::String`: name of the forcefield. + - `r_cutoff::Float64`: cutoff radius beyond which we define the potential energy to be zero (units: Angstrom) + - `mixing_rules::String`: The mixing rules used to compute the cross-interaction terms of the forcefield # Returns -- `ljforcefield::LJForceField`: The data structure containing the forcefield parameters (pure σ, ϵ and cross interaction terms as well) + + - `ljforcefield::LJForceField`: The data structure containing the forcefield parameters (pure σ, ϵ and cross interaction terms as well) """ -function LJForceField(forcefield::String; r_cutoff::Float64=14.0, - mixing_rules::String="Lorentz-Berthelot") - if ! (lowercase(mixing_rules) in ["lorentz-berthelot", "kong", "geometric"]) +function LJForceField( + forcefield::String; + r_cutoff::Float64=14.0, + mixing_rules::String="Lorentz-Berthelot" +) + if !(lowercase(mixing_rules) in ["lorentz-berthelot", "kong", "geometric"]) error(@sprintf("%s mixing rules not implemented...\n", mixing_rules)) end forcefield_file_path = joinpath(rc[:paths][:forcefields], forcefield * ".csv") - df = CSV.read(forcefield_file_path, DataFrame, comment="#") # from DataFrames + df = CSV.read(forcefield_file_path, DataFrame; comment="#") # from DataFrames - ljff = LJForceField(forcefield, Dict(), Dict(), Dict(), Dict(), r_cutoff^ 2) + ljff = LJForceField(forcefield, Dict(), Dict(), Dict(), Dict(), r_cutoff^2) # pure X-X interactions (X = (pseudo)atom) for row in eachrow(df) atom_species = Symbol(row[:atom]) # if atom already recorded, we have a duplicate. this is dangerous to overwrite. if atom_species in keys(ljff.pure_σ) - error(@sprintf("Atom %s listed at least twice in %s.\n", atom_species, - forcefield_file_path)) + error( + @sprintf( + "Atom %s listed at least twice in %s.\n", + atom_species, + forcefield_file_path + ) + ) end - ljff.pure_σ[atom_species] = row[Symbol("sigma(A)")] - ljff.pure_ϵ[atom_species] = row[Symbol("epsilon(K)")] + ljff.pure_σ[atom_species] = row[Symbol("sigma(A)")] + ljff.pure_ϵ[atom_species] = row[Symbol("epsilon(K)")] end # cross X-Y interactions (X, Y = generally different (pseduo)atoms) - for atom in [Symbol(atom) for atom in keys(ljff.pure_σ)] + for atom in [Symbol(atom) for atom in keys(ljff.pure_σ)] ljff.ϵ[atom] = Dict{Symbol, Float64}() ljff.σ²[atom] = Dict{Symbol, Float64}() - for other_atom in [Symbol(other_atom) for other_atom in keys(ljff.pure_σ)] + for other_atom in [Symbol(other_atom) for other_atom in keys(ljff.pure_σ)] if lowercase(mixing_rules) == "lorentz-berthelot" ϵ_ij = sqrt(ljff.pure_ϵ[atom] * ljff.pure_ϵ[other_atom]) - σ_ij² = ((ljff.pure_σ[atom] + ljff.pure_σ[other_atom]) / 2.0) ^ 2 + σ_ij² = ((ljff.pure_σ[atom] + ljff.pure_σ[other_atom]) / 2.0)^2 elseif lowercase(mixing_rules) == "kong" - ϵ_iiσ_ii⁶ = ljff.pure_ϵ[atom] * ljff.pure_σ[atom] ^ 6 - ϵ_jjσ_jj⁶ = ljff.pure_ϵ[other_atom] * ljff.pure_σ[other_atom] ^ 6 + ϵ_iiσ_ii⁶ = ljff.pure_ϵ[atom] * ljff.pure_σ[atom]^6 + ϵ_jjσ_jj⁶ = ljff.pure_ϵ[other_atom] * ljff.pure_σ[other_atom]^6 - ϵ_iiσ_ii¹² = ϵ_iiσ_ii⁶ * ljff.pure_σ[atom] ^ 6 - ϵ_jjσ_jj¹² = ϵ_jjσ_jj⁶ * ljff.pure_σ[other_atom] ^ 6 + ϵ_iiσ_ii¹² = ϵ_iiσ_ii⁶ * ljff.pure_σ[atom]^6 + ϵ_jjσ_jj¹² = ϵ_jjσ_jj⁶ * ljff.pure_σ[other_atom]^6 ϵ_ijσ_ij⁶ = sqrt(ϵ_iiσ_ii⁶ * ϵ_jjσ_jj⁶) - ϵ_ijσ_ij¹² = ((ϵ_iiσ_ii¹² ^ (1/13) + ϵ_jjσ_jj¹² ^ (1/13)) / 2) ^ 13 + ϵ_ijσ_ij¹² = ((ϵ_iiσ_ii¹²^(1 / 13) + ϵ_jjσ_jj¹²^(1 / 13)) / 2)^13 - ϵ_ij = ϵ_ijσ_ij⁶ ^ 2 / ϵ_ijσ_ij¹² - σ_ij² = (ϵ_ijσ_ij¹² / ϵ_ijσ_ij⁶) ^ (1/3) + ϵ_ij = ϵ_ijσ_ij⁶^2 / ϵ_ijσ_ij¹² + σ_ij² = (ϵ_ijσ_ij¹² / ϵ_ijσ_ij⁶)^(1 / 3) elseif lowercase(mixing_rules) == "geometric" ϵ_ij = sqrt(ljff.pure_ϵ[atom] * ljff.pure_ϵ[other_atom]) σ_ij² = ljff.pure_σ[atom] * ljff.pure_σ[other_atom] # √(σ_i σ_j)² end ljff.ϵ[atom][other_atom] = ϵ_ij ljff.σ²[atom][other_atom] = σ_ij² - end - end + end + end - return ljff + return ljff end - """ - repfactors = replication_factors(unitcell, r_cutoff) + repfactors = replication_factors(unitcell, r_cutoff) Find the replication factors needed to make a supercell big enough to fit a sphere with the specified cutoff radius. In PorousMaterials.jl, rather than replicating the atoms in the home unit cell to build the supercell that @@ -111,56 +120,63 @@ This function ensures enough replication factors such that the nearest image con A non-replicated supercell has 1 as the replication factor in each dimension (`repfactors = (1, 1, 1)`). # Arguments -- `unitcell::Box`: The unit cell of the crystal -- `r_cutoff::Float64`: Cutoff radius beyond which we define the potential energy to be zero (units: Angstrom) + + - `unitcell::Box`: The unit cell of the crystal + - `r_cutoff::Float64`: Cutoff radius beyond which we define the potential energy to be zero (units: Angstrom) # Returns -- `repfactors::Tuple{Int, Int, Int}`: The replication factors in the a, b, c directions + + - `repfactors::Tuple{Int, Int, Int}`: The replication factors in the a, b, c directions """ function replication_factors(unitcell::Box, r_cutoff::Float64) - # Unit vectors used to transform from fractional coordinates to cartesian coordinates. We'll be - a = unitcell.f_to_c[:, 1] - b = unitcell.f_to_c[:, 2] - c = unitcell.f_to_c[:, 3] - - n_ab = cross(a, b) - n_ac = cross(a, c) - n_bc = cross(b, c) - - # c0 defines a center in the unit cell - c0 = [a b c] * [.5, .5, .5] - - rep = [1, 1, 1] - - # Repeat for `a` - # |n_bc ⋅ c0|/|n_bc| defines the distance from the end of the supercell and the center. As long as that distance is less than the cutoff radius, we need to increase it - while abs(dot(n_bc, c0)) / norm(n_bc) < r_cutoff - rep[1] += 1 - a += unitcell.f_to_c[:,1] - c0 = [a b c] * [.5, .5, .5] - end - - # Repeat for `b` - while abs(dot(n_ac, c0)) / norm(n_ac) < r_cutoff - rep[2] += 1 - b += unitcell.f_to_c[:,2] - c0 = [a b c] * [.5, .5, .5] - end - - # Repeat for `c` - while abs(dot(n_ab, c0)) / norm(n_ab) < r_cutoff - rep[3] += 1 - c += unitcell.f_to_c[:,3] - c0 = [a b c] * [.5, .5, .5] - end - - return (rep[1], rep[2], rep[3]) -end + # Unit vectors used to transform from fractional coordinates to cartesian coordinates. We'll be + a = unitcell.f_to_c[:, 1] + b = unitcell.f_to_c[:, 2] + c = unitcell.f_to_c[:, 3] + + n_ab = cross(a, b) + n_ac = cross(a, c) + n_bc = cross(b, c) + + # c0 defines a center in the unit cell + c0 = [a b c] * [0.5, 0.5, 0.5] + + rep = [1, 1, 1] + + # Repeat for `a` + # |n_bc ⋅ c0|/|n_bc| defines the distance from the end of the supercell and the center. As long as that distance is less than the cutoff radius, we need to increase it + while abs(dot(n_bc, c0)) / norm(n_bc) < r_cutoff + rep[1] += 1 + a += unitcell.f_to_c[:, 1] + c0 = [a b c] * [0.5, 0.5, 0.5] + end -replication_factors(unitcell::Box, ljforcefield::LJForceField) = replication_factors(unitcell, sqrt(ljforcefield.r²_cutoff)) -replication_factors(crystal::Crystal, r_cutoff::Float64) = replication_factors(crystal.box, r_cutoff) -replication_factors(crystal::Crystal, ljforcefield::LJForceField) = replication_factors(crystal.box, sqrt(ljforcefield.r²_cutoff)) + # Repeat for `b` + while abs(dot(n_ac, c0)) / norm(n_ac) < r_cutoff + rep[2] += 1 + b += unitcell.f_to_c[:, 2] + c0 = [a b c] * [0.5, 0.5, 0.5] + end + + # Repeat for `c` + while abs(dot(n_ab, c0)) / norm(n_ab) < r_cutoff + rep[3] += 1 + c += unitcell.f_to_c[:, 3] + c0 = [a b c] * [0.5, 0.5, 0.5] + end + return (rep[1], rep[2], rep[3]) +end + +function replication_factors(unitcell::Box, ljforcefield::LJForceField) + return replication_factors(unitcell, sqrt(ljforcefield.r²_cutoff)) +end +function replication_factors(crystal::Crystal, r_cutoff::Float64) + return replication_factors(crystal.box, r_cutoff) +end +function replication_factors(crystal::Crystal, ljforcefield::LJForceField) + return replication_factors(crystal.box, sqrt(ljforcefield.r²_cutoff)) +end """ forcefield_coverage(atoms, ljforcefield) @@ -171,11 +187,13 @@ Check that the force field contains parameters for every `species` in `atoms::At Will print out which atoms are missing. # Arguments -- `atoms::Atoms`: a set of atoms -- `ljforcefield::LJForceField`: A Lennard Jones forcefield object containing information on atom interactions + + - `atoms::Atoms`: a set of atoms + - `ljforcefield::LJForceField`: A Lennard Jones forcefield object containing information on atom interactions # Returns -- `all_covered::Bool`: returns true if all species in the atoms are covered by the force field. + + - `all_covered::Bool`: returns true if all species in the atoms are covered by the force field. """ function forcefield_coverage(atoms::Atoms, ljff::LJForceField) unique_species = unique(atoms.species) @@ -188,14 +206,22 @@ function forcefield_coverage(atoms::Atoms, ljff::LJForceField) end return all_covered end -forcefield_coverage(crystal::Crystal, ljff::LJForceField) = forcefield_coverage(crystal.atoms, ljff) - +function forcefield_coverage(crystal::Crystal, ljff::LJForceField) + return forcefield_coverage(crystal.atoms, ljff) +end function Base.show(io::IO, ff::LJForceField) println(io, "Force field: ", ff.name) - println(io, "Number of atoms included: ", length(ff.pure_σ)) - println(io, "Cut-off radius (Å) = ", sqrt(ff.r²_cutoff)) + println(io, "Number of atoms included: ", length(ff.pure_σ)) + println(io, "Cut-off radius (Å) = ", sqrt(ff.r²_cutoff)) for atom in keys(ff.pure_σ) - @printf(io, "%5s-%5s ϵ = %10.5f K, σ = %10.5f Å\n", atom, atom, ff.pure_ϵ[atom], ff.pure_σ[atom]) + @printf( + io, + "%5s-%5s ϵ = %10.5f K, σ = %10.5f Å\n", + atom, + atom, + ff.pure_ϵ[atom], + ff.pure_σ[atom] + ) end end diff --git a/src/gcmc.jl b/src/gcmc.jl index f2ed539cd..8031e35f1 100644 --- a/src/gcmc.jl +++ b/src/gcmc.jl @@ -1,23 +1,9 @@ -import Base: +, / +include("muvt.jl") +include("nvt.jl") -### -# Markov chain proposals -### -const PROPOSAL_ENCODINGS = Dict(1 => "insertion", - 2 => "deletion", - 3 => "translation", - 4 => "rotation", - 5 => "reinsertion", - 6 => "identity change" - ) # helps with printing later -const N_PROPOSAL_TYPES = length(keys(PROPOSAL_ENCODINGS)) -# each proposal type gets an Int for clearer code -const INSERTION = Dict([v => k for (k, v) in PROPOSAL_ENCODINGS])["insertion"] -const DELETION = Dict([v => k for (k, v) in PROPOSAL_ENCODINGS])["deletion"] -const TRANSLATION = Dict([v => k for (k, v) in PROPOSAL_ENCODINGS])["translation"] -const ROTATION = Dict([v => k for (k, v) in PROPOSAL_ENCODINGS])["rotation"] -const REINSERTION = Dict([v => k for (k, v) in PROPOSAL_ENCODINGS])["reinsertion"] -const IDENTITY_CHANGE = Dict([v => k for (k, v) in PROPOSAL_ENCODINGS])["identity change"] +using .MuVT, .NVT + +import Base: +, / # count proposed/accepted for each subtype mutable struct MarkovCounts @@ -25,7 +11,6 @@ mutable struct MarkovCounts n_accepted::Array{Int, 1} end - ### # collecting statistics ### @@ -41,40 +26,49 @@ mutable struct GCMCstats Un::Float64 # ⟨U n⟩ end -GCMCstats(nb_species::Int) = GCMCstats(0, zeros(Int, nb_species), zeros(Int, nb_species), SystemPotentialEnergy(), SystemPotentialEnergy(), 0.0) - - -+(s1::GCMCstats, s2::GCMCstats) = GCMCstats(s1.n_samples + s2.n_samples, - s1.n .+ s2.n, - s1.n² .+ s2.n², - s1.U + s2.U, - s1.U² + s2.U², - s1.Un + s2.Un) +function GCMCstats(nb_species::Int) + return GCMCstats( + 0, + zeros(Int, nb_species), + zeros(Int, nb_species), + SystemPotentialEnergy(), + SystemPotentialEnergy(), + 0.0 + ) +end +function +(s1::GCMCstats, s2::GCMCstats) + return GCMCstats( + s1.n_samples + s2.n_samples, + s1.n .+ s2.n, + s1.n² .+ s2.n², + s1.U + s2.U, + s1.U² + s2.U², + s1.Un + s2.Un + ) +end function Base.sum(gcmc_stats::Array{GCMCstats, 1}) nb_species = length(gcmc_stats[1].n) - sum_stats = GCMCstats(nb_species) + sum_stats = GCMCstats(nb_species) for gs in gcmc_stats sum_stats += gs end return sum_stats end - function Base.print(gcmc_stats::GCMCstats) println("\t# samples: ", gcmc_stats.n_samples) println("\t⟨N⟩ (molecules) = ", gcmc_stats.n / gcmc_stats.n_samples) - println("\t⟨U_gh, vdw⟩ (K) = ", gcmc_stats.U.gh.vdw / gcmc_stats.n_samples) - println("\t⟨U_gh, Coulomb⟩ (K) = ", gcmc_stats.U.gh.es / gcmc_stats.n_samples) - println("\t⟨U_gg, vdw⟩ (K) = ", gcmc_stats.U.gg.vdw / gcmc_stats.n_samples) - println("\t⟨U_gg, Coulomb⟩ (K) = ", gcmc_stats.U.gg.es / gcmc_stats.n_samples) + println("\t⟨U_gh, vdw⟩ (K) = ", gcmc_stats.U.gh.vdw / gcmc_stats.n_samples) + println("\t⟨U_gh, Coulomb⟩ (K) = ", gcmc_stats.U.gh.es / gcmc_stats.n_samples) + println("\t⟨U_gg, vdw⟩ (K) = ", gcmc_stats.U.gg.vdw / gcmc_stats.n_samples) + println("\t⟨U_gg, Coulomb⟩ (K) = ", gcmc_stats.U.gg.es / gcmc_stats.n_samples) - println("\t⟨U⟩ (K) = ", sum(gcmc_stats.U) / gcmc_stats.n_samples) + return println("\t⟨U⟩ (K) = ", sum(gcmc_stats.U) / gcmc_stats.n_samples) end - # Compute average and standard error of the number of molecules and potential # energy from an array of `GCMCstats`, each corresponding to statitics from an # independent block/bin during the simulation. The average from each bin is @@ -92,7 +86,7 @@ function mean_stderr_n_U(gcmc_stats::Array{GCMCstats, 1}) # ⟨N⟩, ⟨U⟩ over each block avg_n_blocks = [gs.n / gs.n_samples for gs in gcmc_stats] avg_U_blocks = [gs.U / gs.n_samples for gs in gcmc_stats] - + # std over the blocks std_n = std(avg_n_blocks) std_U = std(avg_U_blocks) @@ -104,752 +98,26 @@ function mean_stderr_n_U(gcmc_stats::Array{GCMCstats, 1}) return avg_n, err_n, avg_U, err_U end - -# TODO move this to MC helpers? but not sure if it will inline. so wait after test with @time -# potential energy change after inserting/deleting/perturbing coordinates of molecules[which_species][molecule_id] -# compute potential energy of molecules[which_species][molecule_id] with all other molecules and with the framework. -@inline function potential_energy(which_species::Int, - molecule_id::Int, - molecules::Array{Array{Molecule{Frac}, 1}, 1}, - xtal::Crystal, - ljff::LJForceField) - energy = SystemPotentialEnergy() - # van der Waals interactions - energy.gg.vdw = vdw_energy(which_species, molecule_id, molecules, ljff, xtal.box) # guest-guest - energy.gh.vdw = vdw_energy(xtal, molecules[which_species][molecule_id], ljff) # guest-host - # TODO electrostatic interactions (being neglected for now) - energy.gg.es = 0.0 - energy.gh.es = 0.0 - return energy -end - - -""" - results, molecules = μVT_sim(xtal, molecule_templates, temperature, pressure, - ljff; molecules=Array{Molecule, 1}[], settings=settings) - -Runs a grand-canonical (μVT) Monte Carlo simulation of the adsorption of a molecule in a -xtal at a particular temperature and pressure using a -Lennard Jones force field. - -Markov chain Monte Carlo moves include: -* deletion/insertion -* translation -* reinsertion -* identity change (if multiple components) [see here](http://dx.doi.org/10.1080/00268978800100743) - -Translation stepsize is dynamically updated during burn cycles so that acceptance rate of translations is ~0.4. - -A cycle is defined as max(20, number of adsorbates currently in the system) Markov chain -proposals. - -# Arguments -- `xtal::Crystal`: the porous xtal in which we seek to simulate adsorption -- `molecule_templates::Array{Molecule, 1}`: an array of the templates of unique adsorbate molecules of which we seek to simulate -- `temperature::Float64`: temperature of bulk gas phase in equilibrium with adsorbed phase - in the porous material. units: Kelvin (K) -- `pressures::Array{Float64, 1}`: pressure of bulk gas phase in equilibrium with adsorbed phase in the - porous material for each adsorbate. units: bar - the adsorption -- `ljff::LJForceField`: the molecular model used to describe the -- `molecules::Array{Array{Molecule{Cart}, 1}, 1}`: a starting configuration of molecules in the xtal with an array per species. -- `n_cycles_per_volume::Int`: the number of MC cycles per volume, split evenly between `n_burn_cycles` and `'n_sample_cycles`, where - `n_burn_cycles` is the number of cycles to allow the system to reach equilibrium before sampling; and, - `n_sample_cycles` is the number of cycles used for sampling -- `sample_frequency::Int`: during the sampling cycles, sample e.g. the number of - adsorbed gas molecules every this number of Markov proposals -- `verbose::Bool`: whether or not to print off information during the simulation -- `ewald_precision::Float64`: desired precision for the long range Ewald summation -- `eos::Symbol`: equation of state to use for calculation of fugacity from pressure -- `write_adsorbate_snapshots::Bool`: whether the simulation will create and save a snapshot file -- `snapshot_frequency::Int`: the number of cycles taken between each snapshot (after burn cycle completion) -- `calculate_density_grid::Bool`: whether the simulation will keep track of a density grid for adsorbates -- `density_grid_dx::Float64`: The (approximate) space between voxels (in Angstroms) in the density grid. The number of voxels in the simulation box is computed automatically by [`required_n_pts`](@ref). -- `density_grid_molecular_species::Symbol`: the adsorbate for which we will make a density grid of its position (center). -- `density_grid_sim_box::Bool`: `true` if we wish for the density grid to be over the -entire simulation box as opposed to the box of the crystal passed in. `false` if we wish the -density grid to be over the original `xtal.box`, before replication, passed in. -- `autosave::Bool`: `true` if we wish to automatically save the simulation results to the standard path/filename. -- `results_filename_comment::AbstractString`: An optional comment that will be appended to the name of the saved file (if autosaved) -""" -function μVT_sim(xtal::Crystal, - molecule_templates::Array{Molecule{Cart}, 1}, - temperature::Float64, - pressures::Array{Float64, 1}, - ljff::LJForceField; - molecules::Union{Nothing, Array{Array{Molecule{Cart}, 1}, 1}}=nothing, - n_cycles_per_volume::Int=200, - fraction_burn_cycles::Float64=0.5, - sample_frequency::Int=1, - verbose::Bool=true, - ewald_precision::Float64=1e-6, - eos::Symbol=:ideal, - autosave::Bool=true, - show_progress_bar::Bool=false, - write_adsorbate_snapshots::Bool=false, - snapshot_frequency::Int=1, - calculate_density_grid::Bool=false, - density_grid_dx::Float64=1.0, - density_grid_molecular_species::Union{Nothing, Symbol}=nothing, - density_grid_sim_box::Bool=true, - results_filename_comment::String="" - ) - assert_P1_symmetry(xtal) - - start_time = time() - # # to avoid changing the outside object `molecule_` inside this function, we make - # # a deep copy of it here. this serves as a template to copy when we insert a new molecule. - # molecule = deepcopy(molecule_) - - # calculate the number of MC cycles - # separate total number of cycles evenly into burn_cycles and sample_cycles - nb_cycles = max(N_BLOCKS, ceil(Int, n_cycles_per_volume * xtal.box.Ω)) - @assert (0.0 < fraction_burn_cycles) && (fraction_burn_cycles < 1.0) - n_burn_cycles = ceil(Int, nb_cycles * fraction_burn_cycles) - n_sample_cycles = ceil(Int, nb_cycles * (1 - fraction_burn_cycles)) - - nb_species = length(molecule_templates) - molecular_species = [mt.species for mt in molecule_templates] - - if nb_species != length(pressures) - error("# molecules in simulation: $nb_species - # partial pressures: $(length(pressures)) - these should be equal!") - end - - if verbose - pretty_print(xtal, molecule_templates, temperature, pressures, ljff) - println("\t# burn cycles: ", n_burn_cycles) - println("\t# sample cycles: ", n_sample_cycles) - end - - ### - # xyz file for storing snapshots of adsorbate positions - ### - num_snapshots = 0 - xyz_snapshots_filename = μVT_output_filename(xtal, molecule_templates, temperature, pressures, ljff, - n_burn_cycles, n_sample_cycles, extension=".xyz") - xyz_snapshot_file = IOStream(xyz_snapshots_filename) # declare a variable outside of scope so we only open a file if we want to snapshot - if write_adsorbate_snapshots - xyz_snapshot_file = open(xyz_snapshots_filename, "w") - end - - ### - # Convert pressure to fugacity (units: Pascal) using an equation of state - ### - # TODO make PengRobinson work for multiple species (ignore for now) - fugacities = [NaN for s = 1:nb_species] # Pa - if eos == :ideal - fugacities = pressures * 100000.0 # bar --> Pa - elseif eos == :PengRobinson - if nb_species != 1 - error("Peng Robinson EOS not implemented for >1 species") - end - prfluid = PengRobinsonFluid(molecule_templates[1].species) - gas_props = calculate_properties(prfluid, temperature, pressures[1], verbose=false) - fugacities[1] = gas_props["fugacity (bar)"] * 100000.0 # bar --> Pa - else - error("eos=:ideal and eos=:PengRobinson are the only valid options for an equation of state.") - end - if verbose - for s = 1:nb_species - @printf("\t%s equation of state, %s partial fugacity = %f bar\n", eos, - molecule_templates[s].species, fugacities[s] / 100000.0) - end - end - - ### - # replicate xtal so that nearest image convention can be applied for short-range interactions - ### - repfactors = replication_factors(xtal.box, ljff) - original_xtal_box = deepcopy(xtal.box) - xtal = replicate(xtal, repfactors) # frac coords still in [0, 1] - - ### - # prep molecules array - ### - if isnothing(molecules) - # populate the molecules array with an empty array for each species being simulated - molecules = [Molecule[] for i in 1:nb_species] - else - if length(molecules) != nb_species - error("Length of molecules array $(length(molecules)) is not equal to length of molecule_templates $(nb_species).\n") - end - end - # convert molecules array to fractional using this box. - molecules = [Frac.(molecules_species, xtal.box) for molecules_species in molecules] - - ### - # Density grid for adsorbate - # (if more than one adsorbate, user must specify which adsorbate species to make - # the grid for) - ### - if calculate_density_grid && isnothing(density_grid_molecular_species) - if nb_species == 1 - # obviously we are keeping track of the only atom in the adsorbate. - density_grid_molecular_species = molecule_templates[1].species - else - # cannot proceed if we do not know which molecule to keep track of! - error(@printf("Passed `calculate_density_grid=true` but there is %d - different adsorbates in the system. Pass `density_grid_molecular_species` to specify - which adsorbate to make a grid for.", nb_species) - ) - end - end - - if calculate_density_grid - # keep track of the id for the density_grid_molecular_species - molecule_species_id_for_density_grid = findfirst(density_grid_molecular_species .== molecular_species) - @assert ((molecule_species_id_for_density_grid != 0) && (molecule_species_id_for_density_grid <= length(molecule_templates))) "density_grid_molecular_species not found in molecule_templates" - end - - # Initialize a density grid based on the *simulation box* (not xtal box passed in) and the passed in density_grid_dx - # Calculate `n_pts`, number of voxels in grid, based on the sim box and specified voxel spacing - n_pts = (0, 0, 0) # don't store a huge grid if we aren't tracking a density grid - if calculate_density_grid - if density_grid_sim_box - n_pts = required_n_pts(xtal.box, density_grid_dx) - else - n_pts = required_n_pts(original_xtal_box, density_grid_dx) - end - end - density_grid = Grid(density_grid_sim_box ? xtal.box : original_xtal_box, n_pts, - zeros(n_pts...), :inverse_A3, [0.0, 0.0, 0.0]) - - if verbose - println("\tthe crystal:") - @printf("\t\treplicated (%d,%d,%d) for short-range cutoff of %f Å\n", - repfactors[1], repfactors[2], repfactors[3], - sqrt(ljff.r²_cutoff)) - println("\t\tdensity [kg/m³]: ", crystal_density(xtal)) - println("\t\tchemical formula: ", chemical_formula(xtal)) - println("\t\t# atoms: ", xtal.atoms.n) - println("\t\t# point charges: ", xtal.charges.n) - println("\tthe molecules:") - for s = 1:nb_species - println("\t", molecule_templates[s].species) - println("\t\tunique species: ", unique(molecule_templates[s].atoms.species)) - println("\t\t# atoms: ", molecule_templates[s].atoms.n) - println("\t\t# point charges: ", molecule_templates[s].charges.n) - end - if write_adsorbate_snapshots - @printf("\tWriting snapshots of adsorption positions every %d cycles (after burn cycles)\n", snapshot_frequency) - @printf("\t\tWriting to file: %s\n", xyz_snapshots_filename) - end - if calculate_density_grid - @printf("\tTracking adsorbate spatial probability density grid of adsorbate %s, updated every %d cycles (after burn cycles)\n", density_grid_molecular_species, snapshot_frequency) - @printf("\t\tdensity grid voxel spacing specified as %.3f Å => %d by %d by %d voxels\n", density_grid_dx, n_pts...) - if density_grid_sim_box - @printf("\t\tdensity grid is over simulation box\n") - else - @printf("\t\tdensity grid is over original crystal box\n") - end - end - end - if ! forcefield_coverage(xtal.atoms, ljff) - error("crystal $(xtal.name) not covered by the force field $(ljff.name)") - end - - for s = 1:nb_species - if ! neutral(molecule_templates[s].charges) - error(@sprintf("Molecule %s is not charge neutral!\n", molecule_templates[s].species)) - end - if ! forcefield_coverage(molecule_templates[s].atoms, ljff) - error("molecule $(molecule_templates[s].species) not covered by the force field $(ljff.name)") - end - end - - # TODO electrostatics - electrostatics_flag = has_charges(xtal) && any(has_charges.(molecule_templates)) - if electrostatics_flag - error("sorry, electrostatics not supported") - end - - # initiate system energy to which we increment when MC moves are accepted - system_energy = SystemPotentialEnergy() - # if we don't start with an emtpy xtal, compute energy of starting configuration - # (n=0 corresponds to zero energy) - if any(molecules_species -> length(molecules_species) != 0, molecules) - # some checks - for (s, molecules_species) in enumerate(molecules) - # ensure molecule template matches species of starting molecules. - @assert all(molecule -> molecule.species == molecule_templates[s].species, molecules_species) "initializing with wrong molecule species" - # assert that the molecules are inside the simulation box - @assert all(molecule -> inside(molecule), molecules_species) "initializing with molecules outside simulation box!" - # ensure pair-wise bond distance match template - @assert all(molecule -> ! distortion(molecule, Frac(molecule_templates[s], xtal.box), xtal.box), molecules_species) "initializing with distorted molecules" - end - - system_energy.gh.vdw = total_vdw_energy(xtal, molecules, ljff) - system_energy.gg.vdw = total_vdw_energy(molecules, ljff, xtal.box) - system_energy.gh.es = 0.0 #total(total_electrostatic_potential_energy(xtal, molecules, eparams, eikr_gh)) - system_energy.gg.es = 0.0 #total(electrostatic_potential_energy(molecules, eparams, xtal.box, eikr_gg)) - end - - if show_progress_bar - progress_bar = Progress(n_burn_cycles + n_sample_cycles, 1) - end - - #### - # MC proposal probabilities - #### - mc_proposal_probabilities = [0.0 for _ ∈ 1:N_PROPOSAL_TYPES] - # set defaults - mc_proposal_probabilities[INSERTION] = 0.35 - mc_proposal_probabilities[DELETION] = mc_proposal_probabilities[INSERTION] # must be equal - mc_proposal_probabilities[REINSERTION] = 0.05 - mc_proposal_probabilities[TRANSLATION] = 0.25 - if nb_species > 1 # multi-species - mc_proposal_probabilities[IDENTITY_CHANGE] = 0.1 - end - if any(needs_rotations.(molecule_templates)) # rotatable molecules - mc_proposal_probabilities[ROTATION] = 0.25 - end - # normalize - mc_proposal_probabilities /= sum(mc_proposal_probabilities) - # StatsBase.jl functionality for sampling - mc_proposal_probabilities = ProbabilityWeights(mc_proposal_probabilities) - if verbose - println("\tMarkov chain proposals:") - for p = 1:N_PROPOSAL_TYPES - @printf("\t\tprobability of %s: %f\n", - PROPOSAL_ENCODINGS[p], mc_proposal_probabilities[p]) - end - end - - # adaptive translation step - adaptive_δ = AdaptiveTranslationStepSize(2.0) # default is 2 Å - println("\t\ttranslation step size: ", adaptive_δ.δ, " Å") - - # initiate GCMC statistics for each block # break simulation into `N_BLOCKS` blocks to gauge convergence - gcmc_stats = [GCMCstats(nb_species) for block_no ∈ 1:N_BLOCKS] - current_block = 1 - # make sure the number of sample cycles is at least equal to N_BLOCKS - if n_sample_cycles < N_BLOCKS - n_sample_cycles = N_BLOCKS - @warn @sprintf("# sample cycles set to minimum %d, which is number of blocks.", N_BLOCKS) - end - N_CYCLES_PER_BLOCK = floor(Int, n_sample_cycles / N_BLOCKS) - - markov_counts = MarkovCounts(zeros(Int, length(PROPOSAL_ENCODINGS)), zeros(Int, length(PROPOSAL_ENCODINGS))) - - # (n_burn_cycles + n_sample_cycles) is number of outer cycles (MC cycles). - # for each outer cycle, peform max(20, # molecules in the system) MC proposals. - markov_chain_time = 0 - outer_cycle_start = 1 - for outer_cycle = outer_cycle_start:(n_burn_cycles + n_sample_cycles) - if show_progress_bar - next!(progress_bar; showvalues=[(:cycle, outer_cycle), (:number_of_molecules, sum(length.(molecules)))]) - end - for inner_cycle ∈ 1:(sum(length.(molecules)) + 1) - markov_chain_time += 1 - - # choose a species, find current # molecules of that species, n_i - which_species = rand(1:nb_species) - n_i = length(molecules[which_species]) - - # choose proposed move randomly; keep track of proposals - which_move = sample(1:N_PROPOSAL_TYPES, mc_proposal_probabilities) # StatsBase.jl - markov_counts.n_proposed[which_move] += 1 - - if which_move == INSERTION - random_insertion!(molecules[which_species], xtal.box, molecule_templates[which_species]) - - # inserted molecule pushed to end of molecules[which_species] array - # note: length(molecules[which_species]) != n_i here (rather, it is n_i + 1) - ΔE = potential_energy(which_species, n_i + 1, molecules, xtal, ljff) - - # Metropolis Hastings Acceptance for Insertion - if rand() < fugacities[which_species] * xtal.box.Ω / ((n_i + 1) * BOLTZMANN * - temperature) * exp(-sum(ΔE) / temperature) - # accept the move, adjust current_energy - markov_counts.n_accepted[which_move] += 1 - - system_energy += ΔE - else - # reject the move, remove the inserted molecule - pop!(molecules[which_species]) - end - elseif (which_move == DELETION) && (n_i != 0) - # propose which molecule to delete - molecule_id = rand(eachindex(molecules[which_species])) - - # compute the potential energy of the molecule we propose to delete - ΔE = potential_energy(which_species, molecule_id, molecules, xtal, ljff) - - # Metropolis Hastings Acceptance for Deletion - if rand() < n_i * BOLTZMANN * temperature / (fugacities[which_species] * - xtal.box.Ω) * exp(sum(ΔE) / temperature) - # accept the deletion, delete molecule, adjust current_energy - markov_counts.n_accepted[which_move] += 1 - - remove_molecule!(molecule_id, molecules[which_species]) - - system_energy -= ΔE - end - elseif (which_move == TRANSLATION) && (n_i != 0) - adaptive_δ.nb_translations_proposed += 1 - # propose which molecule whose coordinates we should perturb - molecule_id = rand(eachindex(molecules[which_species])) - - # energy of the molecule before it was translated - energy_old = potential_energy(which_species, molecule_id, molecules, xtal, ljff) - - old_molecule = random_translation!(molecules[which_species][molecule_id], xtal.box, adaptive_δ) - - # energy of the molecule after it is translated - energy_new = potential_energy(which_species, molecule_id, molecules, xtal, ljff) - - # Metropolis Hastings Acceptance for translation - if rand() < exp(-(sum(energy_new) - sum(energy_old)) / temperature) - # accept the move, adjust current energy - markov_counts.n_accepted[which_move] += 1 - adaptive_δ.nb_translations_accepted += 1 - - system_energy += energy_new - energy_old - else - # reject the move, put back the old molecule - molecules[which_species][molecule_id] = deepcopy(old_molecule) - end - elseif (which_move == ROTATION) && needs_rotations(molecule_templates[which_species]) && (n_i != 0) - # propose which molecule to rotate - molecule_id = rand(eachindex(molecules[which_species])) - - # energy of the molecule before we rotate it - energy_old = potential_energy(which_species, molecule_id, molecules, xtal, ljff) - - # store old molecule to restore old position in case move is rejected - old_molecule = deepcopy(molecules[which_species][molecule_id]) - - # conduct a random rotation - random_rotation!(molecules[which_species][molecule_id], xtal.box) - - # energy of the molecule after it is translated - energy_new = potential_energy(which_species, molecule_id, molecules, xtal, ljff) - - # Metropolis Hastings Acceptance for rotation - if rand() < exp(-(sum(energy_new) - sum(energy_old)) / temperature) - # accept the move, adjust current energy - markov_counts.n_accepted[which_move] += 1 - - system_energy += energy_new - energy_old - else - # reject the move, put back the old molecule - molecules[which_species][molecule_id] = deepcopy(old_molecule) - end - elseif (which_move == REINSERTION) && (n_i != 0) - # propose which molecule to re-insert - molecule_id = rand(eachindex(molecules[which_species])) - - # compute the potential energy of the molecule we propose to re-insert - energy_old = potential_energy(which_species, molecule_id, molecules, xtal, ljff) - - # reinsert molecule; store old configuration of the molecule in case proposal is rejected - old_molecule = random_reinsertion!(molecules[which_species][molecule_id], xtal.box) - - # compute the potential energy of the molecule in its new configuraiton - energy_new = potential_energy(which_species, molecule_id, molecules, xtal, ljff) - - # Metropolis Hastings Acceptance for reinsertion - if rand() < exp(-(sum(energy_new) - sum(energy_old)) / temperature) - # accept the move, adjust current energy - markov_counts.n_accepted[which_move] += 1 - - system_energy += energy_new - energy_old - else - # reject the move, put back old molecule - molecules[which_species][molecule_id] = deepcopy(old_molecule) - end - elseif (which_move == IDENTITY_CHANGE) && (n_i != 0) - ### - # IDENTITY_CHANGE Procedure - # 1. determine which molecule of a given species to propose identity change - # 2. calculate the energy of that molecule - # 3. remove that molecule from the system, but keep a copy in case of rejection - # 4. select which molecule of a different species is going to replace original molecule - # 5. insert new (trial) molecule at location (with random orientation) of the original molecule - # 6. calculate the energy of the new molecule - # 7. evaluate acceptance rule: accept or reject proposed identity change - # 8. accept: keep new molecule at current location - # reject: remove trial molecule and reinsert the copy of original molecule - # - # NOTE: make sure that the encodings for proposals are updated and that the statistics are updated - ### - # determine which molecule of which_species to propose identity change - molecule_id = rand(1:n_i) - - # calculate the energy of that molecule - energy_old = potential_energy(which_species, molecule_id, molecules, xtal, ljff) - - # remove that molecule from the system, but keep a copy in case of rejection - old_molecule = deepcopy(molecules[which_species][molecule_id]) - remove_molecule!(molecule_id, molecules[which_species]) - - # select which molecule of a different species is going to replace original molecule - candidate_species = rand([sp for sp in 1:nb_species if sp != which_species]) - - # get the current number of molecules of type candidate_species - n_j = length(molecules[candidate_species]) - - # insert trial molecule, with random orientation, at location of the original molecule - insert_w_random_orientation!(molecules[candidate_species], xtal.box, molecule_templates[candidate_species], old_molecule.com) - - # calculate the energy of the new molecule - energy_new = potential_energy(candidate_species, n_j + 1, molecules, xtal, ljff) - - # Acceptance rule for identity change - if rand() < (n_i * fugacities[candidate_species] / ((n_j + 1) * fugacities[which_species])) * - exp(-(sum(energy_new) - sum(energy_old)) / temperature) - # accept the move, adjust current energy - markov_counts.n_accepted[which_move] += 1 - - system_energy += energy_new - energy_old - else - # reject the move: remove trial molecule and reinsert original - remove_molecule!(n_j + 1, molecules[candidate_species]) - - push!(molecules[which_species], deepcopy(old_molecule)) - end - end # which move the code executes - - # if we've done all burn cycles, take samples for statistics - if outer_cycle > n_burn_cycles # then we're in "production" cycles. - if markov_chain_time % sample_frequency == 0 - gcmc_stats[current_block].n_samples += 1 - - gcmc_stats[current_block].n += length.(molecules) - gcmc_stats[current_block].n² += length.(molecules) .^ 2 - - gcmc_stats[current_block].U += system_energy - gcmc_stats[current_block].U² += square(system_energy) - - gcmc_stats[current_block].Un += sum(system_energy) * sum(length.(molecules)) - end - end - end # inner cycles - - # update adaptive step 12 times during burn cycles - if (outer_cycle <= n_burn_cycles) && (outer_cycle % floor(Int, n_burn_cycles / 12) == 0) - adjust!(adaptive_δ) - end - - # print block statistics / increment block - if (outer_cycle > n_burn_cycles) && (current_block != N_BLOCKS) && ( - (outer_cycle - n_burn_cycles) % N_CYCLES_PER_BLOCK == 0) - # move onto new block unless current_block is N_BLOCKS; - # then just keep adding stats to the last block. - # this only occurs if sample_cycles not divisible by N_BLOCKS - # print GCMC stats later and do not increment block if we are in last block. - # print statistics for this block - if verbose - printstyled(@sprintf("\tBlock %d/%d statistics:\n", current_block, N_BLOCKS); color=:yellow) - print(gcmc_stats[current_block]) - end - current_block += 1 - end - # print the last cycle in the last block - if outer_cycle == (n_sample_cycles + n_burn_cycles) - if verbose - printstyled(@sprintf("\tBlock %d/%d statistics:\n", current_block, N_BLOCKS); color=:yellow) - print(gcmc_stats[current_block]) - end - end - - # snapshot cycle - if (outer_cycle > n_burn_cycles) && (outer_cycle % snapshot_frequency == 0) - if write_adsorbate_snapshots - # have a '\n' for every new set of atoms, leaves no '\n' at EOF - if num_snapshots > 0 - @printf(xyz_snapshot_file, "\n") - end - write_xyz(xtal.box, molecules, xyz_snapshot_file) - end - if calculate_density_grid - if density_grid_sim_box - update_density!(density_grid, molecules[molecule_species_id_for_density_grid], density_grid_molecular_species) - else - update_density!(density_grid, Cart.(molecules[molecule_species_id_for_density_grid], xtal.box), density_grid_molecular_species) - end - end - num_snapshots += 1 - end - end # outer cycles - # finished MC moves at this point. - - # close snapshot xyz file - close(xyz_snapshot_file) - - if calculate_density_grid - # divide number of molecules in a given voxel by total snapshots - density_grid.data ./= num_snapshots - end - - # checks - for (s, molecules_species) in enumerate(molecules) - @assert all(molecule -> molecule.species == molecule_templates[s].species, molecules_species) "species got mixed up" - @assert all(molecule -> inside(molecule), molecules_species) "molecule outside box!" - @assert all([! distortion(molecule, Frac(molecule_templates[s], xtal.box), xtal.box, atol=1e-10) for molecule in molecules_species]) "molecule distorted" - end - - # compute total energy, compare to `current_energy*` variables where were incremented - system_energy_end = SystemPotentialEnergy() - system_energy_end.gh.vdw = total_vdw_energy(xtal, molecules, ljff) - system_energy_end.gg.vdw = total_vdw_energy(molecules, ljff, xtal.box) - system_energy_end.gh.es = 0.0 # total(total_electrostatic_potential_energy(xtal, molecules, eparams, eikr_gh)) - system_energy_end.gg.es = 0.0 # total(total_electrostatic_potential_energy(molecules, eparams, xtal.box, eikr_gg)) - - # see Energetics_Util.jl for this function, overloaded isapprox to print mismatch - if ! isapprox(system_energy, system_energy_end, verbose=true, atol=0.01) - error("energy incremented improperly during simulation...") - end - - @assert (markov_chain_time == sum(markov_counts.n_proposed)) - elapsed_time = time() - start_time - if verbose - @printf("\tEstimated elapsed time: %d seconds\n", elapsed_time) - println("\tTotal # MC steps: ", markov_chain_time) - end - - # build dictionary containing summary of simulation results for easy querying - results = Dict{String, Any}() - results["xtal"] = xtal.name - results["adsorbate"] = molecular_species - results["forcefield"] = ljff.name - results["pressure (bar)"] = pressures - results["fugacity (bar)"] = fugacities / 100000.0 - results["temperature (K)"] = temperature - results["repfactors"] = repfactors - - results["# sample cycles"] = n_sample_cycles - results["# burn cycles"] = n_burn_cycles - results["# samples"] = sum(gcmc_stats).n_samples - results["elapsed time (min)"] = elapsed_time / 60 - - # statistics from samples during simulation - # see here: https://cs.nyu.edu/courses/fall06/G22.2112-001/MonteCarlo.pdf for how - # error bars are computed; simulation broken into N_BLOCKS and each average from the - # block is treated as an independent sample. - avg_n, err_n, avg_U, err_U = mean_stderr_n_U(gcmc_stats) - - # averages - results["⟨N⟩ (molecules)"] = avg_n - results["⟨U_gh, vdw⟩ (K)"] = avg_U.gh.vdw - results["⟨U_gh, electro⟩ (K)"] = avg_U.gh.es - results["⟨U_gg, vdw⟩ (K)"] = avg_U.gg.vdw - results["⟨U_gg, electro⟩ (K)"] = avg_U.gg.es - results["⟨U⟩ (K)"] = sum(avg_U) - - # variances - results["var(N)"] = (sum(gcmc_stats).n² / sum(gcmc_stats).n_samples) - (results["⟨N⟩ (molecules)"] .^ 2) - - # isosteric heat of adsorption TODO stdev of this too. - results["Q_st (K)"] = temperature .- (sum(gcmc_stats).Un / sum(gcmc_stats).n_samples .- results["⟨U⟩ (K)"] * results["⟨N⟩ (molecules)"]) ./ results["var(N)"] - - # error bars (confidence intervals) - results["err ⟨N⟩ (molecules)"] = err_n - results["err ⟨U_gh, vdw⟩ (K)"] = err_U.gh.vdw - results["err ⟨U_gh, electro⟩ (K)"] = err_U.gh.es - results["err ⟨U_gg, vdw⟩ (K)"] = err_U.gg.vdw - results["err ⟨U_gg, electro⟩ (K)"] = err_U.gg.es - results["err ⟨U⟩ (K)"] = sum(err_U) - - # average N in more common units - results["⟨N⟩ (molecules/unit cell)"] = avg_n / (repfactors[1] * repfactors[2] * repfactors[3]) - results["err ⟨N⟩ (molecules/unit cell)"] = err_n / (repfactors[1] * repfactors[2] * repfactors[3]) - # (molecules/unit cell) * (mol/6.02 * 10^23 molecules) * (1000 mmol/mol) * - # (unit cell/xtal amu) * (amu/ 1.66054 * 10^-24) - results["⟨N⟩ (mmol/g)"] = results["⟨N⟩ (molecules/unit cell)"] * 1000 / - (6.022140857e23 * molecular_weight(xtal) * 1.66054e-24) * (repfactors[1] * repfactors[2] * repfactors[3]) - results["err ⟨N⟩ (mmol/g)"] = results["err ⟨N⟩ (molecules/unit cell)"] * 1000 / - (6.022140857e23 * molecular_weight(xtal) * 1.66054e-24) * (repfactors[1] * repfactors[2] * repfactors[3]) - - # Markov stats - for (proposal_id, proposal_description) in PROPOSAL_ENCODINGS - results[@sprintf("Total # %s proposals", proposal_description)] = markov_counts.n_proposed[proposal_id] - results[@sprintf("Fraction of %s proposals accepted", proposal_description)] = markov_counts.n_accepted[proposal_id] / markov_counts.n_proposed[proposal_id] - end - - # Snapshot information - results["density grid"] = deepcopy(density_grid) - results["num snapshots"] = num_snapshots - - if verbose - print_results(results, print_title=false) - end - - # return molecules in Cartesian format - molecules = [Cart.(mols, xtal.box) for mols in molecules] - - if autosave - if ! isdir(rc[:paths][:simulations]) - mkdir(rc[:paths][:simulations]) - end - - save_results_filename = joinpath(rc[:paths][:simulations], - μVT_output_filename(xtal, molecule_templates, temperature, pressures, - ljff, n_burn_cycles, n_sample_cycles, - comment=results_filename_comment - ) - ) - - @save save_results_filename results - if verbose - println("\tresults dictionary saved in ", save_results_filename) - end - end - - return results, molecules # summary of statistics and ending configuration of molecules -end # μVT_sim - -# overload function for backward compatibility -μVT_sim(xtal::Crystal, molecule_template::Molecule{Cart}, temperature::Float64, pressure::Float64, ljff::LJForceField; kwargs...) = μVT_sim(xtal, [molecule_template], temperature, [pressure], ljff; kwargs...) - -""" - filename = μVT_output_filename(xtal, molecule_templates, temperature, - pressures, ljff, n_burn_cycles, - n_sample_cycles; comment="", extension=".jld2") - -This is the function that establishes the file naming convention used by [μVT_sim](@ref). - -# Arguments -- `xtal::Crystal`: porous xtal used in adsorption simulation -- `molecule_templates::Array{Molecule, 1}`: template of the adsorbate molecules used in adsorption simulation -- `temperature::Float64`:temperature of bulk gas phase in equilibrium with adsorbed phase in - the porous material. units: Kelvin (K) -- `pressures::Array{Float64, 1}`: partial pressures of bulk gas phase in equilibrium with adsorbed phase in the - porous material. units: bar -- `ljff::LJForceField`: the molecular model used in adsorption simulation -- `n_burn_cycles::Int`: number of cycles to allow the system to reach equilibrium before sampling. -- `n_sample_cycles::Int`: number of cycles used for sampling -- `comment::String=""`: remarks to be included in the filename -- `extension::String=".jld2"`: the file extension - -# Returns -- `filename::String`: the name of the specific `.jld2` simulation file -""" -function μVT_output_filename(xtal::Crystal, molecule_templates::Array{Molecule{Cart}, 1}, temperature::Float64, - pressures::Array{Float64, 1}, ljff::LJForceField, n_burn_cycles::Int, - n_sample_cycles::Int; comment::String="", extension::String=".jld2") - filename = @sprintf("muVT_xtal_%s_T_%.3fK", xtal.name, temperature) - for m = 1:length(molecule_templates) - filename *= @sprintf("_%s_P_%.6fbar", molecule_templates[m].species, pressures[m]) - end - filename *= @sprintf("_%s_%dburn_%dsample", ljff.name, n_burn_cycles, n_sample_cycles) - return filename * comment * extension -end - - function print_results(results::Dict; print_title::Bool=true) if print_title # already print in GCMC tests... - @printf("GCMC simulation of %s in %s at %f K and %f bar pressure, %f bar fugacity using %s forcefield.\n\n", - results["adsorbate"], results["xtal"], results["temperature (K)"], - results["pressure (bar)"], results["fugacity (bar)"] / 100000.0, results["forcefield"]) - end - - @printf("\nUnit cell replication factors: %d %d %d\n\n", results["repfactors"][1], - results["repfactors"][2], - results["repfactors"][3]) + @printf( + "GCMC simulation of %s in %s at %f K and %f bar pressure, %f bar fugacity using %s forcefield.\n\n", + results["adsorbate"], + results["xtal"], + results["temperature (K)"], + results["pressure (bar)"], + results["fugacity (bar)"] / 100000.0, + results["forcefield"] + ) + end + + @printf( + "\nUnit cell replication factors: %d %d %d\n\n", + results["repfactors"][1], + results["repfactors"][2], + results["repfactors"][3] + ) # Markov stats println("") for key in ["# sample cycles", "# burn cycles", "# samples"] @@ -858,7 +126,8 @@ function print_results(results::Dict; print_title::Bool=true) for (_, proposal_description) in PROPOSAL_ENCODINGS total_proposals = results[@sprintf("Total # %s proposals", proposal_description)] - fraction_accepted = results[@sprintf("Fraction of %s proposals accepted", proposal_description)] + fraction_accepted = + results[@sprintf("Fraction of %s proposals accepted", proposal_description)] if total_proposals > 0 printstyled(proposal_description; color=:yellow) @printf("\t%d total proposals.\n", total_proposals) @@ -877,8 +146,13 @@ function print_results(results::Dict; print_title::Bool=true) end end - for key in ["⟨U_gg, vdw⟩ (K)", "⟨U_gh, vdw⟩ (K)", "⟨U_gg, electro⟩ (K)", - "⟨U_gh, electro⟩ (K)", "⟨U⟩ (K)"] + for key in [ + "⟨U_gg, vdw⟩ (K)", + "⟨U_gh, vdw⟩ (K)", + "⟨U_gg, electro⟩ (K)", + "⟨U_gh, electro⟩ (K)", + "⟨U⟩ (K)" + ] @printf("%s: %f +/- %f\n", key, results[key], results["err " * key]) if key == "⟨N⟩ (mmol/g)" println("") @@ -886,36 +160,41 @@ function print_results(results::Dict; print_title::Bool=true) end for s in 1:length(results["adsorbate"]) - @printf("\nQ_st (K) = %f = %f kJ/mol\n\n", results["Q_st (K)"][s], results["Q_st (K)"][s] * 8.314 / 1000.0) + @printf( + "\nQ_st (K) = %f = %f kJ/mol\n\n", + results["Q_st (K)"][s], + results["Q_st (K)"][s] * 8.314 / 1000.0 + ) end return end -function pretty_print(xtal::Crystal, - molecule_templates::Array{Molecule{Cart}, 1}, - temperature::Float64, - pressures::Array{Float64, 1}, - ljff::LJForceField) +function pretty_print( + xtal::Crystal, + molecule_templates::Array{Molecule{Cart}, 1}, + temperature::Float64, + pressures::Array{Float64, 1}, + ljff::LJForceField +) printstyled("μVT simulation\n"; color=:yellow) print("crystal: ") printstyled(xtal.name; color=:green) - + print("\ntemperature: ") printstyled(@sprintf("%f K\n", temperature); color=:green) println("\nadsorbates:") - for m = 1:length(molecule_templates) + for m in 1:length(molecule_templates) printstyled(molecule_templates[m].species; color=:yellow) print("\tpartial pressure = ") printstyled(@sprintf("%f bar\n", pressures[m]); color=:green) end println(" force field.") - printstyled(split(ljff.name, ".")[1]; color=:green) + return printstyled(split(ljff.name, ".")[1]; color=:green) end - """ results = stepwise_adsorption_isotherm(xtal, molecule_templates, temperature, pressures, ljff; n_sample_cycles=5000, @@ -937,12 +216,14 @@ differ significantly from the previous pressure), we can reduce the number of bu required to reach equilibrium in the Monte Carlo simulation. Also see [`adsorption_isotherm`](@ref) which runs the μVT simulation at each pressure in parallel. """ -function stepwise_adsorption_isotherm(xtal::Crystal, - molecule_templates::Array{Molecule{Cart}, 1}, - temperature::Float64, - pressures::Array{Array{Float64, 1}, 1}, - ljff::LJForceField; - kwargs...) +function stepwise_adsorption_isotherm( + xtal::Crystal, + molecule_templates::Array{Molecule{Cart}, 1}, + temperature::Float64, + pressures::Array{Array{Float64, 1}, 1}, + ljff::LJForceField; + kwargs... +) # simulation only works if xtal is in P1 assert_P1_symmetry(xtal) @@ -954,15 +235,16 @@ function stepwise_adsorption_isotherm(xtal::Crystal, results = Dict{String, Any}[] # push results to this array molecules = nothing # initialize with empty xtal - for pressure ∈ pressures - result, molecules = μVT_sim(xtal, - molecule_templates, - temperature, - pressure, - ljff; - molecules=molecules, # essential step here - kwargs... - ) + for pressure in pressures + result, molecules = μVT_sim( + xtal, + molecule_templates, + temperature, + pressure, + ljff; + molecules=molecules, # essential step here + kwargs... + ) push!(results, result) end @@ -970,11 +252,24 @@ function stepwise_adsorption_isotherm(xtal::Crystal, end # overload function for backward compatibility -function stepwise_adsorption_isotherm(xtal::Crystal, molecule_templates::Molecule{Cart}, temperature::Float64, pressures::Array{Float64, 1}, ljff::LJForceField; kwargs...) - return stepwise_adsorption_isotherm(xtal, [molecule_templates], temperature, [[p] for p in pressures], ljff; kwargs...) +function stepwise_adsorption_isotherm( + xtal::Crystal, + molecule_templates::Molecule{Cart}, + temperature::Float64, + pressures::Array{Float64, 1}, + ljff::LJForceField; + kwargs... +) + return stepwise_adsorption_isotherm( + xtal, + [molecule_templates], + temperature, + [[p] for p in pressures], + ljff; + kwargs... + ) end - """ results = adsorption_isotherm(xtal, molecule_templates, temperature, partial_pressures, @@ -993,12 +288,14 @@ The only exception is that we pass an array of pressures, and we only consider a To give Julia access to multiple cores, run your script as `julia -p 4 mysim.jl` to allocate e.g. four cores. See [Parallel Computing](https://docs.julialang.org/en/stable/manual/parallel-computing/#Parallel-Computing-1). """ -function adsorption_isotherm(xtal::Crystal, - molecule_templates::Array{Molecule{Cart}, 1}, - temperature::Float64, - pressures::Array{Array{Float64, 1}, 1}, - ljff::LJForceField; - kwargs...) +function adsorption_isotherm( + xtal::Crystal, + molecule_templates::Array{Molecule{Cart}, 1}, + temperature::Float64, + pressures::Array{Array{Float64, 1}, 1}, + ljff::LJForceField; + kwargs... +) # simulation only works if xtal is in P1 assert_P1_symmetry(xtal) @@ -1009,24 +306,45 @@ function adsorption_isotherm(xtal::Crystal, end # make a function of pressure only to facilitate uses of `pmap` - run_pressure(partial_pressures::Array{Float64, 1}) = μVT_sim(xtal, molecule_templates, temperature, - partial_pressures, ljff; - kwargs...)[1] # only return results + function run_pressure(partial_pressures::Array{Float64, 1}) + return μVT_sim( + xtal, + molecule_templates, + temperature, + partial_pressures, + ljff; + kwargs... + )[1] # only return results + end # only return results # for load balancing, larger pressures with longer computation time goes first - ids = sortperm(pressures, rev=true) + ids = sortperm(pressures; rev=true) # run gcmc simulations in parallel using Julia's pmap parallel computing function results = pmap(run_pressure, pressures[ids]) # return results in same order as original pressure even though we permuted them for # better load balancing. - return results[[findall(x -> x==i, ids)[1] for i = eachindex(ids)]] + return results[[findall(x -> x == i, ids)[1] for i in eachindex(ids)]] end # overload function for backward compatibility -function adsorption_isotherm(xtal::Crystal, molecule_templates::Molecule{Cart}, temperature::Float64, pressures::Array{Float64, 1}, ljff::LJForceField; kwargs...) - return adsorption_isotherm(xtal, [molecule_templates], temperature, [[p] for p in pressures], ljff; kwargs...) +function adsorption_isotherm( + xtal::Crystal, + molecule_templates::Molecule{Cart}, + temperature::Float64, + pressures::Array{Float64, 1}, + ljff::LJForceField; + kwargs... +) + return adsorption_isotherm( + xtal, + [molecule_templates], + temperature, + [[p] for p in pressures], + ljff; + kwargs... + ) end """ @@ -1038,28 +356,31 @@ end convert the `.jld2` results output files auto-saved from [`μVT_sim`](@ref) into a `DataFrame`. each row of the `DataFrame` corresponds to a pressure in the adsorption isotherm. -`desired_props` is an array of desired properties from the results. +`desired_props` is an array of desired properties from the results. to locate the requested output files, this function calls [`μVT_output_filename`](@ref) to retrieve the file names. # Arguments -- `desired_props::Array{String, 1}`: An array containing names of properties to be retrieved from the `.jld2` file -- `xtal::Crystal`: The porous crystal -- `molecule::Array{Molecule{Cart}, 1}`: The adsorbate molecule -- `temperature::Float64`: The temperature in the simulation, units: Kelvin (K) -- `pressures::Array{Array{Float64, 1}, 1}`: The pressures in the adsorption isotherm simulation(s), units: bar -- `ljff::LJForceField`: The molecular model being used in the simulation to describe intermolecular Van - der Waals interactions -- `n_burn_cycles::Int64`: The number of burn cycles used in this simulation -- `n_sample_cycles::Int64`: The number of sample cycles used in the simulations -- `where_are_jld_files::Union{String, Nothing}=nothing`: The location to the simulation files. defaults to + + - `desired_props::Array{String, 1}`: An array containing names of properties to be retrieved from the `.jld2` file + - `xtal::Crystal`: The porous crystal + - `molecule::Array{Molecule{Cart}, 1}`: The adsorbate molecule + - `temperature::Float64`: The temperature in the simulation, units: Kelvin (K) + - `pressures::Array{Array{Float64, 1}, 1}`: The pressures in the adsorption isotherm simulation(s), units: bar + - `ljff::LJForceField`: The molecular model being used in the simulation to describe intermolecular Van + der Waals interactions + - `n_burn_cycles::Int64`: The number of burn cycles used in this simulation + - `n_sample_cycles::Int64`: The number of sample cycles used in the simulations + - `where_are_jld_files::Union{String, Nothing}=nothing`: The location to the simulation files. defaults to `PorousMaterials.rc[:paths][:simulations]`. -- `comment::String=""`: comment appended to outputfilename + - `comment::String=""`: comment appended to outputfilename # Returns -- `dataframe::DataFrame`: A `DataFrame` containing the simulated data along the adsorption isotherm, whose + + - `dataframe::DataFrame`: A `DataFrame` containing the simulated data along the adsorption isotherm, whose columns are for the specified properties # Note + A range of pressures can be used to select a batch of simulation files to be included in the `DataFrame`. # Example @@ -1067,27 +388,36 @@ A range of pressures can be used to select a batch of simulation files to be inc ```julia xtal = Crystal("SBMOF-1.cif") molecule = Molecule("Xe") -ljff = LJForceField("UFF", mixing_rules="Lorentz-Berthelot") +ljff = LJForceField("UFF"; mixing_rules="Lorentz-Berthelot") temperature = 298.0 # K -pressures = 10 .^ range(-2, stop=2, length=15) - -dataframe = isotherm_sim_results_to_dataframe(["pressure (bar)", "⟨N⟩ (mmol/g)"], - xtal, molecule, temperature, - pressures, ljff, 10000, 10000) +pressures = 10 .^ range(-2; stop=2, length=15) + +dataframe = isotherm_sim_results_to_dataframe( + ["pressure (bar)", "⟨N⟩ (mmol/g)"], + xtal, + molecule, + temperature, + pressures, + ljff, + 10000, + 10000 +) dataframe[Symbol("pressure (bar)")] # pressures dataframe[Symbol("⟨N⟩ (mmol/g)")] # adsorption at corresponding pressures ``` """ -function isotherm_sim_results_to_dataframe(desired_props::Array{String, 1}, - xtal::Crystal, - molecule_templates::Array{Molecule{Cart}, 1}, - temperature::Float64, - pressures::Array{Array{Float64, 1}, 1}, - ljff::LJForceField, - n_burn_cycles::Int64, - n_sample_cycles::Int64; - comment::String="", - where_are_jld_files::Union{String, Nothing}=nothing) +function isotherm_sim_results_to_dataframe( + desired_props::Array{String, 1}, + xtal::Crystal, + molecule_templates::Array{Molecule{Cart}, 1}, + temperature::Float64, + pressures::Array{Array{Float64, 1}, 1}, + ljff::LJForceField, + n_burn_cycles::Int64, + n_sample_cycles::Int64; + comment::String="", + where_are_jld_files::Union{String, Nothing}=nothing +) # determine the location of the data files if isnothing(where_are_jld_files) where_are_jld_files = rc[:paths][:simulations] @@ -1096,16 +426,27 @@ function isotherm_sim_results_to_dataframe(desired_props::Array{String, 1}, df = DataFrame() # loop over pressures and populate dataframe for (i, pressure) in enumerate(pressures) - jld2_filename = μVT_output_filename(xtal, molecule_templates, temperature, - pressure, ljff, n_burn_cycles, - n_sample_cycles, comment=comment) + jld2_filename = μVT_output_filename( + xtal, + molecule_templates, + temperature, + pressure, + ljff, + n_burn_cycles, + n_sample_cycles; + comment=comment + ) # load in the results as a dictionary @load joinpath(where_are_jld_files, jld2_filename) results - + # population column names, taking into account types if i == 1 for col in desired_props - insertcols!(df, length(names(df)) + 1, Symbol(col) => typeof(results[col])[]) + insertcols!( + df, + length(names(df)) + 1, + Symbol(col) => typeof(results[col])[] + ) end end push!(df, [results[prop] for prop in desired_props]) @@ -1114,15 +455,26 @@ function isotherm_sim_results_to_dataframe(desired_props::Array{String, 1}, end # overload function for backward compatibility -function isotherm_sim_results_to_dataframe(desired_props::Array{String, 1}, - xtal::Crystal, - molecule_templates::Molecule{Cart}, - temperature::Float64, - pressures::Array{Float64, 1}, - ljff::LJForceField, - n_burn_cycles::Int64, - n_sample_cycles::Int64; - kwargs...) - - return isotherm_sim_results_to_dataframe(desired_props, xtal, [molecule_templates], temperature, [[p] for p in pressures], ljff, n_burn_cycles, n_sample_cycles; kwargs...) +function isotherm_sim_results_to_dataframe( + desired_props::Array{String, 1}, + xtal::Crystal, + molecule_templates::Molecule{Cart}, + temperature::Float64, + pressures::Array{Float64, 1}, + ljff::LJForceField, + n_burn_cycles::Int64, + n_sample_cycles::Int64; + kwargs... +) + return isotherm_sim_results_to_dataframe( + desired_props, + xtal, + [molecule_templates], + temperature, + [[p] for p in pressures], + ljff, + n_burn_cycles, + n_sample_cycles; + kwargs... + ) end diff --git a/src/generic_rotations.jl b/src/generic_rotations.jl index 198ed5c7e..5a0dc9c66 100644 --- a/src/generic_rotations.jl +++ b/src/generic_rotations.jl @@ -7,26 +7,30 @@ Determine the 3D rotation matrix to rotate an angle θ (radians) about axis `u`. See [Wikipedia](https://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle). # Arguments -- `θ::Float64`: angle to rotate about an axis, in radians -- `u::Array{Float64, 1}`: axis about which to rotate -- `dim::Int`: 1, 2, 3 for rotation about x-, y-, or z-axis, respectively. -- `assume_unit_vector::Bool`: assume `u` is a unit vector; otherwise, `u` will be normalized -internal to this function. + + - `θ::Float64`: angle to rotate about an axis, in radians + - `u::Array{Float64, 1}`: axis about which to rotate + - `dim::Int`: 1, 2, 3 for rotation about x-, y-, or z-axis, respectively. + - `assume_unit_vector::Bool`: assume `u` is a unit vector; otherwise, `u` will be normalized + internal to this function. # Returns -- `R::Array{Float64, 2}`: 3D rotation matrix. so `R * x` will rotate vector `x` as desired. + + - `R::Array{Float64, 2}`: 3D rotation matrix. so `R * x` will rotate vector `x` as desired. """ function rotation_matrix(θ::Float64, u::Array{Float64, 1}; assume_unit_vector::Bool=false) - if ! assume_unit_vector + if !assume_unit_vector u = u / norm(u) end c = cos(θ) # for speed pre-compute these s = sin(θ) - R = [c + u[1] ^ 2 * (1.0 - c) u[1] * u[2] * (1.0 - c) - u[3] * s u[1] * u[3] * (1.0 - c) + u[2] * s; - u[2] * u[1] * (1.0 - c) + u[3] * s c + u[2] ^ 2 * (1.0 - c) u[2] * u[3] * (1.0 - c) - u[1] * s; - u[3] * u[1] * (1.0 - c) - u[2] * s u[3] * u[2] * (1.0 - c) + u[1] * s c + u[3] ^ 2 * (1.0 - c)] + R = [ + c+u[1]^2 * (1.0 - c) u[1] * u[2] * (1.0 - c)-u[3] * s u[1] * u[3] * (1.0 - c)+u[2] * s + u[2] * u[1] * (1.0 - c)+u[3] * s c+u[2]^2 * (1.0 - c) u[2] * u[3] * (1.0 - c)-u[1] * s + u[3] * u[1] * (1.0 - c)-u[2] * s u[3] * u[2] * (1.0 - c)+u[1] * s c+u[3]^2 * (1.0 - c) + ] return R end @@ -34,20 +38,26 @@ end function rotation_matrix(θ::Float64, dim::Int) c = cos(θ) # for speed pre-compute these s = sin(θ) - + # see https://en.wikipedia.org/wiki/Rotation_matrix#Basic_rotations if dim == 1 - return [1.0 0.0 0.0; - 0.0 c -s; - 0.0 s c] + return [ + 1.0 0.0 0.0 + 0.0 c -s + 0.0 s c + ] elseif dim == 2 - return [ c 0.0 s; - 0.0 1.0 0.0; - -s 0.0 c] + return [ + c 0.0 s + 0.0 1.0 0.0 + -s 0.0 c + ] elseif dim == 3 - return [ c -s 0.0; - s c 0.0; - 0.0 0.0 1.0] + return [ + c -s 0.0 + s c 0.0 + 0.0 0.0 1.0 + ] else error("dim must be 1, 2, or 3\n") end diff --git a/src/grid.jl b/src/grid.jl index b2bb8737a..2dda1267d 100644 --- a/src/grid.jl +++ b/src/grid.jl @@ -3,11 +3,12 @@ Data structure for a regular [equal spacing between points in each coordinate] g Each grid point has data, `data`, associated with it, of type `T`, stored in a 3D array. # Attributes -- `box::Box`: describes Bravais lattice over which a grid of points is super-imposed. grid points on all faces are included. -- `n_pts::Tuple{Int, Int, Int}`: number of grid points in x, y, z directions. 0 and 1 fractional coordinates are included. -- `data::Array{T, 3}`: three dimensional array conaining data associated with each grid point. -- `units::Symbol`: the units associated with each data point. -- `origin::Array{Float64, 1}`: the origin of the grid. + + - `box::Box`: describes Bravais lattice over which a grid of points is super-imposed. grid points on all faces are included. + - `n_pts::Tuple{Int, Int, Int}`: number of grid points in x, y, z directions. 0 and 1 fractional coordinates are included. + - `data::Array{T, 3}`: three dimensional array conaining data associated with each grid point. + - `units::Symbol`: the units associated with each data point. + - `origin::Array{Float64, 1}`: the origin of the grid. """ struct Grid{T} box::Box @@ -17,7 +18,6 @@ struct Grid{T} origin::Array{Float64, 1} end - """ voxel_id = xf_to_id(n_pts, xf) @@ -26,14 +26,17 @@ partitioned into a regular grid of `n_pts[1]` by `n_pts[2]` by `n_pts[3]` voxels Periodic boundary conditions are applied. # Arguments - - `n_pts::Tuple{Int, Int, Int}`: The number of points for each axis in the `Grid` - - `xf::Array{Float64, 1}`: The fractional coordinates to be converted to an id + + - `n_pts::Tuple{Int, Int, Int}`: The number of points for each axis in the `Grid` + - `xf::Array{Float64, 1}`: The fractional coordinates to be converted to an id + # Returns - - `id::Array{Int, 1}`: The array indices for storing this point in space + + - `id::Array{Int, 1}`: The array indices for storing this point in space """ function xf_to_id(n_pts::Tuple{Int, Int, Int}, xf::Array{Float64, 1}) voxel_id = floor.(Int, xf .* n_pts) .+ 1 - for k = 1:3 + for k in 1:3 if voxel_id[k] <= 0 voxel_id[k] += n_pts[k] elseif voxel_id[k] > n_pts[k] @@ -43,7 +46,6 @@ function xf_to_id(n_pts::Tuple{Int, Int, Int}, xf::Array{Float64, 1}) return voxel_id end - """ xf = id_to_xf(voxel_id, n_pts) @@ -51,16 +53,18 @@ Given a `voxel_id` in a `Grid`, return the fractional coordinates to which this corresponds. # Arguments - - `n_pts::Tuple{Int, Int, Int}`: The number of voxels along each axis in the `Grid` - - `voxel_id::Array{Int, 1}`: the voxel coordinates in `grid.data` + + - `n_pts::Tuple{Int, Int, Int}`: The number of voxels along each axis in the `Grid` + - `voxel_id::Array{Int, 1}`: the voxel coordinates in `grid.data` + # Returns - - `xf::Array{Float64, 1}`: The fractional coordinates corresponding to the grid voxel + + - `xf::Array{Float64, 1}`: The fractional coordinates corresponding to the grid voxel """ function id_to_xf(id::Tuple{Int, Int, Int}, n_pts::Tuple{Int, Int, Int}) - return [(id[k] - 1.0) / (n_pts[k] - 1.0) for k = 1:3] + return [(id[k] - 1.0) / (n_pts[k] - 1.0) for k in 1:3] end - """ update_density!(grid, molecule, species) @@ -70,14 +74,15 @@ doesn't calculate the actual densities, it will need a `./ = num_snapshots` at the end of the GCMC simulation. # Arguments - - `grid::Grid`: the grid to be updated - - `molecules::Array{Molecule, 1}`: An array of molecules whose positions will + + - `grid::Grid`: the grid to be updated + - `molecules::Array{Molecule, 1}`: An array of molecules whose positions will be added to the grid - - `species::Symbol`: The species of atom that can be added to this density grid + - `species::Symbol`: The species of atom that can be added to this density grid """ function update_density!(grid::Grid, molecules::Array{Molecule{Frac}, 1}, species::Symbol) for molecule in molecules - for i = 1:molecule.atoms.n + for i in 1:(molecule.atoms.n) if molecule.atoms.species[i] == species voxel_id = xf_to_id(grid.n_pts, molecule.atoms.coords.xf[:, i]) grid.data[voxel_id...] += 1 @@ -86,7 +91,6 @@ function update_density!(grid::Grid, molecules::Array{Molecule{Frac}, 1}, specie end end - # don't include molecules outside the box. function update_density!(grid::Grid, molecules::Array{Molecule{Cart}, 1}, species::Symbol) for molecule in molecules @@ -95,7 +99,7 @@ function update_density!(grid::Grid, molecules::Array{Molecule{Cart}, 1}, specie if any(xf_com .> 1.0) || any(xf_com .< 0.0) continue # don't look at this molecule end - for i = 1:molecule.atoms.n + for i in 1:(molecule.atoms.n) if molecule.atoms.species[i] == species xf_atom = grid.box.c_to_f * molecule.atoms.coords.x[:, i] voxel_id = xf_to_id(grid.n_pts, xf_atom) @@ -105,7 +109,6 @@ function update_density!(grid::Grid, molecules::Array{Molecule{Cart}, 1}, specie end end - """ n_pts = required_n_pts(box, dx) @@ -114,15 +117,14 @@ distances between grid points less than `dx` apart, where `dx` is in units of An """ function required_n_pts(box::Box, dx::Float64) # columns of f_to_c are the unit cell lattice vectors. - cell_vector_norms = [norm(box.f_to_c[:, i]) for i = 1:3] + cell_vector_norms = [norm(box.f_to_c[:, i]) for i in 1:3] n_pts = zeros(Int, 3) - for i = 1:3 + for i in 1:3 n_pts[i] = ceil(Int, cell_vector_norms[i] / dx) + 1 end return Tuple(n_pts) end - """ write_cube(grid, filename, verbose=true) @@ -131,19 +133,25 @@ http://paulbourke.net/dataformats/cube/ The atoms of the unit cell are not printed in the .cube. Instead, use .xyz files to also visualize atoms. # Arguments -- `grid::Grid`: grid with associated data at each grid point. -- `filename::AbstractString`: name of .cube file to which we write the grid; this is relative to `rc[:paths][:grids]`. -- `verbose::Bool`: print name of file after writing. -- `length_units::String`: units for length. Bohr or Angstrom. + + - `grid::Grid`: grid with associated data at each grid point. + - `filename::AbstractString`: name of .cube file to which we write the grid; this is relative to `rc[:paths][:grids]`. + - `verbose::Bool`: print name of file after writing. + - `length_units::String`: units for length. Bohr or Angstrom. """ -function write_cube(grid::Grid, filename::AbstractString; verbose::Bool=true, length_units::String="Angstrom") - if ! isdir(rc[:paths][:grids]) +function write_cube( + grid::Grid, + filename::AbstractString; + verbose::Bool=true, + length_units::String="Angstrom" +) + if !isdir(rc[:paths][:grids]) mkdir(rc[:paths][:grids]) end @assert (length_units in ["Angstrom", "Bohr"]) - if ! occursin(".cube", filename) + if !occursin(".cube", filename) filename = filename * ".cube" end cubefile = open(joinpath(rc[:paths][:grids], filename), "w") @@ -152,21 +160,27 @@ function write_cube(grid::Grid, filename::AbstractString; verbose::Bool=true, le # the integer refers to 0 atoms (just use .xyz to visualize atoms) # the next three floats correspond to the origin - @printf(cubefile, "%d %f %f %f\n" , 0, grid.origin[1], grid.origin[2], grid.origin[3]) - for k = 1:3 + @printf(cubefile, "%d %f %f %f\n", 0, grid.origin[1], grid.origin[2], grid.origin[3]) + for k in 1:3 # these are the vectors that form the parallelogram comprising the voxels # 0 and 1 fractional coords were included. so voxel vector is unit cell axis divided by # grid pts - 1 voxel_vector = grid.box.f_to_c[:, k] / (grid.n_pts[k] - 1) if length_units == "Bohr" voxel_vector = 1.88973 * voxel_vector end - @printf(cubefile, "%d %f %f %f\n" , grid.n_pts[k], - voxel_vector[1], voxel_vector[2], voxel_vector[3]) - end - - for i = 1:grid.n_pts[1] - for j = 1:grid.n_pts[2] - for k = 1:grid.n_pts[3] + @printf( + cubefile, + "%d %f %f %f\n", + grid.n_pts[k], + voxel_vector[1], + voxel_vector[2], + voxel_vector[3] + ) + end + + for i in 1:grid.n_pts[1] + for j in 1:grid.n_pts[2] + for k in 1:grid.n_pts[3] @printf(cubefile, "%e ", grid.data[i, j, k]) if (k % 6) == 0 @printf(cubefile, "\n") @@ -182,22 +196,23 @@ function write_cube(grid::Grid, filename::AbstractString; verbose::Bool=true, le return end - """ grid = read_cube(filename) -Read a .cube file and return a populated `Grid` data structure. +Read a .cube file and return a populated `Grid` data structure. It will detect and skip over atomic information if it is present in the file. # Arguments -- `filename::AbstractString`: name of .cube file to which we write the grid; this is relative to `rc[:paths][:grids]` -- `has_units::Bool=true`: flag for function to read units from file header + + - `filename::AbstractString`: name of .cube file to which we write the grid; this is relative to `rc[:paths][:grids]` + - `has_units::Bool=true`: flag for function to read units from file header # Returns -- `grid::Grid`: A grid data structure + + - `grid::Grid`: A grid data structure """ function read_cube(filename::AbstractString; has_units::Bool=true) - if ! occursin(".cube", filename) + if !occursin(".cube", filename) filename *= ".cube" end @@ -215,28 +230,28 @@ function read_cube(filename::AbstractString; has_units::Bool=true) # read origin line = readline(cubefile) number_of_atoms = parse(Int64, split(line)[1]) - origin = [parse(Float64, split(line)[1 + i]) for i = 1:3] + origin = [parse(Float64, split(line)[1 + i]) for i in 1:3] # read box info - box_lines = [readline(cubefile) for _ ∈ 1:3] + box_lines = [readline(cubefile) for _ in 1:3] # number of grid pts - n_pts = Tuple([parse(Int, split(box_lines[i])[1]) for i = 1:3]) + n_pts = Tuple([parse(Int, split(box_lines[i])[1]) for i in 1:3]) # f_to_c matrix (given by voxel vectors) f_to_c = zeros(Float64, 3, 3) - for i = 1:3, j=1:3 + for i in 1:3, j in 1:3 f_to_c[j, i] = parse(Float64, split(box_lines[i])[j + 1]) end - for k = 1:3 + for k in 1:3 f_to_c[:, k] = f_to_c[:, k] * (n_pts[k] - 1.0) end # reconstruct box from f_to_c matrix box = Box(f_to_c) - + # skip over atomic information if present - if ! (number_of_atoms == 0) + if !(number_of_atoms == 0) for i in 1:number_of_atoms line = readline(cubefile) end @@ -245,11 +260,11 @@ function read_cube(filename::AbstractString; has_units::Bool=true) # read in data data = zeros(Float64, n_pts...) line = readline(cubefile) - for i = 1:n_pts[1] - for j = 1:n_pts[2] + for i in 1:n_pts[1] + for j in 1:n_pts[2] read_count = 0 - for k = 1:n_pts[3] - data[i, j, k] = parse(Float64, split(line)[read_count+1]) + for k in 1:n_pts[3] + data[i, j, k] = parse(Float64, split(line)[read_count + 1]) read_count += 1 if (k % 6 == 0) line = readline(cubefile) @@ -263,9 +278,8 @@ function read_cube(filename::AbstractString; has_units::Bool=true) return Grid(box, n_pts, data, units, origin) end - """ - grid = energy_grid(crystal, molecule, ljforcefield; resolution=1.0, temperature=298.0, n_rotations=750) + grid = energy_grid(crystal, molecule, ljforcefield; resolution=1.0, temperature=298.0, n_rotations=750) Superimposes a regular grid of points (regularly spaced in fractional coordinates of the `crystal.box`) over the unit cell of a crystal, with `n_gridpts` dictating the number of grid points in the a, b, c directions (including 0 and 1 fractional coords). The fractional coordinates 0 and 1 are included in the grid, although they are redundant. @@ -274,30 +288,43 @@ Then, at each grid point, calculate the ensemble average potential energy of the The ensemble average is a Boltzmann average over rotations: - R T log ⟨e⁻ᵇᵁ⟩ # Arguments -- `crystal::Crystal`: crystal in which we seek to compute an energy grid for a molecule. `grid.box` will be `framework.box`. -- `molecule::Molecule`: molecule for which we seek an energy grid -- `ljforcefield::LJForceField`: molecular model for computing molecule-crystal interactions -- `resolution::Union{Float64, Tuple{Int, Int, Int}}=1.0`: maximum distance between grid points, in Å, or a tuple specifying the number of grid points in each dimension. -- `n_rotations::Int`: number of random rotations to conduct in a Monte Carlo simulation for finding the free energy of a molecule centered at a given grid point. -This is only relevant for molecules that are comprised of more than one Lennard Jones sphere. -- `temperature::Float64`: the temperature at which to compute the free energy for molecules where rotations are required. Lower temperatures overemphasize the minimum potential energy rotational conformation at that point. -- `units::Symbol`: either `:K` or `:kJ_mol`, the units in which the energy should be stored in the returned `Grid`. -- `center::Bool`: shift coords of grid so that the origin is the center of the unit cell `crystal.box`. -- `verbose::Bool=true`: print some information. + + - `crystal::Crystal`: crystal in which we seek to compute an energy grid for a molecule. `grid.box` will be `framework.box`. + - `molecule::Molecule`: molecule for which we seek an energy grid + - `ljforcefield::LJForceField`: molecular model for computing molecule-crystal interactions + - `resolution::Union{Float64, Tuple{Int, Int, Int}}=1.0`: maximum distance between grid points, in Å, or a tuple specifying the number of grid points in each dimension. + - `n_rotations::Int`: number of random rotations to conduct in a Monte Carlo simulation for finding the free energy of a molecule centered at a given grid point. + This is only relevant for molecules that are comprised of more than one Lennard Jones sphere. + - `temperature::Float64`: the temperature at which to compute the free energy for molecules where rotations are required. Lower temperatures overemphasize the minimum potential energy rotational conformation at that point. + - `units::Symbol`: either `:K` or `:kJ_mol`, the units in which the energy should be stored in the returned `Grid`. + - `center::Bool`: shift coords of grid so that the origin is the center of the unit cell `crystal.box`. + - `verbose::Bool=true`: print some information. # Returns -- `grid::Grid`: A grid data structure containing the potential energy of the system + + - `grid::Grid`: A grid data structure containing the potential energy of the system """ -function energy_grid(crystal::Crystal, molecule::Molecule{Cart}, ljforcefield::LJForceField; - resolution::Union{Float64, Tuple{Int, Int, Int}}=1.0, n_rotations::Int=1000, - temperature::Float64=NaN, units::Symbol=:kJ_mol, center::Bool=false, - verbose::Bool=true) +function energy_grid( + crystal::Crystal, + molecule::Molecule{Cart}, + ljforcefield::LJForceField; + resolution::Union{Float64, Tuple{Int, Int, Int}}=1.0, + n_rotations::Int=1000, + temperature::Float64=NaN, + units::Symbol=:kJ_mol, + center::Bool=false, + verbose::Bool=true +) assert_P1_symmetry(crystal) - if ! (units in [:kJ_mol, :K]) + if !(units in [:kJ_mol, :K]) error("Pass :kJ_mol or :K for units of kJ/mol or K, respectively.") end - n_pts = isa(resolution, Tuple{Int,Int,Int}) ? resolution : required_n_pts(crystal.box, resolution) + n_pts = if isa(resolution, Tuple{Int, Int, Int}) + resolution + else + required_n_pts(crystal.box, resolution) + end rotations_required = needs_rotations(molecule) charged_system = has_charges(crystal) && has_charges(molecule) @@ -305,21 +332,38 @@ function energy_grid(crystal::Crystal, molecule::Molecule{Cart}, ljforcefield::L error("Must pass temperature (K) for Boltzmann weighted rotations.\n") end - eparams = setup_Ewald_sum(crystal.box, sqrt(ljforcefield.r²_cutoff), - verbose=verbose & charged_system) + eparams = setup_Ewald_sum( + crystal.box, + sqrt(ljforcefield.r²_cutoff); + verbose=verbose & charged_system + ) eikr = Eikr(crystal, eparams) # grid of voxel centers (each axis at least). - grid_pts = [collect(range(0.0; stop=1.0, length=n_pts[i])) for i = 1:3] + grid_pts = [collect(range(0.0; stop=1.0, length=n_pts[i])) for i in 1:3] - grid = Grid(crystal.box, n_pts, zeros(Float64, n_pts...), units, - center ? crystal.box.f_to_c * [-0.5, -0.5, -0.5] : [0.0, 0.0, 0.0]) + grid = Grid( + crystal.box, + n_pts, + zeros(Float64, n_pts...), + units, + center ? crystal.box.f_to_c * [-0.5, -0.5, -0.5] : [0.0, 0.0, 0.0] + ) if verbose @printf("Computing energy grid of %s in %s\n", molecule.species, crystal.name) - @printf("\tRegular grid (in fractional space) of %d by %d by %d points superimposed over the unit cell.\n", n_pts[1], n_pts[2], n_pts[3]) + @printf( + "\tRegular grid (in fractional space) of %d by %d by %d points superimposed over the unit cell.\n", + n_pts[1], + n_pts[2], + n_pts[3] + ) if rotations_required - @printf("\t%d molecule rotations per grid point with temperature %f K.\n", n_rotations, temperature) + @printf( + "\t%d molecule rotations per grid point with temperature %f K.\n", + n_rotations, + temperature + ) end end @@ -331,60 +375,79 @@ function energy_grid(crystal::Crystal, molecule::Molecule{Cart}, ljforcefield::L repfactors = replication_factors(crystal.box, ljforcefield) crystal = replicate(crystal, repfactors) - for (i, xf) in enumerate(grid_pts[1]), (j, yf) in enumerate(grid_pts[2]), (k, zf) in enumerate(grid_pts[3]) + for (i, xf) in enumerate(grid_pts[1]), + (j, yf) in enumerate(grid_pts[2]), + (k, zf) in enumerate(grid_pts[3]) # must account for fact that crystal is now replicated; use coords in home box translate_to!(molecule, Frac([xf, yf, zf] ./ repfactors)) - if ! rotations_required + if !rotations_required ensemble_average_energy = vdw_energy(crystal, molecule, ljforcefield) else boltzmann_factor_sum = 0.0 - for _ ∈ 1:n_rotations - random_rotation!(molecule) + for _ in 1:n_rotations + random_rotation!(molecule, crystal.box) energy = PotentialEnergy(0.0, 0.0) energy.vdw = vdw_energy(crystal, molecule, ljforcefield) if charged_system - energy.coulomb = total(electrostatic_potential_energy(crystal, molecule, - eparams, eikr)) + energy.es = total( + electrostatic_potential_energy(crystal, molecule, eparams, eikr) + ) end boltzmann_factor_sum += exp(-sum(energy) / temperature) end ensemble_average_energy = -temperature * log(boltzmann_factor_sum / n_rotations) end - grid.data[i, j, k] = ensemble_average_energy # K - end + grid.data[i, j, k] = ensemble_average_energy # K + end if units == :kJ_mol # K - kJ/mol grid.data[:] = grid.data[:] * 8.314 / 1000.0 end - return grid + return grid end - function Base.show(io::IO, grid::Grid) - @printf(io, "Regular grid of %d by %d by %d points superimposed over a unit cell and associated data.\n", grid.n_pts[1], grid.n_pts[2], grid.n_pts[3]) + @printf( + io, + "Regular grid of %d by %d by %d points superimposed over a unit cell and associated data.\n", + grid.n_pts[1], + grid.n_pts[2], + grid.n_pts[3] + ) @printf(io, "\tunits of data attribute: %s\n", grid.units) @printf(io, "\torigin: [%f, %f, %f]\n", grid.origin[1], grid.origin[2], grid.origin[3]) end - # comparing very large numbers in grid.data, so increase rtol to account # for loss of precision when writing grid.data to a cube file. -function Base.isapprox(g1::Grid, g2::Grid; atol::Real=0.0, rtol::Real=atol > 0.0 ? 0.0 : sqrt(eps())) - return (isapprox(g1.box, g2.box, atol=atol, rtol=rtol) && - (g1.n_pts == g2.n_pts) && - isapprox(g1.data, g2.data, atol=atol, rtol=rtol) && - (g1.units == g2.units) && - isapprox(g1.origin, g2.origin, atol=atol, rtol=rtol)) +function Base.isapprox( + g1::Grid, + g2::Grid; + atol::Real=0.0, + rtol::Real=atol > 0.0 ? 0.0 : sqrt(eps()) +) + return ( + isapprox(g1.box, g2.box; atol=atol, rtol=rtol) && + (g1.n_pts == g2.n_pts) && + isapprox(g1.data, g2.data; atol=atol, rtol=rtol) && + (g1.units == g2.units) && + isapprox(g1.origin, g2.origin; atol=atol, rtol=rtol) + ) end - -function _flood_fill!(grid::Grid, segmented_grid::Grid, - queue_of_grid_pts::Array{Tuple{Int, Int, Int}, 1}, - i::Int, j::Int, k::Int, energy_tol::Float64) +function _flood_fill!( + grid::Grid, + segmented_grid::Grid, + queue_of_grid_pts::Array{Tuple{Int, Int, Int}, 1}, + i::Int, + j::Int, + k::Int, + energy_tol::Float64 +) # look left, right, up, down "_s" for shift - for i_s = -1:1, j_s = -1:1, k_s = -1:1 + for i_s in -1:1, j_s in -1:1, k_s in -1:1 # well already know segmented_grid[i, j, k] if (i_s, j_s, k_s) == (0, 0, 0) continue @@ -417,15 +480,14 @@ function _flood_fill!(grid::Grid, segmented_grid::Grid, return nothing end - function _segment_grid(grid::Grid, energy_tol::Float64, verbose::Bool) # grid of Int's corresponding to each original grid point. # let "0" be "unsegmented" # let "-1" be "not occupiable" and, eventually, "not accessible" - segmented_grid = Grid(grid.box, grid.n_pts, zeros(Int, grid.n_pts...), - :Segment_No, grid.origin) + segmented_grid = + Grid(grid.box, grid.n_pts, zeros(Int, grid.n_pts...), :Segment_No, grid.origin) segment_no = 0 - for i = 1:grid.n_pts[1], j = 1:grid.n_pts[2], k = 1:grid.n_pts[3] + for i in 1:grid.n_pts[1], j in 1:grid.n_pts[2], k in 1:grid.n_pts[3] if segmented_grid.data[i, j, k] == 0 # not yet assigned segment if grid.data[i, j, k] > energy_tol # not accessible segmented_grid.data[i, j, k] = -1 @@ -440,8 +502,7 @@ function _segment_grid(grid::Grid, energy_tol::Float64, verbose::Bool) # assign segment number segmented_grid.data[id...] = segment_no # look at surroudning points, add to queue if they are also accessible - _flood_fill!(grid, segmented_grid, queue_of_grid_pts, - id..., energy_tol) + _flood_fill!(grid, segmented_grid, queue_of_grid_pts, id..., energy_tol) # handled first one in queue, remove from queue deleteat!(queue_of_grid_pts, 1) end @@ -455,7 +516,6 @@ function _segment_grid(grid::Grid, energy_tol::Float64, verbose::Bool) return segmented_grid end - # this returns number of segments in a segmented grid. # it excludes the inaccessible portions -1. function _count_segments(segmented_grid::Grid) @@ -471,7 +531,6 @@ function _count_segments(segmented_grid::Grid) return nb_segments end - # struct describing directed edge describing connection between two segments of a flood- # filled grid. the direction (which unit cell face is traversed in the connection) is # also included. @@ -481,9 +540,13 @@ struct SegmentConnection direction::Tuple{Int, Int, Int} # unit cell face traversed end - -function _note_connection!(segment_1::Int, segment_2::Int, connections::Array{SegmentConnection, 1}, - direction::Tuple{Int, Int, Int}, verbose::Bool=true) +function _note_connection!( + segment_1::Int, + segment_2::Int, + connections::Array{SegmentConnection, 1}, + direction::Tuple{Int, Int, Int}, + verbose::Bool=true +) # ignore connections between un-occupiable regions if (segment_1 == -1) || (segment_2 == -1) return nothing @@ -491,46 +554,71 @@ function _note_connection!(segment_1::Int, segment_2::Int, connections::Array{Se # construct edge; if not in list of edges, push it and note connection segment_connection = SegmentConnection(segment_1, segment_2, direction) - if ! (segment_connection in connections) + if !(segment_connection in connections) push!(connections, segment_connection) if verbose - @printf("Noted seg. %d --> %d connection in (%d, %d, %d) direction.\n", - segment_1, segment_2, direction[1], direction[2], direction[3]) + @printf( + "Noted seg. %d --> %d connection in (%d, %d, %d) direction.\n", + segment_1, + segment_2, + direction[1], + direction[2], + direction[3] + ) end # also add opposite direction to take into account symmetry - push!(connections, SegmentConnection(segment_2, segment_1, segment_connection.direction .* -1)) + push!( + connections, + SegmentConnection(segment_2, segment_1, segment_connection.direction .* -1) + ) end return nothing end - # obtain set of Segment Connections function _build_list_of_connections(segmented_grid::Grid; verbose::Bool=true) # initialize empty list of edges connections = SegmentConnection[] # loop over faces of unit cell - for i = 1:segmented_grid.n_pts[1], j = 1:segmented_grid.n_pts[2] - _note_connection!(segmented_grid.data[i, j, end], segmented_grid.data[i, j, 1], - connections, (0, 0, 1), verbose) - end - - for j = 1:segmented_grid.n_pts[2], k = 1:segmented_grid.n_pts[3] - _note_connection!(segmented_grid.data[end, j, k], segmented_grid.data[1, j, k], - connections, (1, 0, 0), verbose) - end - - for i = 1:segmented_grid.n_pts[1], k = 1:segmented_grid.n_pts[3] - _note_connection!(segmented_grid.data[i, end, k], segmented_grid.data[i, 1, k], - connections, (0, 1, 0), verbose) + for i in 1:segmented_grid.n_pts[1], j in 1:segmented_grid.n_pts[2] + _note_connection!( + segmented_grid.data[i, j, end], + segmented_grid.data[i, j, 1], + connections, + (0, 0, 1), + verbose + ) + end + + for j in 1:segmented_grid.n_pts[2], k in 1:segmented_grid.n_pts[3] + _note_connection!( + segmented_grid.data[end, j, k], + segmented_grid.data[1, j, k], + connections, + (1, 0, 0), + verbose + ) + end + + for i in 1:segmented_grid.n_pts[1], k in 1:segmented_grid.n_pts[3] + _note_connection!( + segmented_grid.data[i, end, k], + segmented_grid.data[i, 1, k], + connections, + (0, 1, 0), + verbose + ) end return connections end - -function _translate_into_graph(segmented_grid::Grid, connections::Array{SegmentConnection, 1}) +function _translate_into_graph( + segmented_grid::Grid, + connections::Array{SegmentConnection, 1} +) nb_segments = _count_segments(segmented_grid) # directed graph @@ -565,10 +653,12 @@ function _translate_into_graph(segmented_grid::Grid, connections::Array{SegmentC return graph, vertex_to_direction end - -function _classify_segments(segmented_grid::Grid, graph::SimpleDiGraph{Int64}, - vertex_to_direction::Dict{Int, Union{Nothing, Tuple{Int, Int, Int}}}, - verbose::Bool=true) +function _classify_segments( + segmented_grid::Grid, + graph::SimpleDiGraph{Int64}, + vertex_to_direction::Dict{Int, Union{Nothing, Tuple{Int, Int, Int}}}, + verbose::Bool=true +) nb_segments = _count_segments(segmented_grid) # 0 = inaccessible, 1 = accessible. start out with assuming inaccessible. @@ -577,7 +667,10 @@ function _classify_segments(segmented_grid::Grid, graph::SimpleDiGraph{Int64}, # look for simple cycles in the graph all_cycles = simplecycles(graph) if verbose - @printf("\tFound %d simple cycles in segment connectivity graph.\n", length(all_cycles)) + @printf( + "\tFound %d simple cycles in segment connectivity graph.\n", + length(all_cycles) + ) end # all edges involved in a cycle are connected together and are accessible channels @@ -612,12 +705,12 @@ function _classify_segments(segmented_grid::Grid, graph::SimpleDiGraph{Int64}, # now could have something like 1 --> 2 ---> 4 ---> 2. then 1 is also accessible # loop over all segments - for s = 1:nb_segments + for s in 1:nb_segments # if this segment was involved in a cycle that resulted in ending up in a different # unit cell if segment_classifiction[s] == 1 # look for edges connected to this segment; these must be accessible - for t = 1:nb_segments + for t in 1:nb_segments if Edge(t, s) in edges(graph) segment_classifiction[t] = 1 end @@ -628,10 +721,12 @@ function _classify_segments(segmented_grid::Grid, graph::SimpleDiGraph{Int64}, return segment_classifiction end - -function _assign_inaccessible_pockets_minus_one!(segmented_grid::Grid, - segment_classifiction::Array{Int, 1}; verbose::Bool=true) - for s = eachindex(segment_classifiction) +function _assign_inaccessible_pockets_minus_one!( + segmented_grid::Grid, + segment_classifiction::Array{Int, 1}; + verbose::Bool=true +) + for s in eachindex(segment_classifiction) if segment_classifiction[s] == 1 if verbose @printf("Segment %s classified as accessible channel.\n", s) @@ -645,7 +740,6 @@ function _assign_inaccessible_pockets_minus_one!(segmented_grid::Grid, end end - """ accessibility_grid, nb_segments_blocked, porosity = compute_accessibility_grid(crystal, probe_molecule, ljforcefield; resolution::Union{Float64, Tuple{Int, Int, Int}}=1.0, energy_tol=10.0, energy_unit=:kJ_mol, @@ -671,39 +765,65 @@ Returns `accessibility_grid::Grid{Bool}` and `nb_segments_blocked`, the latter t of segments that were blocked because they were determined to be inaccessible. # Arguments -* `crystal::Crystal`: the crystal for which we seek to compute an accessibility grid. -* `probe_molecule::Molecule` a molecule serving as a probe to determine whether a given -point can be occupied and accessed. -* `LJForceField::LJForceField`: the force field used to compute the potential energy of -the probe molecule -* - `resolution::Union{Float64, Tuple{Int, Int, Int}}=1.0`: maximum distance between grid points, in Å, or a tuple specifying the number of grid points in each dimension. -* `energy_tol::Float64`: if the computed potential energy is less than this, we declare the -grid point to be occupiable. Also this is the energy barrier beyond which we assume the -probe adsorbate cannot pass. Units given by `energy_units` argument -* `energy_units::Symbol`: units of energy (`:kJ_mol` or `:K`) to be used in determining -threshold for occupiability and whether molecule can percolate over barrier in channel. -(see `energy_tol`) -* `write_b4_after_grids::Bool`: write a .cube file of occupiability for visualization both -before and after flood fill/blocking inaccessible pockets + + - `crystal::Crystal`: the crystal for which we seek to compute an accessibility grid. + - `probe_molecule::Molecule` a molecule serving as a probe to determine whether a given + point can be occupied and accessed. + - `LJForceField::LJForceField`: the force field used to compute the potential energy of + the probe molecule + - + `resolution::Union{Float64, Tuple{Int, Int, Int}}=1.0`: maximum distance between grid points, in Å, or a tuple specifying the number of grid points in each dimension. + - `energy_tol::Float64`: if the computed potential energy is less than this, we declare the + grid point to be occupiable. Also this is the energy barrier beyond which we assume the + probe adsorbate cannot pass. Units given by `energy_units` argument + - `energy_units::Symbol`: units of energy (`:kJ_mol` or `:K`) to be used in determining + threshold for occupiability and whether molecule can percolate over barrier in channel. + (see `energy_tol`) + - `write_b4_after_grids::Bool`: write a .cube file of occupiability for visualization both + before and after flood fill/blocking inaccessible pockets """ -function compute_accessibility_grid(crystal::Crystal, probe::Molecule, forcefield::LJForceField; - resolution::Union{Float64, Tuple{Int, Int, Int}}=1.0, - energy_tol::Float64=10.0, energy_units::Symbol=:kJ_mol, - verbose::Bool=true, write_b4_after_grids::Bool=true, - block_inaccessible_pockets::Bool=true) +function compute_accessibility_grid( + crystal::Crystal, + probe::Molecule, + forcefield::LJForceField; + resolution::Union{Float64, Tuple{Int, Int, Int}}=1.0, + energy_tol::Float64=10.0, + energy_units::Symbol=:kJ_mol, + verbose::Bool=true, + write_b4_after_grids::Bool=true, + block_inaccessible_pockets::Bool=true +) assert_P1_symmetry(crystal) if verbose - printstyled(@sprintf("Computing accessibility grid of %s using %f %s potential energy tol and %s probe...\n", - crystal.name, energy_tol, energy_units, probe.species), color=:green) + printstyled( + @sprintf( + "Computing accessibility grid of %s using %f %s potential energy tol and %s probe...\n", + crystal.name, + energy_tol, + energy_units, + probe.species + ); + color=:green + ) end # write potential energy grid - grid = energy_grid(crystal, probe, forcefield, resolution=resolution, verbose=verbose, - units=energy_units) - - if ! block_inaccessible_pockets - accessibility_grid = Grid{Bool}(grid.box, grid.n_pts, grid.data .< energy_tol, - :accessibility, grid.origin) + grid = energy_grid( + crystal, + probe, + forcefield; + resolution=resolution, + verbose=verbose, + units=energy_units + ) + + if !block_inaccessible_pockets + accessibility_grid = Grid{Bool}( + grid.box, + grid.n_pts, + grid.data .< energy_tol, + :accessibility, + grid.origin + ) porosity = sum(accessibility_grid.data) / length(accessibility_grid.data) @@ -716,72 +836,111 @@ function compute_accessibility_grid(crystal::Crystal, probe::Molecule, forcefiel if write_b4_after_grids _segmented_grid = deepcopy(segmented_grid) _segmented_grid.data[segmented_grid.data .!= -1] .= 1 - gridfilename = @sprintf("%s_in_%s_%s_b4_pocket_blocking.cube", probe.species, + gridfilename = @sprintf( + "%s_in_%s_%s_b4_pocket_blocking.cube", + probe.species, replace(replace(crystal.name, ".cif" => ""), ".cssr" => ""), - replace(forcefield.name, ".csv" => "")) + replace(forcefield.name, ".csv" => "") + ) write_cube(_segmented_grid, gridfilename) end # get list of edges describing connectivity of segments across unit cell boundaries - connections = _build_list_of_connections(segmented_grid, verbose=verbose) + connections = _build_list_of_connections(segmented_grid; verbose=verbose) # translate this into a directed LightGraph. Note that these include directions as an # artificial vertex to keep track of which unit cell boundary is traversed. graph, vertex_to_direction = _translate_into_graph(segmented_grid, connections) # get classifications of the segments - segment_classifications = _classify_segments(segmented_grid, graph, vertex_to_direction, verbose) + segment_classifications = + _classify_segments(segmented_grid, graph, vertex_to_direction, verbose) # assign inaccessible pockets minus one if cycle not found in graph - _assign_inaccessible_pockets_minus_one!(segmented_grid, segment_classifications, verbose=verbose) + _assign_inaccessible_pockets_minus_one!( + segmented_grid, + segment_classifications; + verbose=verbose + ) # -1 for not accessible, 1 for accessible segmented_grid.data[segmented_grid.data .!= -1] .= 1 if write_b4_after_grids - gridfilename = @sprintf("%s_in_%s_%s_after_pocket_blocking.cube", probe.species, + gridfilename = @sprintf( + "%s_in_%s_%s_after_pocket_blocking.cube", + probe.species, replace(replace(crystal.name, ".cif" => ""), ".cssr" => ""), - replace(forcefield.name, ".csv" => "")) + replace(forcefield.name, ".csv" => "") + ) write_cube(segmented_grid, gridfilename) end # print warning if there is no use in running a simulation since all pockets are inaccessible if all(segmented_grid.data .== -1) - @warn @sprintf("%s cannot enter the pores of %s with %f K energy tolerance.", - probe.species, crystal.name, energy_tol) + @warn @sprintf( + "%s cannot enter the pores of %s with %f K energy tolerance.", + probe.species, + crystal.name, + energy_tol + ) end nb_segments_blocked = sum(segment_classifications .== 0) # instead of returning grid of int's return a boolean grid for storage efficiency - accessibility_grid = Grid{Bool}(segmented_grid.box, segmented_grid.n_pts, - segmented_grid.data .== 1, :accessibility, segmented_grid.origin) + accessibility_grid = Grid{Bool}( + segmented_grid.box, + segmented_grid.n_pts, + segmented_grid.data .== 1, + :accessibility, + segmented_grid.origin + ) # compute porosity before and after blocking - porosity = Dict(:b4_blocking => sum(grid.data .< energy_tol) / length(grid.data), - :after_blocking => sum(accessibility_grid.data) / length(accessibility_grid.data)) + porosity = Dict( + :b4_blocking => sum(grid.data .< energy_tol) / length(grid.data), + :after_blocking => + sum(accessibility_grid.data) / length(accessibility_grid.data) + ) if nb_segments_blocked != 0 - printstyled(@sprintf("\t%d pockets in %s were found to be inaccessible to %s and blocked.\n", - sum(segment_classifications .== 0), crystal.name, probe.species), color=:yellow) - @printf("\tPorosity of %s b4 pocket blocking is %f\n", crystal.name, porosity[:b4_blocking]) - @printf("\tPorosity of %s after pocket blocking is %f\n", crystal.name, porosity[:after_blocking]) - @printf("\tpotential energy barrier used to determine accessibility: %f %s\n", - energy_tol, energy_units) + printstyled( + @sprintf( + "\t%d pockets in %s were found to be inaccessible to %s and blocked.\n", + sum(segment_classifications .== 0), + crystal.name, + probe.species + ); + color=:yellow + ) + @printf( + "\tPorosity of %s b4 pocket blocking is %f\n", + crystal.name, + porosity[:b4_blocking] + ) + @printf( + "\tPorosity of %s after pocket blocking is %f\n", + crystal.name, + porosity[:after_blocking] + ) + @printf( + "\tpotential energy barrier used to determine accessibility: %f %s\n", + energy_tol, + energy_units + ) end return accessibility_grid, nb_segments_blocked, porosity end - # return ID that is the nearest neighbor. function _arg_nearest_neighbor(n_pts::Tuple{Int, Int, Int}, xf::Array{Float64, 1}) return 1 .+ round.(Int, xf .* (n_pts .- 1)) end - function _apply_pbc_to_index!(id::Array{Int, 1}, n_pts::Tuple{Int, Int, Int}) - for k = 1:3 + for k in 1:3 if id[k] == 0 id[k] = n_pts[k] end @@ -792,7 +951,6 @@ function _apply_pbc_to_index!(id::Array{Int, 1}, n_pts::Tuple{Int, Int, Int}) return nothing end - """ accessible(accessibility_grid, xf) accessible(accessibility_grid, xf, repfactors) @@ -816,7 +974,7 @@ function accessible(accessibility_grid::Grid{Bool}, xf::Array{Float64, 1}) # the point is inaccessible if and only if ALL surrounding points are inaccessible; # if at the boundary, let the energy be computed and automatically address # accessibility during a simulation - for i = -1:1, j = -1:1, k = -1:1 + for i in -1:1, j in -1:1, k in -1:1 id_neighbor = id_nearest_neighbor .+ [i, j, k] _apply_pbc_to_index!(id_neighbor, accessibility_grid.n_pts) # again, if ANY surrounding point is accessible, let the energy computation go on @@ -831,7 +989,11 @@ end # when rep factors are used in the simulation so fractional coord system in grid does not match # that in the simulation. so `xf` is repect to simulation box which is replicated box in # the accessibility grid. -function accessible(accessibility_grid::Grid{Bool}, xf::Array{Float64, 1}, repfactors::Tuple{Int, Int, Int}) +function accessible( + accessibility_grid::Grid{Bool}, + xf::Array{Float64, 1}, + repfactors::Tuple{Int, Int, Int} +) return accessible(accessibility_grid, mod.(xf .* repfactors, 1.0)) end diff --git a/src/henry.jl b/src/henry.jl index 667baa8cd..dd71154dc 100644 --- a/src/henry.jl +++ b/src/henry.jl @@ -14,28 +14,37 @@ Kₕ = β ⟨e^{-β U}⟩, where the average is over positions and orientations in the crystal. # Arguments -- `crystal::Crystal`: the porous crystal in which we seek to simulate adsorption -- `molecule::Molecule`: the adsorbate molecule -- `temperature::Float64`: temperature of bulk gas phase in equilibrium with adsorbed phase + + - `crystal::Crystal`: the porous crystal in which we seek to simulate adsorption + - `molecule::Molecule`: the adsorbate molecule + - `temperature::Float64`: temperature of bulk gas phase in equilibrium with adsorbed phase in the porous material. units: Kelvin (K) -- `ljforcefield::LJForceField`: the molecular model used to describe the + - `ljforcefield::LJForceField`: the molecular model used to describe the energetics of the adsorbate-adsorbate and adsorbate-host van der Waals interactions. -- `insertions_per_volume::Int`: number of Widom insertions to perform for computing the -average, per unit cell volume (ų) -- `verbose::Bool`: whether or not to print off information during the simulation. -- `ewald_precision::Float64`: desired precision for Ewald summations; used to determine -the replication factors in reciprocal space. -- `autosave::Bool`: save results file as a .jld2 in rc[:paths][:simulations] -- `filename_comment::AbstractString`: An optional comment that will be appended to the name of the saved file. + - `insertions_per_volume::Int`: number of Widom insertions to perform for computing the + average, per unit cell volume (ų) + - `verbose::Bool`: whether or not to print off information during the simulation. + - `ewald_precision::Float64`: desired precision for Ewald summations; used to determine + the replication factors in reciprocal space. + - `autosave::Bool`: save results file as a .jld2 in rc[:paths][:simulations] + - `filename_comment::AbstractString`: An optional comment that will be appended to the name of the saved file. # Returns -- `result::Dict{String, Float64}`: A dictionary containing all the results from the Henry coefficient simulation + + - `result::Dict{String, Float64}`: A dictionary containing all the results from the Henry coefficient simulation """ -function henry_coefficient(crystal::Crystal, molecule::Molecule, temperature::Float64, - ljforcefield::LJForceField; insertions_per_volume::Union{Int, Float64}=200, - verbose::Bool=true, ewald_precision::Float64=1e-6, - autosave::Bool=true, filename_comment::AbstractString="", - accessibility_grid::Union{Nothing, Grid{Bool}}=nothing) +function henry_coefficient( + crystal::Crystal, + molecule::Molecule, + temperature::Float64, + ljforcefield::LJForceField; + insertions_per_volume::Union{Int, Float64}=200, + verbose::Bool=true, + ewald_precision::Float64=1e-6, + autosave::Bool=true, + filename_comment::AbstractString="", + accessibility_grid::Union{Nothing, Grid{Bool}}=nothing +) # simulation only works if crystal is in P1 assert_P1_symmetry(crystal) @@ -56,9 +65,13 @@ function henry_coefficient(crystal::Crystal, molecule::Molecule, temperature::Fl if !isnothing(accessibility_grid) println("Using provided accessibility grid to block inaccessible pockets") - if ! isapprox(accessibility_grid.box, crystal.box) - error(@sprintf("accessibility grid box does not match box of %s.\n", - crystal.name)) + if !isapprox(accessibility_grid.box, crystal.box) + error( + @sprintf( + "accessibility grid box does not match box of %s.\n", + crystal.name + ) + ) end end end @@ -71,7 +84,9 @@ function henry_coefficient(crystal::Crystal, molecule::Molecule, temperature::Fl # partition total insertions among blocks. if nprocs() > N_BLOCKS - error("Use $N_BLOCKS cores or less for Henry coefficient calculations to match the number of blocks") + error( + "Use $N_BLOCKS cores or less for Henry coefficient calculations to match the number of blocks" + ) end nb_insertions_per_block = ceil(Int, nb_insertions / N_BLOCKS) @@ -79,8 +94,13 @@ function henry_coefficient(crystal::Crystal, molecule::Molecule, temperature::Fl # replication factors for applying nearest image convention for short-range interactions repfactors = replication_factors(crystal.box, ljforcefield) if verbose - @printf("\tReplicating crystal %d by %d by %d for short-range cutoff %.2f\n", - repfactors[1], repfactors[2], repfactors[3], sqrt(ljforcefield.r²_cutoff)) + @printf( + "\tReplicating crystal %d by %d by %d for short-range cutoff %.2f\n", + repfactors[1], + repfactors[2], + repfactors[3], + sqrt(ljforcefield.r²_cutoff) + ) end # replicate the crystal atoms so fractional coords are in [0, 1] spanning the simulation box crystal = replicate(crystal, repfactors) @@ -100,28 +120,46 @@ function henry_coefficient(crystal::Crystal, molecule::Molecule, temperature::Fl # perform and the molecule to move around/rotate. each core needs a different # molecule because it will change its attributes in the simulation # x = (nb_insertions, molecule, block_no) for that core - henry_loop(x::Tuple{Int, Molecule, Int}) = _conduct_Widom_insertions(crystal, x[2], - temperature, ljforcefield, x[1], - charged_system, x[3], ewald_precision, verbose, - accessibility_grid, repfactors) + function henry_loop(x::Tuple{Int, Molecule, Int}) + return _conduct_Widom_insertions( + crystal, + x[2], + temperature, + ljforcefield, + x[1], + charged_system, + x[3], + ewald_precision, + verbose, + accessibility_grid, + repfactors + ) + end # parallelize insertions across the cores; keep nb_insertions_per_block same - res = pmap(henry_loop, [(nb_insertions_per_block, deepcopy(molecule), b) for b = 1:N_BLOCKS]) + res = pmap( + henry_loop, + [(nb_insertions_per_block, deepcopy(molecule), b) for b in 1:N_BLOCKS] + ) # unpack the boltzmann factor sum and weighted energy sum from each block - boltzmann_factor_sums = [res[b][1] for b = 1:N_BLOCKS] # Σᵢ e^(-βEᵢ) for that core - wtd_energy_sums = [res[b][2] for b = 1:N_BLOCKS] # Σᵢ Eᵢe^(-βEᵢ) for that core + boltzmann_factor_sums = [res[b][1] for b in 1:N_BLOCKS] # Σᵢ e^(-βEᵢ) for that core + wtd_energy_sums = [res[b][2] for b in 1:N_BLOCKS] # Σᵢ Eᵢe^(-βEᵢ) for that core # compute block ⟨U⟩, Kₕ # ⟨U⟩ = Σ Uᵢ e ^(βUᵢ) / [ ∑ e^(βUᵢ) ] # Kₕ = β Σ e ^(βUᵢ) / nb_insertions_per_block # (these N_BLOCKS-long arrays) average_energies = wtd_energy_sums ./ boltzmann_factor_sums # K - henry_coefficients = boltzmann_factor_sums / (UNIV_GAS_CONST * temperature * nb_insertions_per_block) # mol/(m³-bar) + henry_coefficients = + boltzmann_factor_sums / (UNIV_GAS_CONST * temperature * nb_insertions_per_block) # mol/(m³-bar) if verbose - for b = 1:N_BLOCKS - printstyled(@sprintf("\tBlock %d/%d statistics:\n", b, N_BLOCKS); color=:yellow) + for b in 1:N_BLOCKS + printstyled( + @sprintf("\tBlock %d/%d statistics:\n", b, N_BLOCKS); + color=:yellow + ) println("\tHenry coeff. [mmol/(g-bar)]: ", henry_coefficients[b] / ρ) println("\t⟨U, vdw⟩ (K): ", average_energies[b].vdw) println("\t⟨U, es⟩ (K): ", average_energies[b].es) @@ -134,18 +172,21 @@ function henry_coefficient(crystal::Crystal, molecule::Molecule, temperature::Fl # compute error estimates err_kh = 2.0 * std(henry_coefficients) / sqrt(N_BLOCKS) err_energy = PotentialEnergy() - err_energy.vdw = 2.0 * std([average_energies[b].vdw for b = 1:N_BLOCKS]) / sqrt(N_BLOCKS) - err_energy.es = 2.0 * std([average_energies[b].es for b = 1:N_BLOCKS]) / sqrt(N_BLOCKS) + err_energy.vdw = + 2.0 * std([average_energies[b].vdw for b in 1:N_BLOCKS]) / sqrt(N_BLOCKS) + err_energy.es = 2.0 * std([average_energies[b].es for b in 1:N_BLOCKS]) / sqrt(N_BLOCKS) results = Dict{String, Any}() results["henry coefficient [mol/(m³-bar)]"] = mean(henry_coefficients) results["xtal"] = crystal.name - results["henry coefficient [mmol/(g-bar)]"] = results["henry coefficient [mol/(m³-bar)]"] / ρ + results["henry coefficient [mmol/(g-bar)]"] = + results["henry coefficient [mol/(m³-bar)]"] / ρ results["err henry coefficient [mmol/(g-bar)]"] = err_kh / ρ - results["henry coefficient [mol/(kg-Pa)]"] = results["henry coefficient [mmol/(g-bar)]"] / 100000.0 + results["henry coefficient [mol/(kg-Pa)]"] = + results["henry coefficient [mmol/(g-bar)]"] / 100000.0 # note assumes same # insertions per core. - results["⟨U, vdw⟩ (K)"] = mean([average_energies[b].vdw for b = 1:N_BLOCKS]) - results["⟨U, es⟩ (K)"] = mean([average_energies[b].es for b = 1:N_BLOCKS]) + results["⟨U, vdw⟩ (K)"] = mean([average_energies[b].vdw for b in 1:N_BLOCKS]) + results["⟨U, es⟩ (K)"] = mean([average_energies[b].es for b in 1:N_BLOCKS]) results["⟨U⟩ (K)"] = results["⟨U, vdw⟩ (K)"] + results["⟨U, es⟩ (K)"] results["⟨U⟩ (kJ/mol)"] = results["⟨U⟩ (K)"] * K_TO_KJ_PER_MOL @@ -159,11 +200,20 @@ function henry_coefficient(crystal::Crystal, molecule::Molecule, temperature::Fl results["elapsed time (min)"] = elapsed_time / 60 if autosave - if ! isdir(rc[:paths][:simulations]) + if !isdir(rc[:paths][:simulations]) mkdir(rc[:paths][:simulations]) end - savename = joinpath(rc[:paths][:simulations], henry_result_savename(crystal, molecule, temperature, - ljforcefield, insertions_per_volume, comment=filename_comment)) + savename = joinpath( + rc[:paths][:simulations], + henry_result_savename( + crystal, + molecule, + temperature, + ljforcefield, + insertions_per_volume; + comment=filename_comment + ) + ) @save savename results if verbose println("\tResults saved in: ", savename) @@ -173,7 +223,12 @@ function henry_coefficient(crystal::Crystal, molecule::Molecule, temperature::Fl if verbose println("\tElapsed time (min): ", results["elapsed time (min)"]) printstyled("\t----- final results ----\n"; color=:green) - for key in ["henry coefficient [mmol/(g-bar)]", "⟨U, vdw⟩ (kJ/mol)", "⟨U, es⟩ (kJ/mol)", "Qst (kJ/mol)"] + for key in [ + "henry coefficient [mmol/(g-bar)]", + "⟨U, vdw⟩ (kJ/mol)", + "⟨U, es⟩ (kJ/mol)", + "Qst (kJ/mol)" + ] @printf("\t%s = %f +/- %f\n", key, results[key], results["err " * key]) end end @@ -182,13 +237,20 @@ end # assumed crystal is already replicated sufficiently for short-range interactions # to facilitate parallelization -function _conduct_Widom_insertions(crystal::Crystal, molecule::Molecule, - temperature::Float64, ljforcefield::LJForceField, - nb_insertions::Int, charged_system::Bool, which_block::Int, - ewald_precision::Float64, verbose::Bool, - accessibility_grid::Union{Nothing, Grid{Bool}}, - repfactors::Tuple{Int, Int, Int}) - +function _conduct_Widom_insertions( + crystal::Crystal, + molecule::Molecule, + temperature::Float64, + ljforcefield::LJForceField, + nb_insertions::Int, + charged_system::Bool, + which_block::Int, + ewald_precision::Float64, + verbose::Bool, + accessibility_grid::Union{Nothing, Grid{Bool}}, + repfactors::Tuple{Int, Int, Int} +) + # copy the molecule in case we need to reset it when bond lengths drift bond_length_drift_check_frequency = 5000 # every how many insertions check for drift ref_molecule = deepcopy(molecule) @@ -197,19 +259,22 @@ function _conduct_Widom_insertions(crystal::Crystal, molecule::Molecule, # pre-compute weights on k-vector contributions to long-rage interactions in # Ewald summation for electrostatics # allocate memory for exp^{i * n * k ⋅ r} - eparams = setup_Ewald_sum(crystal.box, sqrt(ljforcefield.r²_cutoff), - verbose=(verbose & (myid() == 1) & charged_system), - ϵ=ewald_precision) + eparams = setup_Ewald_sum( + crystal.box, + sqrt(ljforcefield.r²_cutoff); + verbose=(verbose & (myid() == 1) & charged_system), + ϵ=ewald_precision + ) eikr = Eikr(crystal, eparams) # to be Σᵢ Eᵢe^(-βEᵢ) wtd_energy_sum = PotentialEnergy(0.0, 0.0) # to be Σᵢ e^(-βEᵢ) boltzmann_factor_sum = 0.0 - + insertion_start = 1 # which insertion number to start on - - for i = insertion_start:nb_insertions + + for i in insertion_start:nb_insertions # determine uniform random center of mass xf = rand(3) # translate molecule to the new center of mass @@ -225,7 +290,8 @@ function _conduct_Widom_insertions(crystal::Crystal, molecule::Molecule, if isnothing(accessibility_grid) || (accessible(accessibility_grid, xf, repfactors)) energy.vdw = vdw_energy(crystal, molecule, ljforcefield) if charged_system - energy.es = total(electrostatic_potential_energy(crystal, molecule, eparams, eikr)) + energy.es = + total(electrostatic_potential_energy(crystal, molecule, eparams, eikr)) end else energy.vdw = Inf @@ -238,7 +304,7 @@ function _conduct_Widom_insertions(crystal::Crystal, molecule::Molecule, boltzmann_factor_sum += boltzmann_factor # to avoid NaN; contribution to wtd energy sum when energy = Inf is zero. - if (! isinf(energy.vdw)) && (! isinf(energy.es)) + if (!isinf(energy.vdw)) && (!isinf(energy.es)) wtd_energy_sum += boltzmann_factor * energy end # else add 0.0 b/c lim E --> ∞ of E exp(-b * E) is zero. @@ -246,15 +312,27 @@ function _conduct_Widom_insertions(crystal::Crystal, molecule::Molecule, # check for drift in bond lengths if i % bond_length_drift_check_frequency == 0 # assert bond length drift not so severe as to not trust results - if distortion(molecule, ref_molecule, crystal.box, - atol=1e-5, throw_warning=true) - error("significant bond length drift observed after $i insertions. - Change `bond_length_drift_check_frequency` to restart the molecule configuration - more frequently.") + if distortion( + molecule, + ref_molecule, + crystal.box; + atol=1e-5, + throw_warning=true + ) + error( + "significant bond length drift observed after $i insertions. + Change `bond_length_drift_check_frequency` to restart the molecule configuration + more frequently." + ) end # reset molecule if bond length drift observed but not too severe - if distortion(molecule, ref_molecule, crystal.box, - atol=1e-12, throw_warning=true) + if distortion( + molecule, + ref_molecule, + crystal.box; + atol=1e-12, + throw_warning=true + ) @warn "... resetting molecule coordinates with fresh copy after $i insertions" molecule = deepcopy(ref_molecule) end @@ -274,21 +352,34 @@ many pieces of information from the simulation to ensure the file name accuratel describes what it holds. # Arguments -- `crystal::Crystal`: The porous crystal being tested -- `molecule::Molecule`: The molecule being tested inside the crystal -- `temperature::Float64`: The temperature used in the simulation units: Kelvin (K) -- `ljforcefield::LJForceField`: The molecular model being used in the simulation + + - `crystal::Crystal`: The porous crystal being tested + - `molecule::Molecule`: The molecule being tested inside the crystal + - `temperature::Float64`: The temperature used in the simulation units: Kelvin (K) + - `ljforcefield::LJForceField`: The molecular model being used in the simulation to describe the intermolecular Van der Waals forces -- `insertions_per_volume::Union{Int, Float64}`: The number of widom insertions per unit volume. + - `insertions_per_volume::Union{Int, Float64}`: The number of widom insertions per unit volume. Will be scaled according to the crystal we're working with -- `comment::AbstractString`: An optional comment that will be appended to the filename + - `comment::AbstractString`: An optional comment that will be appended to the filename """ -function henry_result_savename(crystal::Crystal, molecule::Molecule, temperature::Float64, - ljforcefield::LJForceField, insertions_per_volume::Union{Int, Float64}; comment::AbstractString="") +function henry_result_savename( + crystal::Crystal, + molecule::Molecule, + temperature::Float64, + ljforcefield::LJForceField, + insertions_per_volume::Union{Int, Float64}; + comment::AbstractString="" +) if comment != "" && comment[1] != '_' comment = "_" * comment end - return @sprintf("henry_sim_%s_in_%s_%fK_%s_ff_%d_insertions_per_volume%s.jld2", - molecule.species, crystal.name, temperature, ljforcefield.name, - insertions_per_volume, comment) + return @sprintf( + "henry_sim_%s_in_%s_%fK_%s_ff_%d_insertions_per_volume%s.jld2", + molecule.species, + crystal.name, + temperature, + ljforcefield.name, + insertions_per_volume, + comment + ) end diff --git a/src/isotherm_fitting.jl b/src/isotherm_fitting.jl index 59f4a17a3..f04f2da9f 100644 --- a/src/isotherm_fitting.jl +++ b/src/isotherm_fitting.jl @@ -1,6 +1,16 @@ +struct FitError <: Exception + method::AbstractString # will be Henry or Langmuir +end +Base.showerror(io::IO, e::FitError) = print(io, e.method, " fitting failed!") + # obtain a reasonable initial guess for the optimization route in fitting adsorption # models to adsorption isotherm data -function _guess(df::DataFrame, pressure_col_name::Symbol, loading_col_name::Symbol, model::Symbol) +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 || model == :henry @@ -27,7 +37,7 @@ end """ params = fit_adsorption_isotherm(df, pressure_col_name, loading_col_name, model) -Takes in a DataFrame `df` containing adsorption isotherm data and fits an analytical model +Takes in a DataFrame `df` containing adsorption isotherm data and fits an analytical model to the data to identify its parameters of best fit, returned as a dictionary. Available models are `:henry` and `:langmuir` @@ -40,36 +50,51 @@ 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 pressure of the gas. # 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 to the adsorption isotherm data + + - `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 to the adsorption isotherm data # 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 "H". + + - `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 "H". """ -function fit_adsorption_isotherm(df::DataFrame, pressure_col_name::Symbol, - loading_col_name::Symbol, model::Symbol, - options::Optim.Options=Optim.Options()) +function fit_adsorption_isotherm( + df::DataFrame, + pressure_col_name::Symbol, + loading_col_name::Symbol, + model::Symbol, + options::Optim.Options=Optim.Options() +) _df = sort(df, [pressure_col_name]) n = _df[!, loading_col_name] p = _df[!, pressure_col_name] θ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 = eachindex(n)]) - res = optimize(objective_function_langmuir, [θ0["M0"], θ0["K0"]], NelderMead(), options) + function objective_function_langmuir(θ) + return sum([ + (n[i] - θ[1] * θ[2] * p[i] / (1 + θ[2] * p[i]))^2 for i in eachindex(n) + ]) + end + res = optimize( + objective_function_langmuir, + [θ0["M0"], θ0["K0"]], + NelderMead(), + options + ) if !Optim.converged(res) - error("Optimization algorithm failed!") + throw(FitError(String(model))) end M, K = res.minimizer mse = res.minimum / length(n) return Dict("M" => M, "K" => K, "MSE" => mse) elseif model == :henry - objective_function_henry(θ) = return sum([(n[i] - θ[1] * p[i])^2 for i = eachindex(n)]) + objective_function_henry(θ) = sum([(n[i] - θ[1] * p[i])^2 for i in eachindex(n)]) res = optimize(objective_function_henry, [θ0["H0"]], LBFGS(), options) if !Optim.converged(res) - error("Optimization algorithm failed!") + throw(FitError(String(model))) end H = res.minimizer mse = res.minimum / length(n) diff --git a/src/mc_helpers.jl b/src/mc_helpers.jl index 9e5e31899..7ec233549 100644 --- a/src/mc_helpers.jl +++ b/src/mc_helpers.jl @@ -17,13 +17,18 @@ Insert a molecule into the simulation box and perform a random rotation if neede this function calls (`insert_w_random_orientation!`)[@ref], supplying it with a random position vector. # Arguments -- `molecules::Array{Molecule{Frac}, 1}`: array containing the molecules in the simulation -- `box::Box`: the box used for fractional coordinats -- `template::Molecule{Cart}`: reference molecule of the type inserted + + - `molecules::Array{Molecule{Frac}, 1}`: array containing the molecules in the simulation + - `box::Box`: the box used for fractional coordinats + - `template::Molecule{Cart}`: reference molecule of the type inserted """ -function random_insertion!(molecules::Array{Molecule{Frac}, 1}, box::Box, template::Molecule{Cart}) +function random_insertion!( + molecules::Array{Molecule{Frac}, 1}, + box::Box, + template::Molecule{Cart} +) xf_com = Frac(rand(3)) - insert_w_random_orientation!(molecules, box, template, xf_com) + return insert_w_random_orientation!(molecules, box, template, xf_com) end """ @@ -32,12 +37,18 @@ end Insert a molecule into the simulation box at a specified location and with a random orientation. # Arguments -- `molecules::Array{Molecule{Frac}, 1}`: array containing the molecules in the simulation -- `box::Box`: the box used for fractional coordinats -- `template::Molecule{Cart}`: reference molecule of the type inserted -- `xf_com`::Frac`: location where molecule will be inserted -""" -function insert_w_random_orientation!(molecules::Array{Molecule{Frac}, 1}, box::Box, template::Molecule{Cart}, xf_com::Frac) + + - `molecules::Array{Molecule{Frac}, 1}`: array containing the molecules in the simulation + - `box::Box`: the box used for fractional coordinats + - `template::Molecule{Cart}`: reference molecule of the type inserted + - `xf_com`::Frac`: location where molecule will be inserted +""" +function insert_w_random_orientation!( + molecules::Array{Molecule{Frac}, 1}, + box::Box, + template::Molecule{Cart}, + xf_com::Frac +) # copy template molecule = deepcopy(template) # rotate @@ -48,7 +59,7 @@ function insert_w_random_orientation!(molecules::Array{Molecule{Frac}, 1}, box:: molecule = Frac(molecule, box) translate_to!(molecule, xf_com) # add the molecule to the array of molecules - push!(molecules, molecule) + return push!(molecules, molecule) end """ @@ -57,23 +68,26 @@ end Remove a molecule from the array of molecules. # Arguments -- `molecule_id::Int`: the ID of the molecule to be removed -- `molecules::Array{<:Molecule, 1}`: array of molecules to be modified + + - `molecule_id::Int`: the ID of the molecule to be removed + - `molecules::Array{<:Molecule, 1}`: array of molecules to be modified """ function remove_molecule!(molecule_id::Int, molecules::Array{<:Molecule, 1}) - splice!(molecules, molecule_id) + return splice!(molecules, molecule_id) end """ apply_periodic_boundary_condition!(molecule::Molecule{Frac}) -Check if each of a molecule's center-of-mass coordinates is within the bounds (0.0, 1.0) in fractional coordinates, and translate if needed. +Check if each of a molecule's center-of-mass coordinates is within the bounds (0.0, 1.0) in fractional coordinates, and translate if needed. # Arguments -- `molecule::Molecule{Frac}`: the molecule to be checked + + - `molecule::Molecule{Frac}`: the molecule to be checked # Returns -- `nothing`, if the molecule is within the boundary; ohterwise, the coordinates of the input molecule will be modified + + - `nothing`, if the molecule is within the boundary; ohterwise, the coordinates of the input molecule will be modified """ function apply_periodic_boundary_condition!(molecule::Molecule{Frac}) # based on center of mass @@ -85,7 +99,7 @@ function apply_periodic_boundary_condition!(molecule::Molecule{Frac}) new_com = deepcopy(molecule.com) # apply periodic boundary conditions - for k = 1:3 # loop over xf, yf, zf components + for k in 1:3 # loop over xf, yf, zf components # if > 1.0, shift down if new_com.xf[k] >= 1.0 new_com.xf[k] -= 1.0 @@ -95,7 +109,7 @@ function apply_periodic_boundary_condition!(molecule::Molecule{Frac}) end # translate molecule to new center of mass if it was found to be outside of the box - translate_to!(molecule, new_com) + return translate_to!(molecule, new_com) end ### @@ -114,10 +128,11 @@ AdaptiveTranslationStepSize(δ::Float64) = AdaptiveTranslationStepSize(δ, 0, 0) """ function adjust!(adaptive_δ::AdaptiveTranslationStepSize) r = 1.2 # factor by which to scale the step. - + # calculate current acceptance rate - acceptance_rate = adaptive_δ.nb_translations_accepted / adaptive_δ.nb_translations_proposed - + acceptance_rate = + adaptive_δ.nb_translations_accepted / adaptive_δ.nb_translations_proposed + # adjust step if acceptance rate not in [0.4, 0.5] if acceptance_rate > 0.5 # acceptance rate is too high. make step more aggressive. @@ -131,7 +146,7 @@ function adjust!(adaptive_δ::AdaptiveTranslationStepSize) # reset counter to only use translation acceptance stats for this block adaptive_δ.nb_translations_proposed = 0 - adaptive_δ.nb_translations_accepted = 0 + return adaptive_δ.nb_translations_accepted = 0 end """ @@ -140,20 +155,26 @@ end Perform a translational perturbation in Cartesian coordinates on a molecule, apply the periodic boundry conditions, and keep a copy of the original in case it needs to be restored. # Arguements -- `molecule::Molecule{Frac}`: molecule to be translated -- `box::Box`: the box used in rotation + + - `molecule::Molecule{Frac}`: molecule to be translated + - `box::Box`: the box used in rotation # Returns -- `old_molecule::Molecule{Frac}`: a copy of the original molecule + + - `old_molecule::Molecule{Frac}`: a copy of the original molecule """ -function random_translation!(molecule::Molecule{Frac}, box::Box, adaptive_δ::AdaptiveTranslationStepSize) +function random_translation!( + molecule::Molecule{Frac}, + box::Box, + adaptive_δ::AdaptiveTranslationStepSize +) # store old molecule and return at the end for possible restoration old_molecule = deepcopy(molecule) # peturb in Cartesian coords in a random cube centered at current coords. dx = Cart(adaptive_δ.δ * (rand(3) .- 0.5)) # move every atom of the molecule by the same vector. translate_by!(molecule, dx, box) - + # done, unless the molecule has moved outside of the box, then apply PBC apply_periodic_boundary_condition!(molecule) @@ -166,11 +187,13 @@ end Perform a translation and rotated (if needed) on a molecule, and keep a copy of the original in case it needs to be restored. # Arguements -- `molecule::Molecule{Frac}`: molecule to be translated and rotated (if needed) -- `box::Box`: the box used in rotation + + - `molecule::Molecule{Frac}`: molecule to be translated and rotated (if needed) + - `box::Box`: the box used in rotation # Returns -- `old_molecule::Molecule{Frac}`: a copy of the original molecule + + - `old_molecule::Molecule{Frac}`: a copy of the original molecule """ function random_reinsertion!(molecule::Molecule{Frac}, box::Box) # store old molecule and return at the end for possible restoration diff --git a/src/molecule.jl b/src/molecule.jl index 91d45689d..3dcc81c9b 100644 --- a/src/molecule.jl +++ b/src/molecule.jl @@ -2,10 +2,11 @@ Data structure for a molecule/adsorbate. # Attributes -- `species::Symbol`: Species of molecule, e.g. `:CO2` -- `atoms::Atoms`: array of Lennard-Jones spheres comprising the molecule -- `charges::Charges`: array of point charges comprising the molecule -- `com::Coords`: center of mass + + - `species::Symbol`: Species of molecule, e.g. `:CO2` + - `atoms::Atoms`: array of Lennard-Jones spheres comprising the molecule + - `charges::Charges`: array of point charges comprising the molecule + - `com::Coords`: center of mass """ struct Molecule{T} # T = Frac or Cart species::Symbol @@ -14,44 +15,47 @@ struct Molecule{T} # T = Frac or Cart com::T # center of mass end - function Base.isapprox(m1::Molecule, m2::Molecule) - return (m1.species == m2.species) && isapprox(m1.com, m2.com) && - isapprox(m1.atoms, m2.atoms) && isapprox(m1.charges, m2.charges) + return (m1.species == m2.species) && + isapprox(m1.com, m2.com) && + isapprox(m1.atoms, m2.atoms) && + isapprox(m1.charges, m2.charges) end - function center_of_mass(molecule::Molecule{Cart}) - masses = [rc[:atomic_masses][x] for x ∈ molecule.atoms.species] + masses = [rc[:atomic_masses][x] for x in molecule.atoms.species] total_mass = sum(masses) x_com = [0.0, 0.0, 0.0] # center of mass if total_mass == 0.0 - for a ∈ 1:molecule.atoms.n + for a in 1:(molecule.atoms.n) x_com += molecule.atoms.coords.x[:, a] end total_mass = length(masses) else - for a = 1:molecule.atoms.n - x_com += rc[:atomic_masses][molecule.atoms.species[a]] * molecule.atoms.coords.x[:, a] + for a in 1:(molecule.atoms.n) + x_com += + rc[:atomic_masses][molecule.atoms.species[a]] * + molecule.atoms.coords.x[:, a] end end return Cart(x_com / total_mass) end - """ molecule = Molecule(species, check_neutrality=true) construt a `Molecule` from input files read from `joinpath(rc[:paths][:molecules], species)` -center of mass assigned using atomic masses from `rc[:atomic_masses]`. +center of mass assigned using atomic masses from `rc[:atomic_masses]`. # Arguments -- `species::String`: name of the molecule -- `check_neutrality::Bool`: assert the molecule is charge neutral for safety. + + - `species::String`: name of the molecule + - `check_neutrality::Bool`: assert the molecule is charge neutral for safety. # Returns -- `molecule::Molecule{Cart}`: a molecule in Cartesian coordinates + + - `molecule::Molecule{Cart}`: a molecule in Cartesian coordinates """ function Molecule(species::String; check_neutrality::Bool=true) ### @@ -75,38 +79,45 @@ function Molecule(species::String; check_neutrality::Bool=true) charges.q[c] = row[:q] charges.coords.x[:, c] = [row[:x], row[:y], row[:z]] end - + molecule = Molecule(Symbol(species), atoms, charges, Cart([NaN, NaN, NaN])) # compute center of mass molecule.com.x .= center_of_mass(molecule).x # check for charge neutrality - if (! neutral(molecule.charges)) && check_neutrality - error(@sprintf("Molecule %s is not charge neutral! Pass - `check_neutrality=false` to ignore this error message.", species)) + if (!neutral(molecule.charges)) && check_neutrality + error(@sprintf( + "Molecule %s is not charge neutral! Pass +`check_neutrality=false` to ignore this error message.", + species + )) end return molecule end - # documented in matter.jl import Xtals.net_charge net_charge(molecule::Molecule) = net_charge(molecule.charges) - # convert between fractional and cartesian coords -Frac(molecule::Molecule{Cart}, box::Box) = Molecule(molecule.species, - Frac(molecule.atoms, box), - Frac(molecule.charges, box), - Frac(molecule.com, box) - ) -Cart(molecule::Molecule{Frac}, box::Box) = Molecule(molecule.species, - Cart(molecule.atoms, box), - Cart(molecule.charges, box), - Cart(molecule.com, box) - ) +function Frac(molecule::Molecule{Cart}, box::Box) + return Molecule( + molecule.species, + Frac(molecule.atoms, box), + Frac(molecule.charges, box), + Frac(molecule.com, box) + ) +end +function Cart(molecule::Molecule{Frac}, box::Box) + return Molecule( + molecule.species, + Cart(molecule.atoms, box), + Cart(molecule.charges, box), + Cart(molecule.com, box) + ) +end """ write_xyz(box, molecules, xyz_file) @@ -117,17 +128,22 @@ once at the beginning of the simulation and closed at the end. This writes the coordinates of the molecules in cartesian coordinates, so the box is needed for the conversion. # Arguments - - `box::Box`: The box the molecules are in, to convert molecule positions - to cartesian coordinates - - `molecules::Array{Array{Molecule{Frac}, 1}, 1}`: The array containing arrays of molecules, separated by species, to be written to the file - - `xyz_file::IOStream`: The open 'write' file stream the data will be saved to + + - `box::Box`: The box the molecules are in, to convert molecule positions + to cartesian coordinates + - `molecules::Array{Array{Molecule{Frac}, 1}, 1}`: The array containing arrays of molecules, separated by species, to be written to the file + - `xyz_file::IOStream`: The open 'write' file stream the data will be saved to """ -function write_xyz(box::Box, molecules::Array{Array{Molecule{Frac}, 1}, 1}, xyz_file::IOStream) +function write_xyz( + box::Box, + molecules::Array{Array{Molecule{Frac}, 1}, 1}, + xyz_file::IOStream +) num_atoms = sum([sum([mol.atoms.n for mol in sp]) for sp in molecules]) @printf(xyz_file, "%s\n", num_atoms) for molecules_species in molecules for molecule in molecules_species - for i = 1:molecule.atoms.n + for i in 1:(molecule.atoms.n) x = Cart(molecule.atoms[i].coords, box) @printf(xyz_file, "\n%s %f %f %f", molecule.atoms.species[i], x.x...) end @@ -135,30 +151,28 @@ function write_xyz(box::Box, molecules::Array{Array{Molecule{Frac}, 1}, 1}, xyz_ end end - # documented in matter.jl import Xtals.translate_by! function translate_by!(molecule::Molecule{Cart}, dx::Cart) - translate_by!(molecule.atoms.coords, dx) + translate_by!(molecule.atoms.coords, dx) translate_by!(molecule.charges.coords, dx) - translate_by!(molecule.com, dx) + return translate_by!(molecule.com, dx) end function translate_by!(molecule::Molecule{Frac}, dxf::Frac) - translate_by!(molecule.atoms.coords, dxf) + translate_by!(molecule.atoms.coords, dxf) translate_by!(molecule.charges.coords, dxf) - translate_by!(molecule.com, dxf) + return translate_by!(molecule.com, dxf) end function translate_by!(molecule::Molecule{Cart}, dxf::Frac, box::Box) - translate_by!(molecule, Cart(dxf, box)) + return translate_by!(molecule, Cart(dxf, box)) end function translate_by!(molecule::Molecule{Frac}, dx::Cart, box::Box) - translate_by!(molecule, Frac(dx, box)) + return translate_by!(molecule, Frac(dx, box)) end - """ translate_to!(molecule, xf) translate_to!(molecule, x) @@ -170,18 +184,21 @@ Cartesian coordinate space. For the latter, a unit cell box is required for cont """ function translate_to!(molecule::Molecule{Cart}, x::Cart) dx = Cart(x.x - molecule.com.x) - translate_by!(molecule, dx) + return translate_by!(molecule, dx) end function translate_to!(molecule::Molecule{Frac}, xf::Frac) dxf = Frac(xf.xf - molecule.com.xf) - translate_by!(molecule, dxf) + return translate_by!(molecule, dxf) end -translate_to!(molecule::Molecule{Frac}, x::Cart, box::Box) = translate_to!(molecule, Frac(x, box)) - -translate_to!(molecule::Molecule{Cart}, xf::Frac, box::Box) = translate_to!(molecule, Cart(xf, box)) +function translate_to!(molecule::Molecule{Frac}, x::Cart, box::Box) + return translate_to!(molecule, Frac(x, box)) +end +function translate_to!(molecule::Molecule{Cart}, xf::Frac, box::Box) + return translate_to!(molecule, Cart(xf, box)) +end function Base.show(io::IO, molecule::Molecule) println(io, "Molecule species: ", molecule.species) @@ -189,38 +206,53 @@ function Base.show(io::IO, molecule::Molecule) if molecule.atoms.n > 0 print(io, "Atoms:\n") if typeof(molecule.atoms.coords) == Frac - for i = 1:molecule.atoms.n - @printf(io, "\n\tatom = %s, xf = [%.3f, %.3f, %.3f]", molecule.atoms.species[i], - molecule.atoms.coords[i].xf...) + for i in 1:(molecule.atoms.n) + @printf( + io, + "\n\tatom = %s, xf = [%.3f, %.3f, %.3f]", + molecule.atoms.species[i], + molecule.atoms.coords[i].xf... + ) end elseif typeof(molecule.atoms.coords) == Cart - for i = 1:molecule.atoms.n - @printf(io, "\n\tatom = %s, x = [%.3f, %.3f, %.3f]", molecule.atoms.species[i], - molecule.atoms.coords[i].x...) + for i in 1:(molecule.atoms.n) + @printf( + io, + "\n\tatom = %s, x = [%.3f, %.3f, %.3f]", + molecule.atoms.species[i], + molecule.atoms.coords[i].x... + ) end end end if molecule.charges.n > 0 print(io, "\nPoint charges: ") if typeof(molecule.charges.coords) == Frac - for i = 1:molecule.charges.n - @printf(io, "\n\tcharge = %f, xf = [%.3f, %.3f, %.3f]", molecule.charges.q[i], - molecule.charges.coords[i].xf...) + for i in 1:(molecule.charges.n) + @printf( + io, + "\n\tcharge = %f, xf = [%.3f, %.3f, %.3f]", + molecule.charges.q[i], + molecule.charges.coords[i].xf... + ) end elseif typeof(molecule.charges.coords) == Cart - for i = 1:molecule.charges.n - @printf(io, "\n\tcharge = %f, x = [%.3f, %.3f, %.3f]", molecule.charges.q[i], - molecule.charges.coords[i].x...) + for i in 1:(molecule.charges.n) + @printf( + io, + "\n\tcharge = %f, x = [%.3f, %.3f, %.3f]", + molecule.charges.q[i], + molecule.charges.coords[i].x... + ) end end end end - """ r = random_rotation_matrix() # rotation matrix in cartesian coords -Generate a 3x3 random rotation matrix `r` such that when a point `x` is rotated using this rotation matrix via `r * x`, +Generate a 3x3 random rotation matrix `r` such that when a point `x` is rotated using this rotation matrix via `r * x`, this point `x` is placed at a uniform random distributed position on the surface of a sphere of radius `norm(x)`. the point `x` is in Cartesian coordinates here. See James Arvo. Fast Random Rotation Matrices. @@ -228,7 +260,8 @@ See James Arvo. Fast Random Rotation Matrices. https://pdfs.semanticscholar.org/04f3/beeee1ce89b9adf17a6fabde1221a328dbad.pdf # Returns -- `r::Array{Float64, 2}`: A 3x3 random rotation matrix + + - `r::Array{Float64, 2}`: A 3x3 random rotation matrix """ function random_rotation_matrix() # random rotation about the z-axis @@ -240,12 +273,11 @@ function random_rotation_matrix() u₃ = rand() v = [cos(u₂) * sqrt(u₃), sin(u₂) * sqrt(u₃), sqrt(1.0 - u₃)] h = Matrix{Float64}(I, 3, 3) - 2 * v * transpose(v) - return - h * r + return -h * r end random_rotation_matrix(box::Box) = box.c_to_f * random_rotation_matrix() * box.f_to_c - """ random_rotation!(molecule{Frac}, box) random_rotation!(molecule{Cart}) @@ -280,93 +312,119 @@ function random_rotation!(molecule::Molecule{Frac}, box::Box) return nothing end - # based on center of mass import Xtals.inside inside(molecule::Molecule{Cart}, box::Box) = inside(molecule.com, box) inside(molecule::Molecule{Frac}) = inside(molecule.com) - # docstring in Misc.jl -function write_xyz(molecules::Array{Molecule{Cart}, 1}, filename::AbstractString; - comment::AbstractString="") - +function write_xyz( + molecules::Array{Molecule{Cart}, 1}, + filename::AbstractString; + comment::AbstractString="" +) + # append all atoms of the molecule together atoms = sum([molecule.atoms for molecule in molecules]) - + if isa(atoms.coords, Frac) # convert to Cartesian atoms = Cart(atoms, box) end - + # send to write_xyz for writing atoms in Cartesian coords. - write_xyz(atoms, filename, comment=comment) # Misc.jl + return write_xyz(atoms, filename; comment=comment) # Misc.jl end -function write_xyz(molecules::Array{Molecule{Frac}, 1}, box::Box, filename::AbstractString; - comment::AbstractString="") - +function write_xyz( + molecules::Array{Molecule{Frac}, 1}, + box::Box, + filename::AbstractString; + comment::AbstractString="" +) molecules = Cart.(molecules, box) - - write_xyz(molecules, filename, comment=comment) # above + + return write_xyz(molecules, filename; comment=comment) # above end -function write_xyz(molecules::Array{Array{Molecule{Frac}, 1}, 1}, box::Box, filename::AbstractString) - all_molecules = [molecule for molecules_species in molecules for molecule in molecules_species] +function write_xyz( + molecules::Array{Array{Molecule{Frac}, 1}, 1}, + box::Box, + filename::AbstractString +) + all_molecules = + [molecule for molecules_species in molecules for molecule in molecules_species] - write_xyz(all_molecules, box, filename) # above + return write_xyz(all_molecules, box, filename) # above end # documented in crystal.jl import Xtals.has_charges has_charges(molecule::Molecule) = molecule.charges.n > 0 - # documented in forcefield.jl -forcefield_coverage(molecule::Molecule, ljff::LJForceField) = forcefield_coverage(molecule.atoms, ljff) - +function forcefield_coverage(molecule::Molecule, ljff::LJForceField) + return forcefield_coverage(molecule.atoms, ljff) +end """ molecule = ion(q, coords) + Facilitate constructing a point charge by constructing a molecule: Molecule(:ion, Atoms{Frac}(0), Charges(q, coords), coords) # Arguments -- `q::Float64`: value of point charge, units: electrons -- `coords::Frac`: fractional coordinates of the charge + + - `q::Float64`: value of point charge, units: electrons + - `coords::Frac`: fractional coordinates of the charge + # Returns -- `molecule::Molecule{Frac}`: the ion as a molecule with Fractional coordinates + + - `molecule::Molecule{Frac}`: the ion as a molecule with Fractional coordinates """ function ion(q::Float64, coords::Frac) @assert size(coords.xf, 2) == 1 return Molecule(:ion, Atoms{Frac}(0), Charges(q, coords), coords) end - """ is_distorted = distortion(molecule, ref_molecule, box; atol=1e-12, throw_warning=true) Determine whether a molecule has distortion w.r.t. a reference molecule via pairwise distance comparison of the atoms and charges coordinates. + # Arguments -- `molecule::Molecule{Frac}`: molecule you want to compare -- `ref_molecule::Molecule{Frac}`: reference molecule -- `box::Box`: box used for the fractional coordinates -- `atol::Float64=1e-12`: absolute tolerance for distance comparison -- `throw_warning::Bool=true`: issue a warning if there is distortion + + - `molecule::Molecule{Frac}`: molecule you want to compare + - `ref_molecule::Molecule{Frac}`: reference molecule + - `box::Box`: box used for the fractional coordinates + - `atol::Float64=1e-12`: absolute tolerance for distance comparison + - `throw_warning::Bool=true`: issue a warning if there is distortion # Returns -- `is_distorted::Bool`: true if there is distortion w.r.t. reference molecule + + - `is_distorted::Bool`: true if there is distortion w.r.t. reference molecule """ -function distortion(molecule::Molecule{Frac}, ref_molecule::Molecule{Frac}, box::Box; - atol::Float64=1e-12, throw_warning::Bool=true) +function distortion( + molecule::Molecule{Frac}, + ref_molecule::Molecule{Frac}, + box::Box; + atol::Float64=1e-12, + throw_warning::Bool=true +) @assert molecule.species == ref_molecule.species - if ! isapprox(pairwise_distances(molecule.atoms.coords, box, false), - pairwise_distances(ref_molecule.atoms.coords, box, false), atol=atol) + if !isapprox( + pairwise_distances(molecule.atoms.coords, box, false), + pairwise_distances(ref_molecule.atoms.coords, box, false); + atol=atol + ) return true end - if ! isapprox(pairwise_distances(molecule.charges.coords, box, false), - pairwise_distances(ref_molecule.charges.coords, box, false), atol=atol) + if !isapprox( + pairwise_distances(molecule.charges.coords, box, false), + pairwise_distances(ref_molecule.charges.coords, box, false); + atol=atol + ) return true end return false # if made it this far... diff --git a/src/muvt.jl b/src/muvt.jl new file mode 100644 index 000000000..9bda91a7b --- /dev/null +++ b/src/muvt.jl @@ -0,0 +1,945 @@ +module MuVT + +import ..Molecule, + ..Frac, + ..Crystal, + ..LJForceField, + ..@printf, + ..@sprintf, + ..@save, + ..Cart, + ..EwaldParams, + ..Eikr + +### +# Markov chain proposals +### +const PROPOSAL_ENCODINGS = Dict( + 1 => "insertion", + 2 => "deletion", + 3 => "translation", + 4 => "rotation", + 5 => "reinsertion", + 6 => "identity change" +) # helps with printing later +const N_PROPOSAL_TYPES = length(keys(PROPOSAL_ENCODINGS)) +# each proposal type gets an Int for clearer code +const INSERTION = Dict([v => k for (k, v) in PROPOSAL_ENCODINGS])["insertion"] +const DELETION = Dict([v => k for (k, v) in PROPOSAL_ENCODINGS])["deletion"] +const TRANSLATION = Dict([v => k for (k, v) in PROPOSAL_ENCODINGS])["translation"] +const ROTATION = Dict([v => k for (k, v) in PROPOSAL_ENCODINGS])["rotation"] +const REINSERTION = Dict([v => k for (k, v) in PROPOSAL_ENCODINGS])["reinsertion"] +const IDENTITY_CHANGE = Dict([v => k for (k, v) in PROPOSAL_ENCODINGS])["identity change"] + +# TODO move this to MC helpers? but not sure if it will inline. so wait after test with @time +# potential energy change after inserting/deleting/perturbing coordinates of molecules[which_species][molecule_id] +# compute potential energy of molecules[which_species][molecule_id] with all other molecules and with the framework. +@inline function potential_energy( + which_species::Int, + molecule_id::Int, + molecules::Array{Array{Molecule{Frac}, 1}, 1}, + xtal::Crystal, + ljff::LJForceField +) + energy = SystemPotentialEnergy() + # van der Waals interactions + energy.gg.vdw = vdw_energy(which_species, molecule_id, molecules, ljff, xtal.box) # guest-guest + energy.gh.vdw = vdw_energy(xtal, molecules[which_species][molecule_id], ljff) # guest-host + # TODO electrostatic interactions (being neglected for now) + energy.gg.es = 0.0 + energy.gh.es = 0.0 + return energy +end + +""" + results, molecules = μVT_sim(xtal, molecule_templates, temperature, pressure, + ljff; molecules=Array{Molecule, 1}[], settings=settings) + +Runs a grand-canonical (μVT) Monte Carlo simulation of the adsorption of a molecule in a +xtal at a particular temperature and pressure using a +Lennard Jones force field. + +Markov chain Monte Carlo moves include: + + - deletion/insertion + - translation + - reinsertion + - identity change (if multiple components) [see here](http://dx.doi.org/10.1080/00268978800100743) + +Translation stepsize is dynamically updated during burn cycles so that acceptance rate of translations is ~0.4. + +A cycle is defined as max(20, number of adsorbates currently in the system) Markov chain +proposals. + +# Arguments + + - `xtal::Crystal`: the porous xtal in which we seek to simulate adsorption + - `molecule_templates::Array{Molecule, 1}`: an array of the templates of unique adsorbate molecules of which we seek to simulate + - `temperature::Float64`: temperature of bulk gas phase in equilibrium with adsorbed phase + in the porous material. units: Kelvin (K) + - `pressures::Array{Float64, 1}`: pressure of bulk gas phase in equilibrium with adsorbed phase in the + porous material for each adsorbate. units: bar + the adsorption + - `ljff::LJForceField`: the molecular model used to describe the + - `molecules::Array{Array{Molecule{Cart}, 1}, 1}`: a starting configuration of molecules in the xtal with an array per species. + - `n_cycles_per_volume::Int`: the number of MC cycles per volume, split evenly between `n_burn_cycles` and `'n_sample_cycles`, where + - `fraction_burn_cycles::Float64`: the proportion of the MC cycles to burn before sampling + - `sample_frequency::Int`: during the sampling cycles, sample e.g. the number of + adsorbed gas molecules every this number of Markov proposals + - `verbose::Bool`: whether or not to print off information during the simulation + - `ewald_precision::Float64`: desired precision for the long range Ewald summation + - `eos::Symbol`: equation of state to use for calculation of fugacity from pressure + - `write_adsorbate_snapshots::Bool`: whether the simulation will create and save a snapshot file + - `snapshot_frequency::Int`: the number of cycles taken between each snapshot (after burn cycle completion) + - `calculate_density_grid::Bool`: whether the simulation will keep track of a density grid for adsorbates + - `density_grid_dx::Float64`: The (approximate) space between voxels (in Angstroms) in the density grid. The number of voxels in the simulation box is computed automatically by [`required_n_pts`](@ref). + - `density_grid_molecular_species::Symbol`: the adsorbate for which we will make a density grid of its position (center). + - `density_grid_sim_box::Bool`: `true` if we wish for the density grid to be over the + entire simulation box as opposed to the box of the crystal passed in. `false` if we wish the + density grid to be over the original `xtal.box`, before replication, passed in. + - `autosave::Bool`: `true` if we wish to automatically save the simulation results to the standard path/filename. + - `results_filename_comment::AbstractString`: An optional comment that will be appended to the name of the saved file (if autosaved) +""" +function μVT_sim( + xtal::Crystal, + molecule_templates::Array{Molecule{Cart}, 1}, + temperature::Float64, + pressures::Array{Float64, 1}, + ljff::LJForceField; + molecules::Union{Nothing, Array{Array{Molecule{Cart}, 1}, 1}}=nothing, + n_cycles_per_volume::Int=200, + fraction_burn_cycles::Float64=0.5, + sample_frequency::Int=1, + verbose::Bool=true, + ewald_precision::Float64=1e-6, + eos::Symbol=:ideal, + autosave::Bool=true, + show_progress_bar::Bool=false, + write_adsorbate_snapshots::Bool=false, + snapshot_frequency::Int=1, + calculate_density_grid::Bool=false, + density_grid_dx::Float64=1.0, + density_grid_molecular_species::Union{Nothing, Symbol}=nothing, + density_grid_sim_box::Bool=true, + results_filename_comment::String="" +) + assert_P1_symmetry(xtal) + + start_time = time() + # # to avoid changing the outside object `molecule_` inside this function, we make + # # a deep copy of it here. this serves as a template to copy when we insert a new molecule. + # molecule = deepcopy(molecule_) + + # calculate the number of MC cycles + # separate total number of cycles evenly into burn_cycles and sample_cycles + nb_cycles = max(N_BLOCKS, ceil(Int, n_cycles_per_volume * xtal.box.Ω)) + if (0.0 < fraction_burn_cycles) && (fraction_burn_cycles < 1.0) + n_burn_cycles = ceil(Int, nb_cycles * fraction_burn_cycles) + n_sample_cycles = ceil(Int, nb_cycles * (1 - fraction_burn_cycles)) + elseif fraction_burn_cycles == 0 + n_burn_cycles = 0 + n_sample_cycles = nb_cycles + end + + nb_species = length(molecule_templates) + molecular_species = [mt.species for mt in molecule_templates] + + if nb_species != length(pressures) + error("# molecules in simulation: $nb_species + # partial pressures: $(length(pressures)) + these should be equal!") + end + + if verbose + pretty_print(xtal, molecule_templates, temperature, pressures, ljff) + println("\t# burn cycles: ", n_burn_cycles) + println("\t# sample cycles: ", n_sample_cycles) + end + + ### + # xyz file for storing snapshots of adsorbate positions + ### + num_snapshots = 0 + xyz_snapshots_filename = μVT_output_filename( + xtal, + molecule_templates, + temperature, + pressures, + ljff, + n_burn_cycles, + n_sample_cycles; + extension=".xyz" + ) + xyz_snapshot_file = IOStream(xyz_snapshots_filename) # declare a variable outside of scope so we only open a file if we want to snapshot + if write_adsorbate_snapshots + xyz_snapshot_file = open(xyz_snapshots_filename, "w") + end + + ### + # Convert pressure to fugacity (units: Pascal) using an equation of state + ### + # TODO make PengRobinson work for multiple species (ignore for now) + fugacities = [NaN for s in 1:nb_species] # Pa + if eos == :ideal + fugacities = pressures * 100000.0 # bar --> Pa + elseif eos == :PengRobinson + if nb_species != 1 + error("Peng Robinson EOS not implemented for >1 species") + end + prfluid = PengRobinsonFluid(molecule_templates[1].species) + gas_props = calculate_properties(prfluid, temperature, pressures[1]; verbose=false) + fugacities[1] = gas_props["fugacity (bar)"] * 100000.0 # bar --> Pa + else + error( + "eos=:ideal and eos=:PengRobinson are the only valid options for an equation of state." + ) + end + if verbose + for s in 1:nb_species + @printf( + "\t%s equation of state, %s partial fugacity = %f bar\n", + eos, + molecule_templates[s].species, + fugacities[s] / 100000.0 + ) + end + end + + ### + # replicate xtal so that nearest image convention can be applied for short-range interactions + ### + repfactors = replication_factors(xtal.box, ljff) + original_xtal_box = deepcopy(xtal.box) + xtal = replicate(xtal, repfactors) # frac coords still in [0, 1] + + ### + # prep molecules array + ### + if isnothing(molecules) + # populate the molecules array with an empty array for each species being simulated + molecules = [Molecule[] for i in 1:nb_species] + else + if length(molecules) != nb_species + error( + "Length of molecules array $(length(molecules)) is not equal to length of molecule_templates $(nb_species).\n" + ) + end + end + # convert molecules array to fractional using this box. + molecules = [Frac.(molecules_species, xtal.box) for molecules_species in molecules] + + ### + # Density grid for adsorbate + # (if more than one adsorbate, user must specify which adsorbate species to make + # the grid for) + ### + if calculate_density_grid && isnothing(density_grid_molecular_species) + if nb_species == 1 + # obviously we are keeping track of the only atom in the adsorbate. + density_grid_molecular_species = molecule_templates[1].species + else + # cannot proceed if we do not know which molecule to keep track of! + error( + @printf( + "Passed `calculate_density_grid=true` but there is %d + different adsorbates in the system. Pass `density_grid_molecular_species` to specify + which adsorbate to make a grid for.", + nb_species + ) + ) + end + end + + if calculate_density_grid + # keep track of the id for the density_grid_molecular_species + molecule_species_id_for_density_grid = + findfirst(density_grid_molecular_species .== molecular_species) + @assert ( + (molecule_species_id_for_density_grid != 0) && + (molecule_species_id_for_density_grid <= length(molecule_templates)) + ) "density_grid_molecular_species not found in molecule_templates" + end + + # Initialize a density grid based on the *simulation box* (not xtal box passed in) and the passed in density_grid_dx + # Calculate `n_pts`, number of voxels in grid, based on the sim box and specified voxel spacing + n_pts = (0, 0, 0) # don't store a huge grid if we aren't tracking a density grid + if calculate_density_grid + if density_grid_sim_box + n_pts = required_n_pts(xtal.box, density_grid_dx) + else + n_pts = required_n_pts(original_xtal_box, density_grid_dx) + end + end + density_grid = Grid( + density_grid_sim_box ? xtal.box : original_xtal_box, + n_pts, + zeros(n_pts...), + :inverse_A3, + [0.0, 0.0, 0.0] + ) + + if verbose + println("\tthe crystal:") + @printf( + "\t\treplicated (%d,%d,%d) for short-range cutoff of %f Å\n", + repfactors[1], + repfactors[2], + repfactors[3], + sqrt(ljff.r²_cutoff) + ) + println("\t\tdensity [kg/m³]: ", crystal_density(xtal)) + println("\t\tchemical formula: ", chemical_formula(xtal)) + println("\t\t# atoms: ", xtal.atoms.n) + println("\t\t# point charges: ", xtal.charges.n) + println("\tthe molecules:") + for s in 1:nb_species + println("\t", molecule_templates[s].species) + println("\t\tunique species: ", unique(molecule_templates[s].atoms.species)) + println("\t\t# atoms: ", molecule_templates[s].atoms.n) + println("\t\t# point charges: ", molecule_templates[s].charges.n) + end + if write_adsorbate_snapshots + @printf( + "\tWriting snapshots of adsorption positions every %d cycles (after burn cycles)\n", + snapshot_frequency + ) + @printf("\t\tWriting to file: %s\n", xyz_snapshots_filename) + end + if calculate_density_grid + @printf( + "\tTracking adsorbate spatial probability density grid of adsorbate %s, updated every %d cycles (after burn cycles)\n", + density_grid_molecular_species, + snapshot_frequency + ) + @printf( + "\t\tdensity grid voxel spacing specified as %.3f Å => %d by %d by %d voxels\n", + density_grid_dx, + n_pts... + ) + if density_grid_sim_box + @printf("\t\tdensity grid is over simulation box\n") + else + @printf("\t\tdensity grid is over original crystal box\n") + end + end + end + if !forcefield_coverage(xtal.atoms, ljff) + error("crystal $(xtal.name) not covered by the force field $(ljff.name)") + end + + for s in 1:nb_species + if !neutral(molecule_templates[s].charges) + error( + @sprintf( + "Molecule %s is not charge neutral!\n", + molecule_templates[s].species + ) + ) + end + if !forcefield_coverage(molecule_templates[s].atoms, ljff) + error( + "molecule $(molecule_templates[s].species) not covered by the force field $(ljff.name)" + ) + end + end + + # TODO electrostatics + electrostatics_flag = has_charges(xtal) && any(has_charges.(molecule_templates)) + if electrostatics_flag + error("sorry, electrostatics not supported") + end + + # initiate system energy to which we increment when MC moves are accepted + system_energy = SystemPotentialEnergy() + # if we don't start with an emtpy xtal, compute energy of starting configuration + # (n=0 corresponds to zero energy) + if any(molecules_species -> length(molecules_species) != 0, molecules) + # some checks + for (s, molecules_species) in enumerate(molecules) + # ensure molecule template matches species of starting molecules. + @assert all( + molecule -> molecule.species == molecule_templates[s].species, + molecules_species + ) "initializing with wrong molecule species" + # assert that the molecules are inside the simulation box + @assert all(molecule -> inside(molecule), molecules_species) "initializing with molecules outside simulation box!" + # ensure pair-wise bond distance match template + @assert all( + molecule -> + !distortion(molecule, Frac(molecule_templates[s], xtal.box), xtal.box), + molecules_species + ) "initializing with distorted molecules" + end + + system_energy.gh.vdw = total_vdw_energy(xtal, molecules, ljff) + system_energy.gg.vdw = total_vdw_energy(molecules, ljff, xtal.box) + system_energy.gh.es = 0.0 #total(total_electrostatic_potential_energy(xtal, molecules, eparams, eikr_gh)) + system_energy.gg.es = 0.0 #total(electrostatic_potential_energy(molecules, eparams, xtal.box, eikr_gg)) + end + + if show_progress_bar + progress_bar = Progress(n_burn_cycles + n_sample_cycles, 1) + end + + #### + # MC proposal probabilities + #### + mc_proposal_probabilities = [0.0 for _ in 1:N_PROPOSAL_TYPES] + # set defaults + mc_proposal_probabilities[INSERTION] = 0.35 + mc_proposal_probabilities[DELETION] = mc_proposal_probabilities[INSERTION] # must be equal + mc_proposal_probabilities[REINSERTION] = 0.05 + mc_proposal_probabilities[TRANSLATION] = 0.25 + if nb_species > 1 # multi-species + mc_proposal_probabilities[IDENTITY_CHANGE] = 0.1 + end + if any(needs_rotations.(molecule_templates)) # rotatable molecules + mc_proposal_probabilities[ROTATION] = 0.25 + end + # normalize + mc_proposal_probabilities /= sum(mc_proposal_probabilities) + # StatsBase.jl functionality for sampling + mc_proposal_probabilities = ProbabilityWeights(mc_proposal_probabilities) + if verbose + println("\tMarkov chain proposals:") + for p in 1:N_PROPOSAL_TYPES + @printf( + "\t\tprobability of %s: %f\n", + PROPOSAL_ENCODINGS[p], + mc_proposal_probabilities[p] + ) + end + end + + # adaptive translation step + adaptive_δ = AdaptiveTranslationStepSize(2.0) # default is 2 Å + println("\t\ttranslation step size: ", adaptive_δ.δ, " Å") + + # initiate GCMC statistics for each block # break simulation into `N_BLOCKS` blocks to gauge convergence + gcmc_stats = [GCMCstats(nb_species) for block_no in 1:N_BLOCKS] + current_block = 1 + # make sure the number of sample cycles is at least equal to N_BLOCKS + if n_sample_cycles < N_BLOCKS + n_sample_cycles = N_BLOCKS + @warn @sprintf( + "# sample cycles set to minimum %d, which is number of blocks.", + N_BLOCKS + ) + end + N_CYCLES_PER_BLOCK = floor(Int, n_sample_cycles / N_BLOCKS) + + markov_counts = MarkovCounts( + zeros(Int, length(PROPOSAL_ENCODINGS)), + zeros(Int, length(PROPOSAL_ENCODINGS)) + ) + + # (n_burn_cycles + n_sample_cycles) is number of outer cycles (MC cycles). + # for each outer cycle, peform max(20, # molecules in the system) MC proposals. + markov_chain_time = 0 + outer_cycle_start = 1 + for outer_cycle in outer_cycle_start:(n_burn_cycles + n_sample_cycles) + if show_progress_bar + next!( + progress_bar; + showvalues=[ + (:cycle, outer_cycle), + (:number_of_molecules, sum(length.(molecules))) + ] + ) + end + for inner_cycle in 1:(sum(length.(molecules)) + 1) + markov_chain_time += 1 + + # choose a species, find current # molecules of that species, n_i + which_species = rand(1:nb_species) + n_i = length(molecules[which_species]) + + # choose proposed move randomly; keep track of proposals + which_move = sample(1:N_PROPOSAL_TYPES, mc_proposal_probabilities) # StatsBase.jl + markov_counts.n_proposed[which_move] += 1 + + if which_move == INSERTION + random_insertion!( + molecules[which_species], + xtal.box, + molecule_templates[which_species] + ) + + # inserted molecule pushed to end of molecules[which_species] array + # note: length(molecules[which_species]) != n_i here (rather, it is n_i + 1) + ΔE = potential_energy(which_species, n_i + 1, molecules, xtal, ljff) + + # Metropolis Hastings Acceptance for Insertion + if rand() < + fugacities[which_species] * xtal.box.Ω / + ((n_i + 1) * BOLTZMANN * temperature) * exp(-sum(ΔE) / temperature) + # accept the move, adjust current_energy + markov_counts.n_accepted[which_move] += 1 + + system_energy += ΔE + else + # reject the move, remove the inserted molecule + pop!(molecules[which_species]) + end + elseif (which_move == DELETION) && (n_i != 0) + # propose which molecule to delete + molecule_id = rand(eachindex(molecules[which_species])) + + # compute the potential energy of the molecule we propose to delete + ΔE = potential_energy(which_species, molecule_id, molecules, xtal, ljff) + + # Metropolis Hastings Acceptance for Deletion + if rand() < + n_i * BOLTZMANN * temperature / + (fugacities[which_species] * xtal.box.Ω) * exp(sum(ΔE) / temperature) + # accept the deletion, delete molecule, adjust current_energy + markov_counts.n_accepted[which_move] += 1 + + remove_molecule!(molecule_id, molecules[which_species]) + + system_energy -= ΔE + end + elseif (which_move == TRANSLATION) && (n_i != 0) + adaptive_δ.nb_translations_proposed += 1 + # propose which molecule whose coordinates we should perturb + molecule_id = rand(eachindex(molecules[which_species])) + + # energy of the molecule before it was translated + energy_old = + potential_energy(which_species, molecule_id, molecules, xtal, ljff) + + old_molecule = random_translation!( + molecules[which_species][molecule_id], + xtal.box, + adaptive_δ + ) + + # energy of the molecule after it is translated + energy_new = + potential_energy(which_species, molecule_id, molecules, xtal, ljff) + + # Metropolis Hastings Acceptance for translation + if rand() < exp(-(sum(energy_new) - sum(energy_old)) / temperature) + # accept the move, adjust current energy + markov_counts.n_accepted[which_move] += 1 + adaptive_δ.nb_translations_accepted += 1 + + system_energy += energy_new - energy_old + else + # reject the move, put back the old molecule + molecules[which_species][molecule_id] = deepcopy(old_molecule) + end + elseif (which_move == ROTATION) && + needs_rotations(molecule_templates[which_species]) && + (n_i != 0) + # propose which molecule to rotate + molecule_id = rand(eachindex(molecules[which_species])) + + # energy of the molecule before we rotate it + energy_old = + potential_energy(which_species, molecule_id, molecules, xtal, ljff) + + # store old molecule to restore old position in case move is rejected + old_molecule = deepcopy(molecules[which_species][molecule_id]) + + # conduct a random rotation + random_rotation!(molecules[which_species][molecule_id], xtal.box) + + # energy of the molecule after it is translated + energy_new = + potential_energy(which_species, molecule_id, molecules, xtal, ljff) + + # Metropolis Hastings Acceptance for rotation + if rand() < exp(-(sum(energy_new) - sum(energy_old)) / temperature) + # accept the move, adjust current energy + markov_counts.n_accepted[which_move] += 1 + + system_energy += energy_new - energy_old + else + # reject the move, put back the old molecule + molecules[which_species][molecule_id] = deepcopy(old_molecule) + end + elseif (which_move == REINSERTION) && (n_i != 0) + # propose which molecule to re-insert + molecule_id = rand(eachindex(molecules[which_species])) + + # compute the potential energy of the molecule we propose to re-insert + energy_old = + potential_energy(which_species, molecule_id, molecules, xtal, ljff) + + # reinsert molecule; store old configuration of the molecule in case proposal is rejected + old_molecule = + random_reinsertion!(molecules[which_species][molecule_id], xtal.box) + + # compute the potential energy of the molecule in its new configuraiton + energy_new = + potential_energy(which_species, molecule_id, molecules, xtal, ljff) + + # Metropolis Hastings Acceptance for reinsertion + if rand() < exp(-(sum(energy_new) - sum(energy_old)) / temperature) + # accept the move, adjust current energy + markov_counts.n_accepted[which_move] += 1 + + system_energy += energy_new - energy_old + else + # reject the move, put back old molecule + molecules[which_species][molecule_id] = deepcopy(old_molecule) + end + elseif (which_move == IDENTITY_CHANGE) && (n_i != 0) + ### + # IDENTITY_CHANGE Procedure + # 1. determine which molecule of a given species to propose identity change + # 2. calculate the energy of that molecule + # 3. remove that molecule from the system, but keep a copy in case of rejection + # 4. select which molecule of a different species is going to replace original molecule + # 5. insert new (trial) molecule at location (with random orientation) of the original molecule + # 6. calculate the energy of the new molecule + # 7. evaluate acceptance rule: accept or reject proposed identity change + # 8. accept: keep new molecule at current location + # reject: remove trial molecule and reinsert the copy of original molecule + # + # NOTE: make sure that the encodings for proposals are updated and that the statistics are updated + ### + # determine which molecule of which_species to propose identity change + molecule_id = rand(1:n_i) + + # calculate the energy of that molecule + energy_old = + potential_energy(which_species, molecule_id, molecules, xtal, ljff) + + # remove that molecule from the system, but keep a copy in case of rejection + old_molecule = deepcopy(molecules[which_species][molecule_id]) + remove_molecule!(molecule_id, molecules[which_species]) + + # select which molecule of a different species is going to replace original molecule + candidate_species = rand([sp for sp in 1:nb_species if sp != which_species]) + + # get the current number of molecules of type candidate_species + n_j = length(molecules[candidate_species]) + + # insert trial molecule, with random orientation, at location of the original molecule + insert_w_random_orientation!( + molecules[candidate_species], + xtal.box, + molecule_templates[candidate_species], + old_molecule.com + ) + + # calculate the energy of the new molecule + energy_new = + potential_energy(candidate_species, n_j + 1, molecules, xtal, ljff) + + # Acceptance rule for identity change + if rand() < + ( + n_i * fugacities[candidate_species] / + ((n_j + 1) * fugacities[which_species]) + ) * exp(-(sum(energy_new) - sum(energy_old)) / temperature) + # accept the move, adjust current energy + markov_counts.n_accepted[which_move] += 1 + + system_energy += energy_new - energy_old + else + # reject the move: remove trial molecule and reinsert original + remove_molecule!(n_j + 1, molecules[candidate_species]) + + push!(molecules[which_species], deepcopy(old_molecule)) + end + end # which move the code executes + + # if we've done all burn cycles, take samples for statistics + if outer_cycle > n_burn_cycles # then we're in "production" cycles. + if markov_chain_time % sample_frequency == 0 + gcmc_stats[current_block].n_samples += 1 + + gcmc_stats[current_block].n += length.(molecules) + gcmc_stats[current_block].n² += length.(molecules) .^ 2 + + gcmc_stats[current_block].U += system_energy + gcmc_stats[current_block].U² += square(system_energy) + + gcmc_stats[current_block].Un += + sum(system_energy) * sum(length.(molecules)) + end + end + end # inner cycles + + # update adaptive step 12 times during burn cycles + if (outer_cycle <= n_burn_cycles) && + (outer_cycle % floor(Int, n_burn_cycles / 12) == 0) + adjust!(adaptive_δ) + end + + # print block statistics / increment block + if (outer_cycle > n_burn_cycles) && + (current_block != N_BLOCKS) && + ((outer_cycle - n_burn_cycles) % N_CYCLES_PER_BLOCK == 0) + # move onto new block unless current_block is N_BLOCKS; + # then just keep adding stats to the last block. + # this only occurs if sample_cycles not divisible by N_BLOCKS + # print GCMC stats later and do not increment block if we are in last block. + # print statistics for this block + if verbose + printstyled( + @sprintf("\tBlock %d/%d statistics:\n", current_block, N_BLOCKS); + color=:yellow + ) + print(gcmc_stats[current_block]) + end + current_block += 1 + end + # print the last cycle in the last block + if outer_cycle == (n_sample_cycles + n_burn_cycles) + if verbose + printstyled( + @sprintf("\tBlock %d/%d statistics:\n", current_block, N_BLOCKS); + color=:yellow + ) + print(gcmc_stats[current_block]) + end + end + + # snapshot cycle + if (outer_cycle > n_burn_cycles) && (outer_cycle % snapshot_frequency == 0) + if write_adsorbate_snapshots + # have a '\n' for every new set of atoms, leaves no '\n' at EOF + if num_snapshots > 0 + @printf(xyz_snapshot_file, "\n") + end + write_xyz(xtal.box, molecules, xyz_snapshot_file) + end + if calculate_density_grid + if density_grid_sim_box + update_density!( + density_grid, + molecules[molecule_species_id_for_density_grid], + density_grid_molecular_species + ) + else + update_density!( + density_grid, + Cart.(molecules[molecule_species_id_for_density_grid], xtal.box), + density_grid_molecular_species + ) + end + end + num_snapshots += 1 + end + end # outer cycles + # finished MC moves at this point. + + # close snapshot xyz file + close(xyz_snapshot_file) + + if calculate_density_grid + # divide number of molecules in a given voxel by total snapshots + density_grid.data ./= num_snapshots + end + + # checks + for (s, molecules_species) in enumerate(molecules) + @assert all( + molecule -> molecule.species == molecule_templates[s].species, + molecules_species + ) "species got mixed up" + @assert all(molecule -> inside(molecule), molecules_species) "molecule outside box!" + @assert all([ + !distortion( + molecule, + Frac(molecule_templates[s], xtal.box), + xtal.box; + atol=1e-10 + ) for molecule in molecules_species + ]) "molecule distorted" + end + + # compute total energy, compare to `current_energy*` variables where were incremented + system_energy_end = SystemPotentialEnergy() + system_energy_end.gh.vdw = total_vdw_energy(xtal, molecules, ljff) + system_energy_end.gg.vdw = total_vdw_energy(molecules, ljff, xtal.box) + system_energy_end.gh.es = 0.0 # total(total_electrostatic_potential_energy(xtal, molecules, eparams, eikr_gh)) + system_energy_end.gg.es = 0.0 # total(total_electrostatic_potential_energy(molecules, eparams, xtal.box, eikr_gg)) + + # see Energetics_Util.jl for this function, overloaded isapprox to print mismatch + if !isapprox(system_energy, system_energy_end; verbose=true, atol=0.01) + error("energy incremented improperly during simulation...") + end + + @assert (markov_chain_time == sum(markov_counts.n_proposed)) + elapsed_time = time() - start_time + if verbose + @printf("\tEstimated elapsed time: %d seconds\n", elapsed_time) + println("\tTotal # MC steps: ", markov_chain_time) + end + + # build dictionary containing summary of simulation results for easy querying + results = Dict{String, Any}() + results["xtal"] = xtal.name + results["adsorbate"] = molecular_species + results["forcefield"] = ljff.name + results["pressure (bar)"] = pressures + results["fugacity (bar)"] = fugacities / 100000.0 + results["temperature (K)"] = temperature + results["repfactors"] = repfactors + + results["# sample cycles"] = n_sample_cycles + results["# burn cycles"] = n_burn_cycles + results["# samples"] = sum(gcmc_stats).n_samples + results["elapsed time (min)"] = elapsed_time / 60 + + # statistics from samples during simulation + # see here: https://cs.nyu.edu/courses/fall06/G22.2112-001/MonteCarlo.pdf for how + # error bars are computed; simulation broken into N_BLOCKS and each average from the + # block is treated as an independent sample. + avg_n, err_n, avg_U, err_U = mean_stderr_n_U(gcmc_stats) + + # averages + results["⟨N⟩ (molecules)"] = avg_n + results["⟨U_gh, vdw⟩ (K)"] = avg_U.gh.vdw + results["⟨U_gh, electro⟩ (K)"] = avg_U.gh.es + results["⟨U_gg, vdw⟩ (K)"] = avg_U.gg.vdw + results["⟨U_gg, electro⟩ (K)"] = avg_U.gg.es + results["⟨U⟩ (K)"] = sum(avg_U) + + # variances + results["var(N)"] = + (sum(gcmc_stats).n² / sum(gcmc_stats).n_samples) - (results["⟨N⟩ (molecules)"] .^ 2) + + # isosteric heat of adsorption TODO stdev of this too. + results["Q_st (K)"] = + temperature .- + ( + sum(gcmc_stats).Un / sum(gcmc_stats).n_samples .- + results["⟨U⟩ (K)"] * results["⟨N⟩ (molecules)"] + ) ./ results["var(N)"] + + # error bars (confidence intervals) + results["err ⟨N⟩ (molecules)"] = err_n + results["err ⟨U_gh, vdw⟩ (K)"] = err_U.gh.vdw + results["err ⟨U_gh, electro⟩ (K)"] = err_U.gh.es + results["err ⟨U_gg, vdw⟩ (K)"] = err_U.gg.vdw + results["err ⟨U_gg, electro⟩ (K)"] = err_U.gg.es + results["err ⟨U⟩ (K)"] = sum(err_U) + + # average N in more common units + results["⟨N⟩ (molecules/unit cell)"] = + avg_n / (repfactors[1] * repfactors[2] * repfactors[3]) + results["err ⟨N⟩ (molecules/unit cell)"] = + err_n / (repfactors[1] * repfactors[2] * repfactors[3]) + # (molecules/unit cell) * (mol/6.02 * 10^23 molecules) * (1000 mmol/mol) * + # (unit cell/xtal amu) * (amu/ 1.66054 * 10^-24) + results["⟨N⟩ (mmol/g)"] = + results["⟨N⟩ (molecules/unit cell)"] * 1000 / + (6.022140857e23 * molecular_weight(xtal) * 1.66054e-24) * + (repfactors[1] * repfactors[2] * repfactors[3]) + results["err ⟨N⟩ (mmol/g)"] = + results["err ⟨N⟩ (molecules/unit cell)"] * 1000 / + (6.022140857e23 * molecular_weight(xtal) * 1.66054e-24) * + (repfactors[1] * repfactors[2] * repfactors[3]) + + # Markov stats + for (proposal_id, proposal_description) in PROPOSAL_ENCODINGS + results[@sprintf("Total # %s proposals", proposal_description)] = + markov_counts.n_proposed[proposal_id] + results[@sprintf("Fraction of %s proposals accepted", proposal_description)] = + markov_counts.n_accepted[proposal_id] / markov_counts.n_proposed[proposal_id] + end + + # Snapshot information + results["density grid"] = deepcopy(density_grid) + results["num snapshots"] = num_snapshots + + if verbose + print_results(results; print_title=false) + end + + # return molecules in Cartesian format + molecules = [Cart.(mols, xtal.box) for mols in molecules] + + if autosave + if !isdir(rc[:paths][:simulations]) + mkdir(rc[:paths][:simulations]) + end + + save_results_filename = joinpath( + rc[:paths][:simulations], + μVT_output_filename( + xtal, + molecule_templates, + temperature, + pressures, + ljff, + n_burn_cycles, + n_sample_cycles; + comment=results_filename_comment + ) + ) + + @save save_results_filename results + if verbose + println("\tresults dictionary saved in ", save_results_filename) + end + end + + return results, molecules # summary of statistics and ending configuration of molecules +end # μVT_sim + +# overload function for backward compatibility +function μVT_sim( + xtal::Crystal, + molecule_template::Molecule{Cart}, + temperature::Float64, + pressure::Float64, + ljff::LJForceField; + kwargs... +) + return μVT_sim(xtal, [molecule_template], temperature, [pressure], ljff; kwargs...) +end + +""" + filename = μVT_output_filename(xtal, molecule_templates, temperature, + pressures, ljff, n_burn_cycles, + n_sample_cycles; comment="", extension=".jld2") + +This is the function that establishes the file naming convention used by [μVT_sim](@ref). + +# Arguments + + - `xtal::Crystal`: porous xtal used in adsorption simulation + - `molecule_templates::Array{Molecule, 1}`: template of the adsorbate molecules used in adsorption simulation + - `temperature::Float64`:temperature of bulk gas phase in equilibrium with adsorbed phase in + the porous material. units: Kelvin (K) + - `pressures::Array{Float64, 1}`: partial pressures of bulk gas phase in equilibrium with adsorbed phase in the + porous material. units: bar + - `ljff::LJForceField`: the molecular model used in adsorption simulation + - `n_burn_cycles::Int`: number of cycles to allow the system to reach equilibrium before sampling. + - `n_sample_cycles::Int`: number of cycles used for sampling + - `comment::String=""`: remarks to be included in the filename + - `extension::String=".jld2"`: the file extension + +# Returns + + - `filename::String`: the name of the specific `.jld2` simulation file +""" +function μVT_output_filename( + xtal::Crystal, + molecule_templates::Array{Molecule{Cart}, 1}, + temperature::Float64, + pressures::Array{Float64, 1}, + ljff::LJForceField, + n_burn_cycles::Int, + n_sample_cycles::Int; + comment::String="", + extension::String=".jld2" +) + filename = @sprintf("muVT_xtal_%s_T_%.3fK", xtal.name, temperature) + for m in 1:length(molecule_templates) + filename *= @sprintf("_%s_P_%.6fbar", molecule_templates[m].species, pressures[m]) + end + filename *= @sprintf("_%s_%dburn_%dsample", ljff.name, n_burn_cycles, n_sample_cycles) + return filename * comment * extension +end + +export μVT_output_filename, μVT_sim + +end diff --git a/src/nvt.jl b/src/nvt.jl new file mode 100644 index 000000000..d76a95933 --- /dev/null +++ b/src/nvt.jl @@ -0,0 +1,341 @@ +module NVT + +import ..Molecule, + ..Frac, + ..Crystal, + ..LJForceField, + ..@printf, + ..@sprintf, + ..@save, + ..Cart, + ..EwaldParams, + ..Eikr + +const KB = 1.38064852e7 # Boltmann constant (Pa-m3/K --> Pa-A3/K) + +### +# Markov chain proposals +### +const PROPOSAL_ENCODINGS = Dict(1 => "translation", 2 => "rotation", 3 => "reinsertion") # helps with printing later +const N_PROPOSAL_TYPES = length(keys(PROPOSAL_ENCODINGS)) +# each proposal type gets an Int for clearer code +const TRANSLATION = Dict([v => k for (k, v) in PROPOSAL_ENCODINGS])["translation"] +const ROTATION = Dict([v => k for (k, v) in PROPOSAL_ENCODINGS])["rotation"] +const REINSERTION = Dict([v => k for (k, v) in PROPOSAL_ENCODINGS])["reinsertion"] + +# TODO move this to MC helpers? but not sure if it will inline. so wait after test with @time +# potential energy change after inserting/deleting/perturbing coordinates of molecules[molecule_id] +@inline function potential_energy( + molecule_id::Int, + molecules::Array{Molecule{Frac}, 1}, + xtal::Crystal, + ljff::LJForceField, + eparams::EwaldParams, + eikr_gh::Eikr, + eikr_gg::Eikr +) + energy = SystemPotentialEnergy() + # van der Waals interactions + energy.gg.vdw = vdw_energy(molecule_id, molecules, ljff, xtal.box) # guest-guest + energy.gh.vdw = vdw_energy(xtal, molecules[molecule_id], ljff) # guest-host + # electrostatic interactions + if has_charges(molecules[molecule_id]) + # guest-guest + energy.gg.es = total( + electrostatic_potential_energy( + molecules, + molecule_id, + eparams, + xtal.box, + eikr_gg + ) + ) + # guest-host + if has_charges(xtal) + energy.gh.es = total( + electrostatic_potential_energy( + xtal, + molecules[molecule_id], + eparams, + eikr_gh + ) + ) + end + end + return energy +end + +function NVT_sim( + xtal::Crystal, + molecule_template::Molecule{Cart}, + molecules::Array{Molecule{Cart}, 1}, + temperature::Float64, + ljff::LJForceField; + n_burn_cycles::Int=5000, + n_sample_cycles::Int=5000, + verbose=true, + ewald_precision::Float64=1e-6 +) + assert_P1_symmetry(xtal) + + start_time = time() + # # to avoid changing the outside object `molecule_` inside this function, we make + # # a deep copy of it here. this serves as a template to copy when we insert a new molecule. + # molecule = deepcopy(molecule_) + + ### + # NON-RIGOROUS PLACEHOLDER -- DELETE + ### + molecule = deepcopy(molecule_template) + + ### + # replicate xtal so that nearest image convention can be applied for short-range interactions + ### + repfactors = replication_factors(xtal.box, ljff) + xtal = replicate(xtal, repfactors) # frac coords still in [0, 1] + + # convert molecules array to fractional using this box. + molecules = Frac.(molecules, xtal.box) + + if !neutral(molecule.charges) + error(@sprintf("Molecule %s is not charge neutral!\n", molecule.species)) + end + + if !(forcefield_coverage(xtal.atoms, ljff) & forcefield_coverage(molecule.atoms, ljff)) + error("Missing atoms from forcefield.") + end + + # define Ewald summation params + # pre-compute weights on k-vector contributions to long-rage interactions in + # Ewald summation for electrostatics + # allocate memory for exp^{i * n * k ⋅ r} + eparams = setup_Ewald_sum( + xtal.box, + sqrt(ljff.r²_cutoff); + verbose=verbose & (has_charges(molecule) || has_charges(xtal)), + ϵ=ewald_precision + ) + eikr_gh = Eikr(xtal, eparams) + eikr_gg = Eikr(molecule, eparams) + + # initiate system energy to which we increment when MC moves are accepted + system_energy = SystemPotentialEnergy() + # if we don't start with an emtpy xtal, compute energy of starting configuration + # (n=0 corresponds to zero energy) + if length(molecules) != 0 + # some checks + for m in molecules + # ensure molecule template matches species of starting molecules. + @assert m.species == molecule.species "initializing with wrong molecule species" + # assert that the molecules are inside the simulation box + @assert inside(m) "initializing with molecules outside simulation box!" + # ensure pair-wise bond distances match template + @assert !distortion(m, Frac(molecule, xtal.box), xtal.box) + end + + system_energy.gh.vdw = total_vdw_energy(xtal, molecules, ljff) + system_energy.gg.vdw = total_vdw_energy(molecules, ljff, xtal.box) + system_energy.gh.es = + total(total_electrostatic_potential_energy(xtal, molecules, eparams, eikr_gh)) + system_energy.gg.es = + total(electrostatic_potential_energy(molecules, eparams, xtal.box, eikr_gg)) + end + + #### + # proposal probabilities + ### + mc_proposal_probabilities = [0.0 for p in 1:N_PROPOSAL_TYPES] + # set defaults + mc_proposal_probabilities[REINSERTION] = 0.05 + if needs_rotations(molecule) + mc_proposal_probabilities[TRANSLATION] = 0.125 + mc_proposal_probabilities[ROTATION] = 0.125 + else + mc_proposal_probabilities[TRANSLATION] = 0.25 + mc_proposal_probabilities[ROTATION] = 0.0 + end + mc_proposal_probabilities /= sum(mc_proposal_probabilities) # normalize + # StatsBase.jl functionality for sampling + mc_proposal_probabilities = ProbabilityWeights(mc_proposal_probabilities) + if verbose + println("\tMarkov chain proposals:") + for p in 1:N_PROPOSAL_TYPES + @printf( + "\t\tprobability of %s: %f\n", + PROPOSAL_ENCODINGS[p], + mc_proposal_probabilities[p] + ) + end + end + + markov_counts = MarkovCounts( + zeros(Int, length(PROPOSAL_ENCODINGS)), + zeros(Int, length(PROPOSAL_ENCODINGS)) + ) + + # (n_burn_cycles + n_sample_cycles) is number of outer cycles. + # for each outer cycle, peform max(20, # molecules in the system) MC proposals. + markov_chain_time = 0 + outer_cycle_start = 1 + for outer_cycle in outer_cycle_start:(n_burn_cycles + n_sample_cycles) + for inner_cycle in 1:max(20, length(molecules)) + markov_chain_time += 1 + + # choose proposed move randomly; keep track of proposals + which_move = sample(1:N_PROPOSAL_TYPES, mc_proposal_probabilities) # StatsBase.jl + markov_counts.n_proposed[which_move] += 1 + + if which_move == TRANSLATION + # propose which molecule whose coordinates we should perturb + molecule_id = rand(1:length(molecules)) + + # energy of the molecule before it was translated + energy_old = potential_energy( + molecule_id, + molecules, + xtal, + ljff, + eparams, + eikr_gh, + eikr_gg + ) + + old_molecule = random_translation!(molecules[molecule_id], xtal.box) + + # energy of the molecule after it is translated + energy_new = potential_energy( + molecule_id, + molecules, + xtal, + ljff, + eparams, + eikr_gh, + eikr_gg + ) + + # Metropolis Hastings Acceptance for translation + if rand() < exp(-(sum(energy_new) - sum(energy_old)) / temperature) + # accept the move, adjust current energy + markov_counts.n_accepted[which_move] += 1 + + system_energy += energy_new - energy_old + else + # reject the move, put back the old molecule + molecules[molecule_id] = deepcopy(old_molecule) + end + elseif which_move == ROTATION + # propose which molecule to rotate + molecule_id = rand(1:length(molecules)) + + # energy of the molecule before we rotate it + energy_old = potential_energy( + molecule_id, + molecules, + xtal, + ljff, + eparams, + eikr_gh, + eikr_gg + ) + + # store old molecule to restore old position in case move is rejected + old_molecule = deepcopy(molecules[molecule_id]) + + # conduct a random rotation + random_rotation!(molecules[molecule_id], xtal.box) + + # energy of the molecule after it is translated + energy_new = potential_energy( + molecule_id, + molecules, + xtal, + ljff, + eparams, + eikr_gh, + eikr_gg + ) + + # Metropolis Hastings Acceptance for rotation + if rand() < exp(-(sum(energy_new) - sum(energy_old)) / temperature) + # accept the move, adjust current energy + markov_counts.n_accepted[which_move] += 1 + + system_energy += energy_new - energy_old + else + # reject the move, put back the old molecule + molecules[molecule_id] = deepcopy(old_molecule) + end + elseif which_move == REINSERTION + # propose which molecule to re-insert + molecule_id = rand(1:length(molecules)) + + # compute the potential energy of the molecule we propose to re-insert + energy_old = potential_energy( + molecule_id, + molecules, + xtal, + ljff, + eparams, + eikr_gh, + eikr_gg + ) + + # reinsert molecule; store old configuration of the molecule in case proposal is rejected + old_molecule = random_reinsertion!(molecules[molecule_id], xtal.box) + + # compute the potential energy of the molecule in its new configuraiton + energy_new = potential_energy( + molecule_id, + molecules, + xtal, + ljff, + eparams, + eikr_gh, + eikr_gg + ) + + # Metropolis Hastings Acceptance for reinsertion + if rand() < exp(-(sum(energy_new) - sum(energy_old)) / temperature) + # accept the move, adjust current energy + markov_counts.n_accepted[which_move] += 1 + + system_energy += energy_new - energy_old + else + # reject the move, put back old molecule + molecules[molecule_id] = deepcopy(old_molecule) + end + end # which move the code executes + end # inner cycles + end # outer cycles + # finished MC moves at this point. + + for m in molecules + @assert inside(m) "molecule outside box!" + @assert !distortion(m, Frac(molecule, xtal.box), xtal.box; atol=1e-10) "molecules distorted" + end + + # compute total energy, compare to `current_energy*` variables where were incremented + system_energy_end = SystemPotentialEnergy() + system_energy_end.gh.vdw = total_vdw_energy(xtal, molecules, ljff) + system_energy_end.gg.vdw = total_vdw_energy(molecules, ljff, xtal.box) + system_energy_end.gh.es = + total(total_electrostatic_potential_energy(xtal, molecules, eparams, eikr_gh)) + system_energy_end.gg.es = + total(total_electrostatic_potential_energy(molecules, eparams, xtal.box, eikr_gg)) + + # see Energetics_Util.jl for this function, overloaded isapprox to print mismatch + if !isapprox(system_energy, system_energy_end; verbose=true, atol=0.01) + error("energy incremented improperly during simulation...") + end + + @assert (markov_chain_time == sum(markov_counts.n_proposed)) + + # return molecules in Cartesian format + molecules = Cart.(molecules, xtal.box) + + return molecules # summary of statistics and ending configuration of molecules +end # NVT_sim + +export NVT_sim + +end # module NVT diff --git a/src/vdw_energetics.jl b/src/vdw_energetics.jl index 1c5134a72..2b4961c64 100644 --- a/src/vdw_energetics.jl +++ b/src/vdw_energetics.jl @@ -9,36 +9,45 @@ lennard-jones spheres. σ and ϵ are specific to interaction between two element the potential energy in units Kelvin (well, whatever the units of ϵ are). # Arguments -- `r²::Float64`: distance between two (pseudo)atoms in question squared (Angstrom²) -- `σ²::Float64`: sigma parameter in Lennard Jones potential squared (units: Angstrom²) -- `ϵ::Float64`: epsilon parameter in Lennard Jones potential (units: Kelvin) + + - `r²::Float64`: distance between two (pseudo)atoms in question squared (Angstrom²) + - `σ²::Float64`: sigma parameter in Lennard Jones potential squared (units: Angstrom²) + - `ϵ::Float64`: epsilon parameter in Lennard Jones potential (units: Kelvin) # Returns -- `energy::Float64`: Lennard Jones potential energy + + - `energy::Float64`: Lennard Jones potential energy """ function lennard_jones(r²::Float64, σ²::Float64, ϵ::Float64) - ratio = (σ² / r²) ^ 3 + ratio = (σ² / r²)^3 return 4.0 * ϵ * ratio * (ratio - 1.0) end # generic atoms-atoms van der waals energy function; pass bigger list of atoms second for speed -function vdw_energy(atoms_i::Atoms{Frac}, atoms_j::Atoms{Frac}, box::Box, ljff::LJForceField) +function vdw_energy( + atoms_i::Atoms{Frac}, + atoms_j::Atoms{Frac}, + box::Box, + ljff::LJForceField +) energy = 0.0 - for i = 1:atoms_i.n + for i in 1:(atoms_i.n) # vectors from atom i to atoms_j in fractional space @inbounds dx = broadcast(-, atoms_j.coords.xf, atoms_i.coords.xf[:, i]) nearest_image!(dx) # convert to Cartesian coords @inbounds dx .= box.f_to_c * dx - for j = 1:atoms_j.n - @inbounds r² = dx[1, j] ^ 2 + dx[2, j] ^ 2 + dx[3, j] ^ 2 + for j in 1:(atoms_j.n) + @inbounds r² = dx[1, j]^2 + dx[2, j]^2 + dx[3, j]^2 if r² < ljff.r²_cutoff # within cutoff radius if r² < R²_OVERLAP # overlapping atoms return Inf # no sense in continuing to next atom and adding Inf else # not overlapping - @inbounds energy += lennard_jones(r², - ljff.σ²[atoms_i.species[i]][atoms_j.species[j]], - ljff.ϵ[ atoms_i.species[i]][atoms_j.species[j]]) + @inbounds energy += lennard_jones( + r², + ljff.σ²[atoms_i.species[i]][atoms_j.species[j]], + ljff.ϵ[atoms_i.species[i]][atoms_j.species[j]] + ) end end end @@ -46,12 +55,33 @@ function vdw_energy(atoms_i::Atoms{Frac}, atoms_j::Atoms{Frac}, box::Box, ljff:: return energy end -vdw_energy(atoms_i::Atoms{Cart}, atoms_j::Atoms{Cart}, box::Box, ljff::LJForceField) = vdw_energy(Frac(atoms_i, box), Frac(atoms_j, box), box, ljff) -vdw_energy(atoms_i::Atoms{Frac}, atoms_j::Atoms{Cart}, box::Box, ljff::LJForceField) = vdw_energy(atoms_i, Frac(atoms_j, box), box, ljff) -vdw_energy(atoms_i::Atoms{Cart}, atoms_j::Atoms{Frac}, box::Box, ljff::LJForceField) = vdw_energy(Frac(atoms_i, box), atoms_j, box, ljff) +function vdw_energy( + atoms_i::Atoms{Cart}, + atoms_j::Atoms{Cart}, + box::Box, + ljff::LJForceField +) + return vdw_energy(Frac(atoms_i, box), Frac(atoms_j, box), box, ljff) +end +function vdw_energy( + atoms_i::Atoms{Frac}, + atoms_j::Atoms{Cart}, + box::Box, + ljff::LJForceField +) + return vdw_energy(atoms_i, Frac(atoms_j, box), box, ljff) +end +function vdw_energy( + atoms_i::Atoms{Cart}, + atoms_j::Atoms{Frac}, + box::Box, + ljff::LJForceField +) + return vdw_energy(Frac(atoms_i, box), atoms_j, box, ljff) +end """ - energy = vdw_energy(crystal, molecule, ljforcefield) +energy = vdw_energy(crystal, molecule, ljforcefield) Calculates the van der Waals interaction energy between a molecule and a crystal. Applies the nearest image convention to find the closest replicate of a specific atom. @@ -60,43 +90,61 @@ WARNING: it is assumed that the framework is replicated sufficiently such that t image convention can be applied. See [`replicate`](@ref) and [`replication_factors`](@ref). """ function vdw_energy(crystal::Crystal, molecule::Molecule, ljff::LJForceField) - return vdw_energy(molecule.atoms, crystal.atoms, crystal.box, ljff) + return vdw_energy(molecule.atoms, crystal.atoms, crystal.box, ljff) end """ - gg_energy = vdw_energy(molecule_id, molecules, ljforcefield, simulation_box) +gg_energy = vdw_energy(molecule_id, molecules, ljforcefield, simulation_box) Calculates van der Waals interaction energy of a single adsorbate `molecules[molecule_id]` with all of the other molecules in the system. Periodic boundary conditions are applied, using the nearest image convention. # Arguments -- `molecule_id::Int`: Molecule ID used to determine which molecule in `molecules` we wish to calculate the guest-guest interactions -- `molecules::Array{Molecule, 1}`: An array of Molecule data structures -- `ljforcefield::LJForceField`: A Lennard Jones forcefield data structure describing the interactions between different atoms -- `simulation_box::Box`: The simulation box for the computation. + + - `molecule_id::Int`: Molecule ID used to determine which molecule in `molecules` we wish to calculate the guest-guest interactions + - `molecules::Array{Molecule, 1}`: An array of Molecule data structures + - `ljforcefield::LJForceField`: A Lennard Jones forcefield data structure describing the interactions between different atoms + - `simulation_box::Box`: The simulation box for the computation. # Returns -- `gg_energy::Float64`: The guest-guest interaction energy of `molecules[molecule_id]` with the other molecules in `molecules` + + - `gg_energy::Float64`: The guest-guest interaction energy of `molecules[molecule_id]` with the other molecules in `molecules` """ -function vdw_energy(molecule_id::Int, molecules::Array{<:Molecule, 1}, ljff::LJForceField, box::Box) - energy = 0.0 - # loop over all other molecule - for other_molecule_id = eachindex(molecules) - # molecule cannot interact with itself - if other_molecule_id == molecule_id - continue - end - energy += vdw_energy(molecules[molecule_id].atoms, molecules[other_molecule_id].atoms, box, ljff) - end - return energy # units are the same as in ϵ for forcefield (Kelvin) +function vdw_energy( + molecule_id::Int, + molecules::Array{<:Molecule, 1}, + ljff::LJForceField, + box::Box +) + energy = 0.0 + # loop over all other molecule + for other_molecule_id in eachindex(molecules) + # molecule cannot interact with itself + if other_molecule_id == molecule_id + continue + end + energy += vdw_energy( + molecules[molecule_id].atoms, + molecules[other_molecule_id].atoms, + box, + ljff + ) + end + return energy # units are the same as in ϵ for forcefield (Kelvin) end ### # for mixture simulations. # compute potential energy of molecules[which_spcies][molecule_id] with all other molecules. ### -function vdw_energy(species_id::Int, molecule_id::Int, molecules::Array{Array{Molecule{Frac}, 1}, 1}, ljff::LJForceField, box::Box) +function vdw_energy( + species_id::Int, + molecule_id::Int, + molecules::Array{Array{Molecule{Frac}, 1}, 1}, + ljff::LJForceField, + box::Box +) energy = 0.0 # loop over species for s in 1:length(molecules) @@ -107,15 +155,20 @@ function vdw_energy(species_id::Int, molecule_id::Int, molecules::Array{Array{Mo continue end - energy += vdw_energy(molecules[species_id][molecule_id].atoms, molecules[s][m].atoms, box, ljff) + energy += vdw_energy( + molecules[species_id][molecule_id].atoms, + molecules[s][m].atoms, + box, + ljff + ) end end return energy # units are the same as in ϵ for forcefield (Kelvin) end """ - total_gh_energy = total_vdw_energy(framework, molecules, ljforcefield) # guest-host - total_gg_energy = total_vdw_energy(molecules, ljforcefield, simulation_box) # guest-guest +total_gh_energy = total_vdw_energy(framework, molecules, ljforcefield) # guest-host +total_gg_energy = total_vdw_energy(molecules, ljforcefield, simulation_box) # guest-guest Compute total guest-host (gh) or guest-guest (gg) interaction energy, i.e. the contribution from all adsorbates in `molecules`. @@ -124,49 +177,57 @@ WARNING: it is assumed that the framework is replicated sufficiently such that t image convention can be applied. See [`replicate`](@ref). # Arguments -- `framework::Framework`: The framework containing the crystal structure information -- `molecules::Array{Molecule, 1}`: An array of Molecule data structures -- `ljforcefield::LJForceField`: A Lennard Jones forcefield data structure describing the interactions between different atoms -- `simulation_box::Box`: The simulation box for application of PBCs. + + - `framework::Framework`: The framework containing the crystal structure information + - `molecules::Array{Molecule, 1}`: An array of Molecule data structures + - `ljforcefield::LJForceField`: A Lennard Jones forcefield data structure describing the interactions between different atoms + - `simulation_box::Box`: The simulation box for application of PBCs. # Returns -- `total_energy::Float64`: The total guest-host or guest-guest van der Waals energy + + - `total_energy::Float64`: The total guest-host or guest-guest van der Waals energy """ -function total_vdw_energy(crystal::Crystal, molecules::Array{<:Molecule, 1}, ljff::LJForceField) - total_energy = 0.0 - for molecule in molecules - total_energy += vdw_energy(crystal, molecule, ljff) - end - return total_energy +function total_vdw_energy( + crystal::Crystal, + molecules::Array{<:Molecule, 1}, + ljff::LJForceField +) + total_energy = 0.0 + for molecule in molecules + total_energy += vdw_energy(crystal, molecule, ljff) + end + return total_energy end function total_vdw_energy(molecules::Array{<:Molecule, 1}, ljff::LJForceField, box::Box) - total_energy = 0.0 - for i = eachindex(molecules) - total_energy += vdw_energy(i, molecules, ljff, box) - end - return total_energy / 2.0 # avoid double-counting pairs + total_energy = 0.0 + for i in eachindex(molecules) + total_energy += vdw_energy(i, molecules, ljff, box) + end + return total_energy / 2.0 # avoid double-counting pairs end """ - pot_energy = vdw_energy_no_PBC(atoms_i, atoms_j , ljff) +pot_energy = vdw_energy_no_PBC(atoms_i, atoms_j , ljff) compute vdw potential energy without periodic boundary conditions """ function vdw_energy_no_PBC(atoms_i::Atoms{Cart}, atoms_j::Atoms{Cart}, ljff::LJForceField) energy = 0.0 - for i = 1:atoms_i.n + for i in 1:(atoms_i.n) # vectors from atom i to atoms_j in fractional space @inbounds dx = broadcast(-, atoms_j.coords.x, atoms_i.coords.x[:, i]) - for j = 1:atoms_j.n - @inbounds r² = dx[1, j] ^ 2 + dx[2, j] ^ 2 + dx[3, j] ^ 2 + for j in 1:(atoms_j.n) + @inbounds r² = dx[1, j]^2 + dx[2, j]^2 + dx[3, j]^2 if r² < ljff.r²_cutoff # within cutoff radius if r² < R²_OVERLAP # overlapping atoms return Inf # no sense in continuing to next atom and adding Inf else # not overlapping - @inbounds energy += lennard_jones(r², - ljff.σ²[atoms_i.species[i]][atoms_j.species[j]], - ljff.ϵ[ atoms_i.species[i]][atoms_j.species[j]]) + @inbounds energy += lennard_jones( + r², + ljff.σ²[atoms_i.species[i]][atoms_j.species[j]], + ljff.ϵ[atoms_i.species[i]][atoms_j.species[j]] + ) end end end @@ -174,7 +235,11 @@ function vdw_energy_no_PBC(atoms_i::Atoms{Cart}, atoms_j::Atoms{Cart}, ljff::LJF return energy end -function total_vdw_energy(crystal::Crystal, molecules::Array{Array{Molecule{Frac}, 1}, 1}, ljff::LJForceField) +function total_vdw_energy( + crystal::Crystal, + molecules::Array{Array{Molecule{Frac}, 1}, 1}, + ljff::LJForceField +) total_energy = 0.0 for molecules_species in molecules total_energy += total_vdw_energy(crystal, molecules_species, ljff) @@ -182,8 +247,12 @@ function total_vdw_energy(crystal::Crystal, molecules::Array{Array{Molecule{Frac return total_energy end -function total_vdw_energy(molecules::Array{Array{Molecule{Frac}, 1}, 1}, ljff::LJForceField, box::Box) - total_energy = 0.0 +function total_vdw_energy( + molecules::Array{Array{Molecule{Frac}, 1}, 1}, + ljff::LJForceField, + box::Box +) + total_energy = 0.0 for (species_id, molecules_species) in enumerate(molecules) for molecule_id in 1:length(molecules_species) total_energy += vdw_energy(species_id, molecule_id, molecules, ljff, box) diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 000000000..5b9b8f760 --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,15 @@ +[deps] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" +Optim = "429524aa-4258-5aef-a3af-852621145aeb" +Polynomials = "f27b6e38-b328-58d1-80ce-0feddd5e7a45" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/aqua.jl b/test/aqua.jl index 98895ee3b..4194ea014 100644 --- a/test/aqua.jl +++ b/test/aqua.jl @@ -2,14 +2,10 @@ using PorousMaterials import Aqua # ambiguity testing finds many "problems" outside the scope of this package -ambiguities=false +ambiguities = false # to skip when checking for stale dependencies and missing compat entries # Aqua is added in a separate CI job, so (ironically) does not work w/ itself stale_deps = (ignore=[:Aqua],) -Aqua.test_all( - PorousMaterials; - ambiguities=ambiguities, - stale_deps=stale_deps -) \ No newline at end of file +Aqua.test_all(PorousMaterials; ambiguities=ambiguities, stale_deps=stale_deps) diff --git a/test/data/molecules/N2_TraPPE/atoms.csv b/test/data/molecules/N2_TraPPE/atoms.csv new file mode 100644 index 000000000..362f064a4 --- /dev/null +++ b/test/data/molecules/N2_TraPPE/atoms.csv @@ -0,0 +1,4 @@ +atom,x,y,z +N_in_N2,0,0,0.55 +PSEUDOATOM_LABEL,0,0,0 +N_in_N2,0,0,-0.55 \ No newline at end of file diff --git a/test/data/molecules/N2_TraPPE/charges.csv b/test/data/molecules/N2_TraPPE/charges.csv new file mode 100644 index 000000000..096c15f96 --- /dev/null +++ b/test/data/molecules/N2_TraPPE/charges.csv @@ -0,0 +1,4 @@ +q,x,y,z +-0.482,0,0,0.55 +0.964,0,0,0 +-0.482,0,0,-0.55 \ No newline at end of file diff --git a/test/electrostatics.jl b/test/electrostatics.jl index 3a3ad599d..519f1082a 100644 --- a/test/electrostatics.jl +++ b/test/electrostatics.jl @@ -14,7 +14,7 @@ using Random rep_factors = replication_factors(crystal, sr_cutoff_r) sim_box = replicate(crystal.box, rep_factors) crystal = replicate(crystal, rep_factors) - eparams = setup_Ewald_sum(crystal.box, sr_cutoff_r, verbose=false, ϵ=1e-6) + eparams = setup_Ewald_sum(crystal.box, sr_cutoff_r; verbose=false, ϵ=1e-6) eikr = Eikr(crystal, eparams) q_test = 0.8096 # ensure getting right Ewald settings @@ -25,7 +25,7 @@ using Random @test eparams.kreps == (9, 9, 9) @test isapprox(eparams.α, 0.2471, atol=0.05) # construct box so recip. lattice is dimension (2, 10, 5) - box = Box(0.5*2*π, 0.1*2*π, 0.2*2*π, π/2, π/2, π/2) + box = Box(0.5 * 2 * π, 0.1 * 2 * π, 0.2 * 2 * π, π / 2, π / 2, π / 2) @test PorousMaterials.required_kreps(box, 2.1^2) == (1, 0, 0) @test PorousMaterials.required_kreps(box, 5.1^2) == (2, 0, 1) @test PorousMaterials.required_kreps(box, 10.1^2) == (5, 1, 2) @@ -48,18 +48,40 @@ using Random # NIST data to test Ewald sums # data from here: https://www.nist.gov/mml/csd/chemical-informatics-research-group/spce-water-reference-calculations-10%C3%A5-cutoff # what the energies should be for all four configurations provided by NIST - energies_should_be = [Dict(["real"=> -5.58889e05, "fourier"=> 6.27009e03, "self"=> -2.84469e06, "intra" => 2.80999e06]), - Dict(["real"=> -1.19295e06, "fourier"=> 6.03495e03, "self"=> -5.68938e06, "intra" => 5.61998e06]), - Dict(["real"=> -1.96297e06, "fourier"=> 5.24461e03, "self"=> -8.53407e06, "intra" => 8.42998e06]), - Dict(["real"=> -3.57226e06, "fourier"=> 7.58785e03, "self"=> -1.42235e07, "intra" => 1.41483e07])] + energies_should_be = [ + Dict([ + "real" => -5.58889e05, + "fourier" => 6.27009e03, + "self" => -2.84469e06, + "intra" => 2.80999e06 + ]), + Dict([ + "real" => -1.19295e06, + "fourier" => 6.03495e03, + "self" => -5.68938e06, + "intra" => 5.61998e06 + ]), + Dict([ + "real" => -1.96297e06, + "fourier" => 5.24461e03, + "self" => -8.53407e06, + "intra" => 8.42998e06 + ]), + Dict([ + "real" => -3.57226e06, + "fourier" => 7.58785e03, + "self" => -1.42235e07, + "intra" => 1.41483e07 + ]) + ] # loop over all four configurations provided by NIST - for c = eachindex(energies_should_be) + for c in eachindex(energies_should_be) # read in positions of atoms provided by NIST ("X" atoms) posfile = open("nist/electrostatics/spce_sample_config_periodic$c.txt") lines = readlines(posfile) # first line is dims of unit cell box dims = parse.(Float64, split(lines[1])) - box = Box(dims..., π/2, π/2, π/2) + box = Box(dims..., π / 2, π / 2, π / 2) # second line is # of molecules n = parse(Int, lines[2]) * 3 # 2H, 1O per n @@ -68,7 +90,7 @@ using Random q_H = 0.42380 # on H, -2q on O ion_number = -1 qs = Charges(zeros(3), Frac(zeros(3, 3))) - for i = 1:n + for i in 1:n if i % 3 == 1 # new water molecule qs = Charges(3, zeros(3), Frac(zeros(3, 3))) ion_number = 1 @@ -76,10 +98,10 @@ using Random ion_number += 1 end # get x position - xyz = split(lines[2+i])[2:4] + xyz = split(lines[2 + i])[2:4] x = parse.(Float64, xyz) # get species - O_or_H = split(lines[2+i])[end] + O_or_H = split(lines[2 + i])[end] q = O_or_H == "O" ? -2 * q_H : q_H # add to charges qs.q[ion_number] = q @@ -87,17 +109,17 @@ using Random # construct molecule if i % 3 == 0 com = [0.0, 0.0, 0.0] - for blah = 1:3 - com += box.f_to_c * qs.coords.xf[:, blah] + for blah in 1:3 + com += box.f_to_c * qs.coords.xf[:, blah] end com /= 3 m = Molecule(:H2O, Atoms{Frac}(0), qs, Frac(box.c_to_f * com)) @assert (ion_number == 3) push!(ms, m) - @assert (isapprox(sum(m.charges.q), 0.0, rtol=0.001)) + @assert (isapprox(sum(m.charges.q), 0.0; rtol=0.001)) end end - @assert (length(ms) == n/3) + @assert (length(ms) == n / 3) close(posfile) # compute energy of the configuration @@ -107,20 +129,35 @@ using Random α = 5.6 / box.a kvecs = PorousMaterials.precompute_kvec_wts(kreps, box, α) # only include kvecs with <27 acc to NIST website - kvec_keep = [kvec.ka ^ 2 + kvec.kb ^2 + kvec.kc ^2 < 27 for kvec in kvecs] + kvec_keep = [kvec.ka^2 + kvec.kb^2 + kvec.kc^2 < 27 for kvec in kvecs] kvecs = kvecs[kvec_keep] eparams = PorousMaterials.EwaldParams(kreps, α, sr_cutoff_r, kvecs) eikr = Eikr(ms[1], eparams) ϕ = electrostatic_potential_energy(ms, eparams, box, eikr) @test isapprox(ϕ.self, energies_should_be[c]["self"], rtol=0.00001) @test isapprox(ϕ.sr, energies_should_be[c]["real"], rtol=0.00001) - @test isapprox(ϕ.lr_excluding_own_images + ϕ.lr_own_images, energies_should_be[c]["fourier"], rtol=0.00001) - @test isapprox(ϕ.intra, energies_should_be[c]["intra"], rtol=0.0001) + @test isapprox( + ϕ.lr_excluding_own_images + ϕ.lr_own_images, + energies_should_be[c]["fourier"], + rtol=0.00001 + ) + @test isapprox(ϕ.intra, energies_should_be[c]["intra"], rtol=0.0001) # test incremented potential energy - @test isapprox(total(PorousMaterials.total_electrostatic_potential_energy(ms, eparams, box, eikr)), total(ϕ), atol=0.01) + @test isapprox( + total( + PorousMaterials.total_electrostatic_potential_energy( + ms, + eparams, + box, + eikr + ) + ), + total(ϕ), + atol=0.01 + ) # test potential energy function for MC sims ϕ_minus_molecule = electrostatic_potential_energy(ms[2:end], eparams, box, eikr) - ϕ_MC = electrostatic_potential_energy(ms, 1, eparams, box, eikr) + ϕ_MC = electrostatic_potential_energy(ms, 1, eparams, box, eikr) @test(isapprox(total(ϕ) - total(ϕ_minus_molecule), total(ϕ_MC), atol=0.01)) end @@ -129,7 +166,7 @@ using Random ### zif71 = Crystal("zif71_bogus_charges.cif") strip_numbers_from_atom_labels!(zif71) - ff = LJForceField("Greg_bogus_ZIF71", r_cutoff=12.8) + ff = LJForceField("Greg_bogus_ZIF71"; r_cutoff=12.8) co2 = Molecule("CO2EPM2") co2 = Frac(co2, zif71.box) @@ -146,7 +183,7 @@ using Random # test vdW energy @test isapprox(vdw_energy(zif71, co2, ff), -132.56, atol=0.01) # test electrostatics - eparams = setup_Ewald_sum(zif71.box, 12.0, verbose=false, ϵ=1e-6) + eparams = setup_Ewald_sum(zif71.box, 12.0; verbose=false, ϵ=1e-6) eikr_gh = Eikr(zif71, eparams) eikr_gg = Eikr(co2, eparams) ϕ = electrostatic_potential_energy(zif71, co2, eparams, eikr_gh) @@ -168,9 +205,28 @@ using Random co2_2.charges.coords.xf[:, 1] = [0.50680, 0.38496, 0.50788] co2_2.charges.coords.xf[:, 2] = [0.54340, 0.38451, 0.49116] co2_2.charges.coords.xf[:, 3] = [0.47020, 0.38540, 0.52461] - @test isapprox(PorousMaterials.total_vdw_energy(zif71, [co2, co2_2], ff), -311.10392551, atol=0.1) - @test isapprox(PorousMaterials.total_vdw_energy([co2, co2_2], ff, zif71.box), -50.975, atol=0.1) - @test isapprox(total(PorousMaterials.total_electrostatic_potential_energy(zif71, [co2, co2_2], eparams, eikr_gh)), -36.00, atol=0.3) + @test isapprox( + PorousMaterials.total_vdw_energy(zif71, [co2, co2_2], ff), + -311.10392551, + atol=0.1 + ) + @test isapprox( + PorousMaterials.total_vdw_energy([co2, co2_2], ff, zif71.box), + -50.975, + atol=0.1 + ) + @test isapprox( + total( + PorousMaterials.total_electrostatic_potential_energy( + zif71, + [co2, co2_2], + eparams, + eikr_gh + ) + ), + -36.00, + atol=0.3 + ) ϕ = electrostatic_potential_energy([co2, co2_2], eparams, zif71.box, eikr_gg) @test isapprox(total(ϕ), 59.3973, atol=0.05) @@ -185,8 +241,16 @@ using Random @test isapprox(total(ϕ_for_MC), total(ϕ_one)) # assert total_electrostatic_potential function incrementer works - @test isapprox(total(PorousMaterials.total_electrostatic_potential_energy([co2, co2_2], eparams, zif71.box, eikr_gg)), - total(electrostatic_potential_energy([co2, co2_2], eparams, zif71.box, eikr_gg))) - + @test isapprox( + total( + PorousMaterials.total_electrostatic_potential_energy( + [co2, co2_2], + eparams, + zif71.box, + eikr_gg + ) + ), + total(electrostatic_potential_energy([co2, co2_2], eparams, zif71.box, eikr_gg)) + ) end end diff --git a/test/energy_min.jl b/test/energy_min.jl index 32f02fcac..cc8fae3b9 100644 --- a/test/energy_min.jl +++ b/test/energy_min.jl @@ -12,12 +12,12 @@ Xe = Molecule("Xe") @testset "Energy Minimizaiton Tests" begin xtal = deepcopy(FIQCEN_clean) strip_numbers_from_atom_labels!(xtal) - molecule = deepcopy(Xe) + molecule = deepcopy(Xe) ljff = LJForceField("UFF") n_pts = (15, 15, 15) - + # grid search (gs) - min_mol_gs, min_E_gs = find_energy_minimum_gridsearch(xtal, molecule, ljff, n_pts=n_pts) + min_mol_gs, min_E_gs = find_energy_minimum_gridsearch(xtal, molecule, ljff; n_pts=n_pts) @test isapprox(min_E_gs, -25.1, atol=1.0) @test isapprox(min_mol_gs.com.xf, [0.785, 0.785, 0.785], atol=0.05) @@ -31,7 +31,7 @@ Xe = Molecule("Xe") rep_factors = replication_factors(xtal, sqrt(ljff.r²_cutoff)) xtal = replicate(xtal, rep_factors) @test isapprox(vdw_energy(xtal, min_mol, ljff) * 8.314 / 1000, min_E, atol=0.1) - + # make sure it works with cart coords too. xtal = deepcopy(FIQCEN_clean) strip_numbers_from_atom_labels!(xtal) @@ -40,14 +40,13 @@ Xe = Molecule("Xe") min_mol, min_E = find_energy_minimum(xtal, molecule, ljff) @test isapprox(min_E, -27.0269, atol=0.01) @test isapprox(min_mol.com.xf, [0.75, 0.75, 0.75], atol=0.001) - + # one more, know the min is in the mid b/c this is a centered cage. xtal = Crystal("cage_in_space.cif") - min_mol, min_E = find_energy_minimum_gridsearch(xtal, molecule, ljff, n_pts=n_pts) + min_mol, min_E = find_energy_minimum_gridsearch(xtal, molecule, ljff; n_pts=n_pts) @test isapprox(min_mol.com.xf, [0.5, 0.5, 0.5], atol=0.1) min_mol, min_E = find_energy_minimum(xtal, min_mol, ljff) @test isapprox(min_E, -38.55, atol=0.1) @test isapprox(min_mol.com.xf, [0.5, 0.5, 0.5], atol=0.1) @test isapprox(vdw_energy(xtal, min_mol, ljff) * 8.314 / 1000, min_E, atol=0.1) - end diff --git a/test/energy_utilities.jl b/test/energy_utilities.jl index efe3beb80..b70275b52 100644 --- a/test/energy_utilities.jl +++ b/test/energy_utilities.jl @@ -1,33 +1,39 @@ -module Energetics_Util_Test - -using PorousMaterials, Test - -@testset "Energetics_Util Tests" begin - # data types for potential energies - u = PotentialEnergy(10.0, 30.0) - v = PotentialEnergy(3.0, 4.0) - @test ! isapprox(v, PotentialEnergy(3.0, 1.2), verbose=false) # isapprox - @test isapprox(sum(v), 7.0) # sum - @test isapprox(u + v, PotentialEnergy(13.0, 34.0)) # + - @test isapprox(u - v, PotentialEnergy(7.0, 26.0)) # - - @test isapprox(2.0 * v, PotentialEnergy(6.0, 8.0)) # * - @test isapprox(v * 2.0, PotentialEnergy(6.0, 8.0)) # * - @test isapprox(u / 2.0, PotentialEnergy(5.0, 15.0)) # / - @test isapprox(sqrt(PotentialEnergy(4.0, 16.0)), PotentialEnergy(2.0, 4.0)) # sqrt - @test isapprox(PorousMaterials.square(PotentialEnergy(2.0, 4.0)), PotentialEnergy(4.0, 16.0)) # square - - t = PotentialEnergy(1.0, 2.0) - s = PotentialEnergy(300.0, 100.0) - us = SystemPotentialEnergy(u, v) - vs = SystemPotentialEnergy(s, t) - @test isapprox(sum(vs), 403.0) # sum - @test isapprox(us - vs, SystemPotentialEnergy(u - s, v - t)) # - - @test isapprox(us + vs, SystemPotentialEnergy(u + s, v + t)) # - - @test isapprox(2.0 * us, SystemPotentialEnergy(2.0 * u, 2.0 * v)) # * - @test isapprox(2.0 * us, SystemPotentialEnergy(2.0 * u, 2.0 * v)) # * - @test isapprox(us * 2.0, SystemPotentialEnergy(2.0 * u, 2.0 * v)) # * - @test isapprox(us / 2.0, SystemPotentialEnergy(u / 2.0, v / 2.0)) # / - @test isapprox(sqrt(us), SystemPotentialEnergy(sqrt(u), sqrt(v))) # sqrt - @test isapprox(PorousMaterials.square(us), SystemPotentialEnergy(PorousMaterials.square(u), PorousMaterials.square(v))) # square -end -end +module Energetics_Util_Test + +using PorousMaterials, Test + +@testset "Energetics_Util Tests" begin + # data types for potential energies + u = PotentialEnergy(10.0, 30.0) + v = PotentialEnergy(3.0, 4.0) + @test !isapprox(v, PotentialEnergy(3.0, 1.2); verbose=false) # isapprox + @test isapprox(sum(v), 7.0) # sum + @test isapprox(u + v, PotentialEnergy(13.0, 34.0)) # + + @test isapprox(u - v, PotentialEnergy(7.0, 26.0)) # - + @test isapprox(2.0 * v, PotentialEnergy(6.0, 8.0)) # * + @test isapprox(v * 2.0, PotentialEnergy(6.0, 8.0)) # * + @test isapprox(u / 2.0, PotentialEnergy(5.0, 15.0)) # / + @test isapprox(sqrt(PotentialEnergy(4.0, 16.0)), PotentialEnergy(2.0, 4.0)) # sqrt + @test isapprox( + PorousMaterials.square(PotentialEnergy(2.0, 4.0)), + PotentialEnergy(4.0, 16.0) + ) # square + + t = PotentialEnergy(1.0, 2.0) + s = PotentialEnergy(300.0, 100.0) + us = SystemPotentialEnergy(u, v) + vs = SystemPotentialEnergy(s, t) + @test isapprox(sum(vs), 403.0) # sum + @test isapprox(us - vs, SystemPotentialEnergy(u - s, v - t)) # - + @test isapprox(us + vs, SystemPotentialEnergy(u + s, v + t)) # - + @test isapprox(2.0 * us, SystemPotentialEnergy(2.0 * u, 2.0 * v)) # * + @test isapprox(2.0 * us, SystemPotentialEnergy(2.0 * u, 2.0 * v)) # * + @test isapprox(us * 2.0, SystemPotentialEnergy(2.0 * u, 2.0 * v)) # * + @test isapprox(us / 2.0, SystemPotentialEnergy(u / 2.0, v / 2.0)) # / + @test isapprox(sqrt(us), SystemPotentialEnergy(sqrt(u), sqrt(v))) # sqrt + @test isapprox( + PorousMaterials.square(us), + SystemPotentialEnergy(PorousMaterials.square(u), PorousMaterials.square(v)) + ) # square +end +end diff --git a/test/eos.jl b/test/eos.jl index 89661de39..d19d4d5ba 100644 --- a/test/eos.jl +++ b/test/eos.jl @@ -1,36 +1,36 @@ -module EOS_Test - -using PorousMaterials, Test, Polynomials - -@testset "equation of state (eos.jl) Tests" begin - # test that we are reading in params correctly - fluid = PengRobinsonFluid(:Xe) - @test isapprox(fluid.Tc, 289.733, atol=0.001) - @test isapprox(fluid.Pc, 58.420, atol=0.001) - @test isapprox(fluid.ω, 0.00363, atol=0.00001) - - # Peng-Robinsion EOS test for methane. - fluid = PengRobinsonFluid(:CH4) - props = calculate_properties(fluid, 298.0, 65.0, verbose=false) - @test isapprox(props["compressibility factor"], 0.874496226625811, atol=1e-4) - @test isapprox(props["fugacity coefficient"], 0.8729028157628362, atol=1e-4) - @test isapprox(props["fugacity (bar)"], 65.0 * 0.8729028157628362, atol=1e-4) - @test isapprox(props["density (mol/m³)"], 3000.054418, atol=0.2) - @test isapprox(props["molar volume (L/mol)"], 0.333327, atol=1e-4) - - # Van der Waals EOS test for CO2 compressibility, molar volume, and density. - # https://www.webqc.org/van_der_waals_gas_law.html - fluid = VdWFluid(:CO2_test) - props = calculate_properties(fluid, 150., 50., verbose=false) - @test isapprox(props["compressibility factor"], 0.20609717091, atol=1e-4) - @test isapprox(props["density (mol/m³)"], 19452.3715854, atol=0.2) - @test isapprox(props["molar volume (L/mol)"], 0.051407613493789, atol=1e-4) - - # Van der Waals EOS test for nitrogen fugacity and fugacity coefficient. - # https://mabdelsalam.kau.edu.sa/Files/0053615/files/16538_Lecture%207%20phase%20equilibrium.pdf - fluid = VdWFluid(:N2_test) - props = calculate_properties(fluid, 298., 50., verbose=false) - @test isapprox(props["fugacity coefficient"], 0.964, atol = 1e-2) - @test isapprox(props["fugacity (bar)"], 48.2, atol = 0.1) -end -end +module EOS_Test + +using PorousMaterials, Test, Polynomials + +@testset "equation of state (eos.jl) Tests" begin + # test that we are reading in params correctly + fluid = PengRobinsonFluid(:Xe) + @test isapprox(fluid.Tc, 289.733, atol=0.001) + @test isapprox(fluid.Pc, 58.420, atol=0.001) + @test isapprox(fluid.ω, 0.00363, atol=0.00001) + + # Peng-Robinsion EOS test for methane. + fluid = PengRobinsonFluid(:CH4) + props = calculate_properties(fluid, 298.0, 65.0; verbose=false) + @test isapprox(props["compressibility factor"], 0.874496226625811, atol=1e-4) + @test isapprox(props["fugacity coefficient"], 0.8729028157628362, atol=1e-4) + @test isapprox(props["fugacity (bar)"], 65.0 * 0.8729028157628362, atol=1e-4) + @test isapprox(props["density (mol/m³)"], 3000.054418, atol=0.2) + @test isapprox(props["molar volume (L/mol)"], 0.333327, atol=1e-4) + + # Van der Waals EOS test for CO2 compressibility, molar volume, and density. + # https://www.webqc.org/van_der_waals_gas_law.html + fluid = VdWFluid(:CO2_test) + props = calculate_properties(fluid, 150.0, 50.0; verbose=false) + @test isapprox(props["compressibility factor"], 0.20609717091, atol=1e-4) + @test isapprox(props["density (mol/m³)"], 19452.3715854, atol=0.2) + @test isapprox(props["molar volume (L/mol)"], 0.051407613493789, atol=1e-4) + + # Van der Waals EOS test for nitrogen fugacity and fugacity coefficient. + # https://mabdelsalam.kau.edu.sa/Files/0053615/files/16538_Lecture%207%20phase%20equilibrium.pdf + fluid = VdWFluid(:N2_test) + props = calculate_properties(fluid, 298.0, 50.0; verbose=false) + @test isapprox(props["fugacity coefficient"], 0.964, atol=1e-2) + @test isapprox(props["fugacity (bar)"], 48.2, atol=0.1) +end +end diff --git a/test/ewald_timing.jl b/test/ewald_timing.jl index dae7e0806..658978e23 100644 --- a/test/ewald_timing.jl +++ b/test/ewald_timing.jl @@ -2,17 +2,17 @@ using PorousMaterials using Test using BenchmarkTools using Profile - # using ProfileView +# using ProfileView framework = Framework("NU-1000_Greg.cif") - # kreps = (11, 11, 9) - # α = 0.265058 +# kreps = (11, 11, 9) +# α = 0.265058 sr_cutoff_r = 12.5 rep_factors = replication_factors(framework, sr_cutoff_r) sim_box = replicate(framework.box, rep_factors) framework = replicate(framework, rep_factors) -eparams = setup_Ewald_sum(framework.box, sr_cutoff_r, verbose=false, ϵ=1e-6) +eparams = setup_Ewald_sum(framework.box, sr_cutoff_r; verbose=false, ϵ=1e-6) eikr = Eikr(framework.charges.n_charges, eparams.kreps) q_test = 0.8096 @@ -25,7 +25,7 @@ q_test = 0.8096 @test eparams.kreps == (9, 9, 9) @test isapprox(eparams.α, 0.2471, atol=0.05) # construct box so recip. lattice is dimension (2, 10, 5) - box = Box(0.5*2*π, 0.1*2*π, 0.2*2*π, π/2, π/2, π/2) + box = Box(0.5 * 2 * π, 0.1 * 2 * π, 0.2 * 2 * π, π / 2, π / 2, π / 2) @test PorousMaterials.required_kreps(box, 2.1^2) == (1, 0, 0) @test PorousMaterials.required_kreps(box, 5.1^2) == (2, 0, 1) @test PorousMaterials.required_kreps(box, 10.1^2) == (5, 1, 2) @@ -52,11 +52,11 @@ m = Ion(q_test, xf) ϕ = electrostatic_potential_energy(framework, m, eparams, eikr) @btime electrostatic_potential_energy(framework, m, eparams, eikr) - # @profile electrostatic_potential_energy(framework, m, eparams, eikar, eikbr, eikcr) - # ProfileView.view() +# @profile electrostatic_potential_energy(framework, m, eparams, eikar, eikbr, eikcr) +# ProfileView.view() - # ϕ = ϕ_sr(framework, x, rep_factors, sr_cutoff, α) - # @btime ϕ_sr(framework, x, rep_factors, sr_cutoff, α) - # - # ϕ = ϕ_lr(framework, x, sim_box, rep_factors, kvectors, α) - # @btime ϕ_lr(framework, x, sim_box, rep_factors, kvectors, α) +# ϕ = ϕ_sr(framework, x, rep_factors, sr_cutoff, α) +# @btime ϕ_sr(framework, x, rep_factors, sr_cutoff, α) +# +# ϕ = ϕ_lr(framework, x, sim_box, rep_factors, kvectors, α) +# @btime ϕ_lr(framework, x, sim_box, rep_factors, kvectors, α) diff --git a/test/forcefield.jl b/test/forcefield.jl index aa4b13f88..53f3b5adc 100644 --- a/test/forcefield.jl +++ b/test/forcefield.jl @@ -1,38 +1,37 @@ -module Forcefield_Test - -using PorousMaterials -using OffsetArrays -using LinearAlgebra -using Test -using JLD2 -using Statistics -using Random - -@testset "Forcefield Tests" begin - ljforcefield = LJForceField("Dreiding", r_cutoff=12.5, - mixing_rules="Lorentz-Berthelot") # Dreiding - # test reading of force field - @test ljforcefield.pure_σ[:He] == 1.0 - @test ljforcefield.pure_ϵ[:Zn] == 12.0 - @test ljforcefield.σ²[:Zn][:He] == ((1.0 + 3.0) / 2) ^ 2 - @test ljforcefield.ϵ[:He][:Zn] == sqrt(12.0 * 3.0) - @test ljforcefield.ϵ[:He][:Zn] == ljforcefield.ϵ[:Zn][:He] # symmetry - @test ljforcefield.σ²[:He][:Zn] == ljforcefield.σ²[:Zn][:He] # symmetry - @test ljforcefield.r²_cutoff == 12.5 ^ 2 - - # TODO test two other mixing rules - # TODO test rep factors better - - # test calculation of replication factors required - crystal = Crystal("test_structure.cif") # .cif - strip_numbers_from_atom_labels!(crystal) - rep_factors = replication_factors(crystal.box, ljforcefield) - @test rep_factors == (25, 25, 25) - - # test check for force field coverage - SBMOF1 = Crystal("SBMOF-1.cif") - @test forcefield_coverage(SBMOF1.atoms, ljforcefield) - @test forcefield_coverage(SBMOF1, ljforcefield) - @test ! forcefield_coverage(SBMOF1.atoms, LJForceField("bogus")) -end -end +module Forcefield_Test + +using PorousMaterials +using OffsetArrays +using LinearAlgebra +using Test +using JLD2 +using Statistics +using Random + +@testset "Forcefield Tests" begin + ljforcefield = LJForceField("Dreiding"; r_cutoff=12.5, mixing_rules="Lorentz-Berthelot") # Dreiding + # test reading of force field + @test ljforcefield.pure_σ[:He] == 1.0 + @test ljforcefield.pure_ϵ[:Zn] == 12.0 + @test ljforcefield.σ²[:Zn][:He] == ((1.0 + 3.0) / 2)^2 + @test ljforcefield.ϵ[:He][:Zn] == sqrt(12.0 * 3.0) + @test ljforcefield.ϵ[:He][:Zn] == ljforcefield.ϵ[:Zn][:He] # symmetry + @test ljforcefield.σ²[:He][:Zn] == ljforcefield.σ²[:Zn][:He] # symmetry + @test ljforcefield.r²_cutoff == 12.5^2 + + # TODO test two other mixing rules + # TODO test rep factors better + + # test calculation of replication factors required + crystal = Crystal("test_structure.cif") # .cif + strip_numbers_from_atom_labels!(crystal) + rep_factors = replication_factors(crystal.box, ljforcefield) + @test rep_factors == (25, 25, 25) + + # test check for force field coverage + SBMOF1 = Crystal("SBMOF-1.cif") + @test forcefield_coverage(SBMOF1.atoms, ljforcefield) + @test forcefield_coverage(SBMOF1, ljforcefield) + @test !forcefield_coverage(SBMOF1.atoms, LJForceField("bogus")) +end +end diff --git a/test/gcmc_long.jl b/test/gcmc_long.jl index 6aab61a23..e1315b31f 100644 --- a/test/gcmc_long.jl +++ b/test/gcmc_long.jl @@ -8,11 +8,11 @@ using Printf using Statistics tests_to_run = Dict( - "ideal_gas" => true, - "Xe in SBMOF-1" => true, - "CO2 in ZIF-71" => false, # will not work in current version - "density grid" => true - ) + "ideal_gas" => true, + "Xe in SBMOF-1" => true, + "CO2 in ZIF-71" => false, # will not work in current version + "density grid" => true +) @testset "GCMC (long) tests" begin # @@ -23,7 +23,7 @@ tests_to_run = Dict( # if tests_to_run["ideal_gas"] @info "running ideal gas test" - empty_space = replicate(Crystal("empty_box.cssr"), (3,3,3)) # zero atoms! + empty_space = replicate(Crystal("empty_box.cssr"), (3, 3, 3)) # zero atoms! ideal_gas = Molecule("IG") @assert empty_space.atoms.n == 0 forcefield = LJForceField("Dreiding") @@ -32,10 +32,23 @@ tests_to_run = Dict( # according to ideal gas law, number of molecules in box should be: n_ig = fugacity * empty_space.box.Ω / (BOLTZMANN * temperature) * 100000.0 n_sim = similar(n_ig) - for i = eachindex(fugacity) - results, molecules = μVT_sim(empty_space, ideal_gas, temperature, fugacity[i], forcefield, - n_burn_cycles=100000, n_sample_cycles=100000, verbose=false) - @printf("fugacity %f, n_ig = %f, n_sim = %f\n", fugacity[i], n_ig[i], results["⟨N⟩ (molecules/unit cell)"]) + for i in eachindex(fugacity) + results, molecules = μVT_sim( + empty_space, + ideal_gas, + temperature, + fugacity[i], + forcefield; + n_burn_cycles=100000, + n_sample_cycles=100000, + verbose=false + ) + @printf( + "fugacity %f, n_ig = %f, n_sim = %f\n", + fugacity[i], + n_ig[i], + results["⟨N⟩ (molecules/unit cell)"] + ) @test isapprox(results["⟨N⟩ (molecules/unit cell)"], n_ig[i], rtol=0.05) end end @@ -46,12 +59,21 @@ tests_to_run = Dict( n_pts = (4, 4, 4) # testing a grid with 4x4x4 voxels empty_space = Crystal("empty_box.cssr") # zero atoms! ideal_gas = Molecule("IG") - forcefield = LJForceField("Dreiding", r_cutoff=4.0) + forcefield = LJForceField("Dreiding"; r_cutoff=4.0) temperature = 25.0 fugacity = 500.0 # mk sure lots of molecules - results, molecules = μVT_sim(empty_space, ideal_gas, temperature, fugacity, forcefield, - n_burn_cycles=1, n_sample_cycles=1000, calculate_density_grid=true, density_grid_dx=10.1, - show_progress_bar=true) + results, molecules = μVT_sim( + empty_space, + ideal_gas, + temperature, + fugacity, + forcefield; + n_burn_cycles=1, + n_sample_cycles=1000, + calculate_density_grid=true, + density_grid_dx=10.1, + show_progress_bar=true + ) mean_density = mean(results["density grid"].data) println(results["density grid"].data) @test all(isapprox.(results["density grid"].data, mean_density, rtol=0.05)) @@ -61,19 +83,33 @@ tests_to_run = Dict( if tests_to_run["Xe in SBMOF-1"] @info "running Xe in SBMOF-1" xtal = Crystal("SBMOF-1.cif") - ljff = LJForceField("Dreiding", r_cutoff=12.5) + ljff = LJForceField("Dreiding"; r_cutoff=12.5) molecule = Molecule("Xe") test_fugacities = [20.0, 200.0, 2000.0] / 100000.0 # bar test_mmol_g = [0.18650, 1.00235, 1.39812] test_molec_unit_cell = [0.2568, 1.3806, 1.9257] - # results = adsorption_isotherm(sbmof1, 298.0, test_fugacities, molecule, dreiding_forcefield, n_burn_cycles=20, n_sample_cycles=20, verbose=true) - results = stepwise_adsorption_isotherm(xtal, molecule, 298.0, test_fugacities, ljff, - n_burn_cycles=25000, n_sample_cycles=25000, verbose=true, sample_frequency=1, show_progress_bar=true) + # results = adsorption_isotherm(sbmof1, 298.0, test_fugacities, molecule, dreiding_forcefield, n_burn_cycles=20, n_sample_cycles=20, verbose=true) + results = stepwise_adsorption_isotherm( + xtal, + molecule, + 298.0, + test_fugacities, + ljff; + n_burn_cycles=25000, + n_sample_cycles=25000, + verbose=true, + sample_frequency=1, + show_progress_bar=true + ) - for i = eachindex(test_fugacities) - @test isapprox(results[i]["⟨N⟩ (molecules/unit cell)"], test_molec_unit_cell[i], rtol=0.025) + for i in eachindex(test_fugacities) + @test isapprox( + results[i]["⟨N⟩ (molecules/unit cell)"], + test_molec_unit_cell[i], + rtol=0.025 + ) @test isapprox(results[i]["⟨N⟩ (mmol/g)"], test_mmol_g[i], rtol=0.025) end end @@ -85,55 +121,73 @@ tests_to_run = Dict( ### # Test isotherm 1: by greg chung. co2 at 313 k ### - # f = Crystal("ZnCo-ZIF-71_atom_relax_RESP.cif") - # co2 = Molecule("CO2") + # f = Crystal("ZnCo-ZIF-71_atom_relax_RESP.cif") + # co2 = Molecule("CO2") - # strip_numbers_from_atom_labels!(f) - # ff = LJForceField("Greg_CO2_GCMCtest_ff.csv", cutoffradius=12.5) - # - # # load in test data - # df = CSV.read("greg_chung/ZnCo-ZIF-71_atom_relax_RESP_CO2_adsorption_isotherm313K_test.csv") - # - # # simulate with PorousMaterials.jl in parallel - # if run_sims - # results = adsorption_isotherm(f, co2, 313.0, convert(Array{Float64, 1}, df[:fugacity_Pa] / 100000.0), ff, - # n_burn_cycles=10000, n_sample_cycles=10000, verbose=true, sample_frequency=5) - # JLD.save("ZnCo-ZIF-71_atom_relax_RESP_co2_simulated_isotherm.jld", "results", results) - # else - # results = JLD.load("ZnCo-ZIF-71_atom_relax_RESP_co2_simulated_isotherm.jld")["results"] - # end - # n_sim = [result["⟨N⟩ (molecules/unit cell)"] for result in results] - # - # # plot comparison - # if plot_results - # figure() - # xlabel("Fugacity (bar)") - # ylabel("Molecules/unit cell") - # scatter(df[:fugacity_Pa] / 100000.0, df[:molecules_per_unit_cell], label="Greg") - # scatter(df[:fugacity_Pa] / 100000.0, n_sim, label="PorousMaterials.jl") - # legend() - # savefig("ZnCo-ZIF-71_atom_relax_RESP_CO2_adsorption_isotherm313K_test.png", format="png", dpi=300) - # end + # strip_numbers_from_atom_labels!(f) + # ff = LJForceField("Greg_CO2_GCMCtest_ff.csv", cutoffradius=12.5) + # + # # load in test data + # df = CSV.read("greg_chung/ZnCo-ZIF-71_atom_relax_RESP_CO2_adsorption_isotherm313K_test.csv") + # + # # simulate with PorousMaterials.jl in parallel + # if run_sims + # results = adsorption_isotherm(f, co2, 313.0, convert(Array{Float64, 1}, df[:fugacity_Pa] / 100000.0), ff, + # n_burn_cycles=10000, n_sample_cycles=10000, verbose=true, sample_frequency=5) + # JLD.save("ZnCo-ZIF-71_atom_relax_RESP_co2_simulated_isotherm.jld", "results", results) + # else + # results = JLD.load("ZnCo-ZIF-71_atom_relax_RESP_co2_simulated_isotherm.jld")["results"] + # end + # n_sim = [result["⟨N⟩ (molecules/unit cell)"] for result in results] + # + # # plot comparison + # if plot_results + # figure() + # xlabel("Fugacity (bar)") + # ylabel("Molecules/unit cell") + # scatter(df[:fugacity_Pa] / 100000.0, df[:molecules_per_unit_cell], label="Greg") + # scatter(df[:fugacity_Pa] / 100000.0, n_sim, label="PorousMaterials.jl") + # legend() + # savefig("ZnCo-ZIF-71_atom_relax_RESP_CO2_adsorption_isotherm313K_test.png", format="png", dpi=300) + # end ### # Test isotherm 2: by greg chung. co2 at 298 K ### zif71 = Crystal("zif71_bogus_charges.cif") strip_numbers_from_atom_labels!(zif71) - ff = LJForceField("Greg_bogus_ZIF71", r_cutoff=12.8) + ff = LJForceField("Greg_bogus_ZIF71"; r_cutoff=12.8) co2 = Molecule("CO2EPM2") # make sure bond lenghts are preserved - results, molecules = μVT_sim(zif71, co2, 298.0, 1.0, ff, - n_burn_cycles=25, n_sample_cycles=25, verbose=false) + results, molecules = μVT_sim( + zif71, + co2, + 298.0, + 1.0, + ff; + n_burn_cycles=25, + n_sample_cycles=25, + verbose=false + ) # load in test data df = CSV.read("greg_chung/zif_71_co2_isotherm_w_preos_fugacity.csv") # simulate with PorousMaterials.jl in parallel if run_sims - results = stepwise_adsorption_isotherm(zif71, co2, 298.0, convert(Array{Float64, 1}, df[:fugacity_Pa] / 100000.0), ff, - n_burn_cycles=2000, n_sample_cycles=5000, verbose=true, sample_frequency=1, ewald_precision=1e-6) + results = stepwise_adsorption_isotherm( + zif71, + co2, + 298.0, + convert(Array{Float64, 1}, df[:fugacity_Pa] / 100000.0), + ff; + n_burn_cycles=2000, + n_sample_cycles=5000, + verbose=true, + sample_frequency=1, + ewald_precision=1e-6 + ) @save "ZIF71_bogus_charges_co2_simulated_isotherm.jld" results else @load "ZIF71_bogus_charges_co2_simulated_isotherm.jld" results @@ -146,10 +200,10 @@ tests_to_run = Dict( figure() xlabel("Fugacity (bar)") ylabel("Molecules/unit cell") - scatter(df[:fugacity_Pa] / 100000.0, df[:L_mmol_g], label="Greg") - scatter(df[:fugacity_Pa] / 100000.0, n_sim, label="PorousMaterials.jl") + scatter(df[:fugacity_Pa] / 100000.0, df[:L_mmol_g]; label="Greg") + scatter(df[:fugacity_Pa] / 100000.0, n_sim; label="PorousMaterials.jl") legend() - savefig("Greg_bogus_ZIF71_298K_co2_isotherm_test.png", format="png", dpi=300) + savefig("Greg_bogus_ZIF71_298K_co2_isotherm_test.png"; format="png", dpi=300) end end end diff --git a/test/gcmc_mixture_long.jl b/test/gcmc_mixture_long.jl index 25b7363a2..07c7dff35 100644 --- a/test/gcmc_mixture_long.jl +++ b/test/gcmc_mixture_long.jl @@ -5,11 +5,12 @@ using CSV using DataFrames using JLD2 -tests_to_run = Dict("Kr/Xe in SBMOF-1" => true, - "run mixture sims" => false, - "run pure gas sims" => false, - "ideal gas mixture" => false - ) +tests_to_run = Dict( + "Kr/Xe in SBMOF-1" => true, + "run mixture sims" => false, + "run pure gas sims" => false, + "ideal gas mixture" => false +) ### # Simulate Kr/Xe mixture in SBMOF-1 @@ -17,68 +18,103 @@ tests_to_run = Dict("Kr/Xe in SBMOF-1" => true, if tests_to_run["Kr/Xe in SBMOF-1"] @info "running Kr/Xe in SBMOF-1 test" # set up sim - xtal = Crystal("SBMOF-1.cif") - adsorbates = ["Kr", "Xe"] - mol_templates = Molecule.(adsorbates) - ljff = LJForceField("UFF", mixing_rules="Lorentz-Berthelot") - temperature = 298.0 + xtal = Crystal("SBMOF-1.cif") + adsorbates = ["Kr", "Xe"] + mol_templates = Molecule.(adsorbates) + ljff = LJForceField("UFF"; mixing_rules="Lorentz-Berthelot") + temperature = 298.0 # n_sample_cycles and n_burn_cycles are on the lower end of minimum requirment to get agreement with RASPA n_sample_cycles = 250000 - n_burn_cycles = 250000 - mol_fraction = [0.9, 0.1] + n_burn_cycles = 250000 + mol_fraction = [0.9, 0.1] # load RASPA (benchmark) data - raspa_data = Dict(:Kr => CSV.read(joinpath(pwd(), "data", "raspa_data/Kr.csv"), DataFrame), - :Xe => CSV.read(joinpath(pwd(), "data", "raspa_data/Xe.csv"), DataFrame), - :mix => CSV.read(joinpath(pwd(), "data", "raspa_data/Kr_Xe.csv"), DataFrame)) + raspa_data = Dict( + :Kr => CSV.read(joinpath(pwd(), "data", "raspa_data/Kr.csv"), DataFrame), + :Xe => CSV.read(joinpath(pwd(), "data", "raspa_data/Xe.csv"), DataFrame), + :mix => CSV.read(joinpath(pwd(), "data", "raspa_data/Kr_Xe.csv"), DataFrame) + ) # run sim over range of total pressures for (i, p) in enumerate([0.01, 0.1, 0.5]) if tests_to_run["run mixture sims"] @warn "Mixture simulation set will take about a day to run on a serial machine." - results, molecules = μVT_sim(xtal, - mol_templates, - temperature, - p * mol_fraction, - ljff, - n_burn_cycles=n_burn_cycles, - n_sample_cycles=n_sample_cycles) + results, molecules = μVT_sim( + xtal, + mol_templates, + temperature, + p * mol_fraction, + ljff; + n_burn_cycles=n_burn_cycles, + n_sample_cycles=n_sample_cycles + ) else # use the simulation files that are provided - filename = μVT_output_filename(xtal, mol_templates, temperature, p * mol_fraction, ljff, - n_burn_cycles, n_sample_cycles) + filename = μVT_output_filename( + xtal, + mol_templates, + temperature, + p * mol_fraction, + ljff, + n_burn_cycles, + n_sample_cycles + ) @load joinpath(rc[:paths][:simulations], filename) results end # evaluate mixture results for (j, sp) in enumerate(adsorbates) # make sure the pressures are correct - @test results["pressure (bar)"][j] ≈ raspa_data[:mix][i, Symbol(sp * " pressure (bar)")] + @test results["pressure (bar)"][j] ≈ + raspa_data[:mix][i, Symbol(sp * " pressure (bar)")] # evaluate adsorption uptake - std_err = raspa_data[:mix][i, Symbol("err $sp loading (mmol/g)")] + results["err ⟨N⟩ (mmol/g)"][j] - @test isapprox(raspa_data[:mix][i, Symbol(sp * " loading (mmol/g)")], results["⟨N⟩ (mmol/g)"][j], atol=std_err) + std_err = + raspa_data[:mix][i, Symbol("err $sp loading (mmol/g)")] + + results["err ⟨N⟩ (mmol/g)"][j] + @test isapprox( + raspa_data[:mix][i, Symbol(sp * " loading (mmol/g)")], + results["⟨N⟩ (mmol/g)"][j], + atol=std_err + ) end # evaluate pure gas results, keep separate sinse we re-assign results dictionary for mol in mol_templates - if tests_to_run["run pure gas sims"] + if tests_to_run["run pure gas sims"] @warn "Running pure component simulation set will take about a day to run on a serial machine." - results, molecules = μVT_sim(xtal, - [mol], - temperature, - [p], - ljff, - n_burn_cycles=50000, - n_sample_cycles=50000) + results, molecules = μVT_sim( + xtal, + [mol], + temperature, + [p], + ljff; + n_burn_cycles=50000, + n_sample_cycles=50000 + ) else # load pure gas dictionary - pure_gas_filename = μVT_output_filename(xtal, [mol], temperature, [p], ljff, 50000, 50000) - @load joinpath(rc[:paths][:simulations], pure_gas_filename) results + pure_gas_filename = + μVT_output_filename(xtal, [mol], temperature, [p], ljff, 50000, 50000) + @load joinpath(rc[:paths][:simulations], pure_gas_filename) results end # make sure the pressures are correct - @test results["pressure (bar)"][1] ≈ raspa_data[mol.species][i, Symbol(String(mol.species) * " pressure (bar)")] + @test results["pressure (bar)"][1] ≈ raspa_data[mol.species][ + i, + Symbol(String(mol.species) * " pressure (bar)") + ] # evaluate adsorption uptake - std_err = raspa_data[mol.species][i, Symbol("err $(String(mol.species)) loading (mmol/g)")] + results["err ⟨N⟩ (mmol/g)"][1] - @test isapprox(raspa_data[mol.species][i, Symbol(String(mol.species) * " loading (mmol/g)")], results["⟨N⟩ (mmol/g)"][1], atol=std_err) + std_err = + raspa_data[mol.species][ + i, + Symbol("err $(String(mol.species)) loading (mmol/g)") + ] + results["err ⟨N⟩ (mmol/g)"][1] + @test isapprox( + raspa_data[mol.species][ + i, + Symbol(String(mol.species) * " loading (mmol/g)") + ], + results["⟨N⟩ (mmol/g)"][1], + atol=std_err + ) end end end @@ -91,33 +127,41 @@ end # basically, this tests the acceptance rules when energy is always zero. if tests_to_run["ideal gas mixture"] @info "running ideal gas mixture test" - empty_space = replicate(Crystal("empty_box.cssr"), (3,3,3)) # zero atoms! - ideal_gas1 = Molecule("ig") - ideal_gas2 = Molecule("ig") + empty_space = replicate(Crystal("empty_box.cssr"), (3, 3, 3)) # zero atoms! + ideal_gas1 = Molecule("ig") + ideal_gas2 = Molecule("ig") @assert empty_space.atoms.n == 0 - forcefield = LJForceField("Dreiding") - temperature = 298.0 - mol_fraction = [0.70, 0.30] - fugacities = 10.0 .^ [2.0, 4.0, 5.0, 6.0, 7.0] / 100000.0 # bar + forcefield = LJForceField("Dreiding") + temperature = 298.0 + mol_fraction = [0.70, 0.30] + fugacities = 10.0 .^ [2.0, 4.0, 5.0, 6.0, 7.0] / 100000.0 # bar partial_fugacities = [mol_fraction * f for f in fugacities] # according to ideal gas law (P_i*V=n_i*k_b*T), number of molecules per species in box should be - n_ig = partial_fugacities * empty_space.box.Ω / (PorousMaterials.BOLTZMANN * temperature) * 100000.0 + n_ig = + partial_fugacities * empty_space.box.Ω / (PorousMaterials.BOLTZMANN * temperature) * + 100000.0 n_sim = similar(n_ig) for i in 1:length(partial_fugacities) @info partial_fugacities[i] - results, molecules = μVT_sim(empty_space, - [ideal_gas1, ideal_gas2], - temperature, - partial_fugacities[i], - forcefield, - n_burn_cycles=1000000, - n_sample_cycles=1000000, - verbose=false) + results, molecules = μVT_sim( + empty_space, + [ideal_gas1, ideal_gas2], + temperature, + partial_fugacities[i], + forcefield; + n_burn_cycles=1000000, + n_sample_cycles=1000000, + verbose=false + ) # print fugacities for s in 1:2 - @printf("\nfugacities %f, n_ig = %f, n_sim = %f\n", - partial_fugacities[i][s], n_ig[i][s], results["⟨N⟩ (molecules/unit cell)"][s]) + @printf( + "\nfugacities %f, n_ig = %f, n_sim = %f\n", + partial_fugacities[i][s], + n_ig[i][s], + results["⟨N⟩ (molecules/unit cell)"][s] + ) end # test results @test isapprox(sum(results["⟨N⟩ (molecules/unit cell)"]), sum(n_ig[i]), rtol=0.05) diff --git a/test/gcmc_quick.jl b/test/gcmc_quick.jl index aefe5f76b..55c66e736 100644 --- a/test/gcmc_quick.jl +++ b/test/gcmc_quick.jl @@ -13,35 +13,54 @@ using Statistics ### # isotherm_sim_results_to_dataframe test ### - desired_props = ["xtal", "adsorbate", "pressure (bar)", - "repfactors", "⟨N⟩ (mmol/g)", "density grid"] + desired_props = [ + "xtal", + "adsorbate", + "pressure (bar)", + "repfactors", + "⟨N⟩ (mmol/g)", + "density grid" + ] - xtal = Crystal("SBMOF-1.cif") - molecule = Molecule("Xe") # adsorbate - ljff = LJForceField("UFF", mixing_rules="Lorentz-Berthelot") - temperature = 298.0 # temperature: K + xtal = Crystal("SBMOF-1.cif") + molecule = Molecule("Xe") # adsorbate + ljff = LJForceField("UFF"; mixing_rules="Lorentz-Berthelot") + temperature = 298.0 # temperature: K n_sample_cycles = 5000 - n_burn_cycles = 5000 + n_burn_cycles = 5000 - pressures = 10 .^ range(-2.0, stop=0.0, length=3) + pressures = 10 .^ range(-2.0; stop=0.0, length=3) ### manually checked test data ### - test_mmol_g =[1.0366454091341166, 1.4113592107687385, 1.4479326116209061] + test_mmol_g = [1.0366454091341166, 1.4113592107687385, 1.4479326116209061] # read output files into dataframe - df_data = isotherm_sim_results_to_dataframe(desired_props, xtal, - molecule, temperature, pressures, - ljff, n_burn_cycles, n_sample_cycles) + df_data = isotherm_sim_results_to_dataframe( + desired_props, + xtal, + molecule, + temperature, + pressures, + ljff, + n_burn_cycles, + n_sample_cycles + ) # check the dataframe entries against manualy checked output - @test all(i -> isapprox(df_data[i, Symbol("pressure (bar)")][1], pressures[i]), 1:length(pressures)) - @test all(i -> isapprox(df_data[i, Symbol("⟨N⟩ (mmol/g)")][1], test_mmol_g[i]), 1:length(test_mmol_g)) + @test all( + i -> isapprox(df_data[i, Symbol("pressure (bar)")][1], pressures[i]), + 1:length(pressures) + ) + @test all( + i -> isapprox(df_data[i, Symbol("⟨N⟩ (mmol/g)")][1], test_mmol_g[i]), + 1:length(test_mmol_g) + ) @test all(df_data[:, :xtal] .== xtal.name) @test all(df_data[:, :adsorbate][1] .== molecule.species) # check the data type of specific columns - @test all(typeof.(df_data[:, :repfactors]) .== Tuple{Int64,Int64,Int64}) + @test all(typeof.(df_data[:, :repfactors]) .== Tuple{Int64, Int64, Int64}) @test all(typeof.(df_data[:, Symbol("density grid")]) .== Grid{Float64}) end diff --git a/test/generic_rotation_test.jl b/test/generic_rotation_test.jl index c27f6aa0e..ab78b6381 100644 --- a/test/generic_rotation_test.jl +++ b/test/generic_rotation_test.jl @@ -1,28 +1,28 @@ -module Generic_Rotation_Test - -using PorousMaterials -using Test -using LinearAlgebra - -@testset "Generic Rotation Tests" begin - # direction and angle about which to rotate - u = randn(3) - u = u / norm(u) - θ = π / 7 - - # test unit vector normalization - @test isapprox(rotation_matrix(θ, u), rotation_matrix(θ, u * 10)) - R = rotation_matrix(θ, u) - @test isapprox(transpose(R) * R, diagm(0 => ones(3))) - # rotate θ, then -θ, will get back... - @test isapprox(rotation_matrix(-θ, u) * rotation_matrix(θ, u * 10), diagm(0 => ones(3))) - - # test rotating about x, y, z axes - @test isapprox(rotation_matrix(θ, [1.0, 0.0, 0.0]), rotation_matrix(θ, 1)) - @test isapprox(rotation_matrix(θ, [0.0, 1.0, 0.0]), rotation_matrix(θ, 2)) - @test isapprox(rotation_matrix(θ, [0.0, 0.0, 1.0]), rotation_matrix(θ, 3)) - - # determinant should be 1.0 - @test isapprox(det(rotation_matrix(rand() * 2 * π, randn(3))), 1.0) -end -end +module Generic_Rotation_Test + +using PorousMaterials +using Test +using LinearAlgebra + +@testset "Generic Rotation Tests" begin + # direction and angle about which to rotate + u = randn(3) + u = u / norm(u) + θ = π / 7 + + # test unit vector normalization + @test isapprox(rotation_matrix(θ, u), rotation_matrix(θ, u * 10)) + R = rotation_matrix(θ, u) + @test isapprox(transpose(R) * R, diagm(0 => ones(3))) + # rotate θ, then -θ, will get back... + @test isapprox(rotation_matrix(-θ, u) * rotation_matrix(θ, u * 10), diagm(0 => ones(3))) + + # test rotating about x, y, z axes + @test isapprox(rotation_matrix(θ, [1.0, 0.0, 0.0]), rotation_matrix(θ, 1)) + @test isapprox(rotation_matrix(θ, [0.0, 1.0, 0.0]), rotation_matrix(θ, 2)) + @test isapprox(rotation_matrix(θ, [0.0, 0.0, 1.0]), rotation_matrix(θ, 3)) + + # determinant should be 1.0 + @test isapprox(det(rotation_matrix(rand() * 2 * π, randn(3))), 1.0) +end +end diff --git a/test/grid.jl b/test/grid.jl index ac8d16ea3..868850993 100644 --- a/test/grid.jl +++ b/test/grid.jl @@ -11,14 +11,20 @@ using Random CH4 = Molecule("CH4") UFF = LJForceField("UFF") -@testset "Grid Tests" begin - # required number of pts - box = Box(1.0, 10.0, 5.0, π/2, π/2, π/2) - @test required_n_pts(box, 0.1) == (11, 101, 51) +@testset "Grid Tests" verbose = true begin + @testset "required_n_pts" verbose = true begin + box = Box(1.0, 10.0, 5.0, π / 2, π / 2, π / 2) + @test required_n_pts(box, 0.1) == (11, 101, 51) + end # test read and write - grid = Grid(Box(0.7, 0.8, 0.9, 1.5, 1.6, 1.7), (3, 3, 3), rand(Float64, (3, 3, 3)), - :kJ_mol, [1., 2., 3.]) + grid = Grid( + Box(0.7, 0.8, 0.9, 1.5, 1.6, 1.7), + (3, 3, 3), + rand(Float64, (3, 3, 3)), + :kJ_mol, + [1.0, 2.0, 3.0] + ) write_cube(grid, "test_grid.cube") grid2 = read_cube("test_grid.cube") @test isapprox(grid, grid2, atol=1e-5) # atol b/c loose precision when reading/writing to file @@ -26,7 +32,8 @@ UFF = LJForceField("UFF") # nearest neighbor ID checker n_pts = (10, 20, 30) @test PorousMaterials._arg_nearest_neighbor(n_pts, [0.001, 0.001, 0.001]) == [1, 1, 1] - @test PorousMaterials._arg_nearest_neighbor(n_pts, [0.999, 0.999, 0.999]) == [10, 20, 30] + @test PorousMaterials._arg_nearest_neighbor(n_pts, [0.999, 0.999, 0.999]) == + [10, 20, 30] idx = [0, 21, 31] PorousMaterials._apply_pbc_to_index!(idx, n_pts) @test idx == [10, 1, 1] @@ -45,7 +52,7 @@ UFF = LJForceField("UFF") write_xyz(crystal) molecule = deepcopy(CH4) forcefield = deepcopy(UFF) - grid = energy_grid(crystal, molecule, forcefield, resolution=5.) + grid = energy_grid(crystal, molecule, forcefield; resolution=5.0) # endpoints included, ensure periodic since endpoints of grid pts included # first cut out huge values. 1e46 == 1.00001e46 @@ -54,9 +61,15 @@ UFF = LJForceField("UFF") @test isapprox(grid.data[:, 1, :], grid.data[:, end, :], atol=1e-7) @test isapprox(grid.data[:, :, 1], grid.data[:, :, end], atol=1e-7) - accessibility_grid, nb_segments_blocked, porosity = compute_accessibility_grid(crystal, - molecule, forcefield, resolution=2., energy_tol=0.0, verbose=false, - write_b4_after_grids=true) + accessibility_grid, nb_segments_blocked, porosity = compute_accessibility_grid( + crystal, + molecule, + forcefield; + resolution=2.0, + energy_tol=0.0, + verbose=false, + write_b4_after_grids=true + ) @test nb_segments_blocked > 0 if zeolite == "LTA" @test nb_segments_blocked == 8 @@ -66,19 +79,19 @@ UFF = LJForceField("UFF") @test isapprox(crystal.box, accessibility_grid.box) if zeolite == "SOD" - @test all(.! accessibility_grid.data) + @test all(.!accessibility_grid.data) end # test accessibility by inserting random particles and writing to .xyz only if not accessible nb_insertions = 100000 x = zeros(3, 0) - for i = 1:nb_insertions + for i in 1:nb_insertions xf = rand(3) if accessible(accessibility_grid, xf) x = hcat(x, crystal.box.f_to_c * xf) @assert accessible(accessibility_grid, xf, (1, 1, 1)) else - @assert ! accessible(accessibility_grid, xf, (1, 1, 1)) + @assert !accessible(accessibility_grid, xf, (1, 1, 1)) end end if zeolite == "SOD" @@ -86,14 +99,22 @@ UFF = LJForceField("UFF") @test length(x) == 0 else xyzfilename = zeolite * "accessible_inertions.xyz" - write_xyz(Atoms([:CH4 for i = 1:size(x)[2]], Cart(x)), xyzfilename) + write_xyz(Atoms([:CH4 for i in 1:size(x)[2]], Cart(x)), xyzfilename) println("See ", xyzfilename) end # w./o blocking (nb = no blocking) - accessibility_grid_nb, nb_segments_blocked_nb, porosity_nb = compute_accessibility_grid(crystal, - molecule, forcefield, resolution=2., energy_tol=0.0, verbose=false, - write_b4_after_grids=false, block_inaccessible_pockets=false) + accessibility_grid_nb, nb_segments_blocked_nb, porosity_nb = + compute_accessibility_grid( + crystal, + molecule, + forcefield; + resolution=2.0, + energy_tol=0.0, + verbose=false, + write_b4_after_grids=false, + block_inaccessible_pockets=false + ) @test nb_segments_blocked_nb == 0 @test porosity_nb > porosity[:after_blocking] @test isapprox(porosity_nb, porosity[:b4_blocking]) @@ -103,78 +124,112 @@ UFF = LJForceField("UFF") crystal = Crystal("LTA.cif") molecule = deepcopy(CH4) forcefield = deepcopy(UFF) - accessibility_grid, nb_segments_blocked, porosity = compute_accessibility_grid(crystal, - molecule, forcefield, resolution=2., energy_tol=0.0, verbose=false, - write_b4_after_grids=true) + accessibility_grid, nb_segments_blocked, porosity = compute_accessibility_grid( + crystal, + molecule, + forcefield; + resolution=2.0, + energy_tol=0.0, + verbose=false, + write_b4_after_grids=true + ) # replicate crystal and build accessibility grid that includes the other accessibility grid in a corner repfactors = (2, 3, 1) crystal = replicate(crystal, repfactors) - rep_accessibility_grid, rep_nb_segments_blocked, porosity = compute_accessibility_grid(crystal, - molecule, forcefield, resolution=2., energy_tol=0.0, verbose=false, - write_b4_after_grids=true) + rep_accessibility_grid, rep_nb_segments_blocked, porosity = compute_accessibility_grid( + crystal, + molecule, + forcefield; + resolution=2.0, + energy_tol=0.0, + verbose=false, + write_b4_after_grids=true + ) @test all(accessibility_grid.data .== rep_accessibility_grid.data[1:7, 1:7, 1:7]) @test rep_nb_segments_blocked > 0 same_accessibility_repfactors = true - for i = 1:10000 + for i in 1:10000 xf = rand(3) # in (2, 3, 1) box - if ! (accessible(rep_accessibility_grid, xf) == accessible(accessibility_grid, xf, repfactors)) + if !( + accessible(rep_accessibility_grid, xf) == + accessible(accessibility_grid, xf, repfactors) + ) same_accessibility_repfactors = false end end @test same_accessibility_repfactors - # SBMOF-1, CAXVILL_clean hv no pockets blocked. test accessibility grid w./o pocket blocking - molecule = deepcopy(CH4) - forcefield = deepcopy(UFF) - for crystal in [Crystal("SBMOF-1.cif"), Crystal("CAXVII_clean.cif")] - - # w./ blocking - accessibility_grid, nb_segments_blocked, porosity = compute_accessibility_grid(crystal, - molecule, forcefield, energy_tol=5.0, verbose=false, - write_b4_after_grids=true, energy_units=:kJ_mol) - @test nb_segments_blocked == 0 - - # w./o blocking (nb = no blocking) - accessibility_grid_nb, nb_segments_blocked_nb, porosity_nb = compute_accessibility_grid(crystal, - molecule, forcefield, energy_tol=5.0, verbose=false, energy_units=:kJ_mol, - write_b4_after_grids=true, block_inaccessible_pockets=false) + @testset "accessibility grid w./o pocket blocking" verbose = true begin + molecule = deepcopy(CH4) + forcefield = deepcopy(UFF) + for crystal in [Crystal("SBMOF-1.cif"), Crystal("CAXVII_clean.cif")] + + # w./ blocking + accessibility_grid, nb_segments_blocked, porosity = compute_accessibility_grid( + crystal, + molecule, + forcefield; + energy_tol=5.0, + verbose=false, + write_b4_after_grids=true, + energy_units=:kJ_mol + ) + @test nb_segments_blocked == 0 + + # w./o blocking (nb = no blocking) + accessibility_grid_nb, nb_segments_blocked_nb, porosity_nb = + compute_accessibility_grid( + crystal, + molecule, + forcefield; + energy_tol=5.0, + verbose=false, + energy_units=:kJ_mol, + write_b4_after_grids=true, + block_inaccessible_pockets=false + ) + + @test isapprox(porosity_nb, porosity[:b4_blocking]) + @test nb_segments_blocked_nb == 0 + @test isapprox(accessibility_grid, accessibility_grid_nb) + end + end - @test isapprox(porosity_nb, porosity[:b4_blocking]) - @test nb_segments_blocked_nb == 0 - @test isapprox(accessibility_grid, accessibility_grid_nb) + @testset "xf_to_id" verbose = true begin + n_pts = (4, 4, 4) # testing a grid with 4x4x4 voxels + @test all(xf_to_id(n_pts, [0.0001, 0.0001, 0.0001]) .== 1) + @test all(xf_to_id(n_pts, [0.9999, 0.9999, 0.9999]) .== n_pts[end]) + @test all(xf_to_id(n_pts, [0.0001, 0.2400, 0.2600]) .== [1, 1, 2]) + @test all(xf_to_id(n_pts, [-0.0001, 0.2400, 1.01]) .== [4, 1, 1]) # PBC check end - # test xf_to_id - n_pts = (4, 4, 4) # testing a grid with 4x4x4 voxels - @test all(xf_to_id(n_pts, [0.0001, 0.0001, 0.0001]) .== 1) - @test all(xf_to_id(n_pts, [0.9999, 0.9999, 0.9999]) .== n_pts[end]) - @test all(xf_to_id(n_pts, [0.0001, 0.2400, 0.2600]) .== [1, 1, 2]) - @test all(xf_to_id(n_pts, [-0.0001, 0.2400, 1.01]) .== [4, 1, 1]) # PBC check - - # test update_density! - unit_box = unit_cube() - density_grid_c_co2 = Grid(unit_box, n_pts, zeros(n_pts...), :invers_A3, [0.0, 0.0, 0.0]) - density_grid_o_co2 = Grid(unit_box, n_pts, zeros(n_pts...), :invers_A3, [0.0, 0.0, 0.0]) - molecule = Molecule("CO2") - molecule = Frac(molecule, unit_box) - translate_to!(molecule, Frac([0.24, 0.26, 0.99])) - update_density!(density_grid_c_co2, [molecule], :C_CO2) # only updates for a single atom - @test isapprox(sum(density_grid_c_co2.data), 1.0) - @test isapprox(density_grid_c_co2.data[1, 2, 4], 1.0) - - update_density!(density_grid_o_co2, [molecule], :O_CO2) # only updates for a single atom - @test isapprox(sum(density_grid_o_co2.data), 2.0) - - translate_to!(molecule, Frac([-0.26, 0.26, 1.1])) # test PBCs - update_density!(density_grid_c_co2, [molecule], :C_CO2) # only updates for a single atom - @test isapprox(sum(density_grid_c_co2.data), 2.0) - @test isapprox(density_grid_c_co2.data[3, 2, 1], 1.0) + @testset "update_density!" verbose = true begin + n_pts = (4, 4, 4) # testing a grid with 4x4x4 voxels + unit_box = unit_cube() + density_grid_c_co2 = + Grid(unit_box, n_pts, zeros(n_pts...), :invers_A3, [0.0, 0.0, 0.0]) + density_grid_o_co2 = + Grid(unit_box, n_pts, zeros(n_pts...), :invers_A3, [0.0, 0.0, 0.0]) + molecule = Molecule("CO2") + molecule = Frac(molecule, unit_box) + translate_to!(molecule, Frac([0.24, 0.26, 0.99])) + update_density!(density_grid_c_co2, [molecule], :C_CO2) # only updates for a single atom + @test isapprox(sum(density_grid_c_co2.data), 1.0) + @test isapprox(density_grid_c_co2.data[1, 2, 4], 1.0) + + update_density!(density_grid_o_co2, [molecule], :O_CO2) # only updates for a single atom + @test isapprox(sum(density_grid_o_co2.data), 2.0) + + translate_to!(molecule, Frac([-0.26, 0.26, 1.1])) # test PBCs + update_density!(density_grid_c_co2, [molecule], :C_CO2) # only updates for a single atom + @test isapprox(sum(density_grid_c_co2.data), 2.0) + @test isapprox(density_grid_c_co2.data[3, 2, 1], 1.0) + end # xf to id @test isapprox(id_to_xf((1, 1, 1), (10, 12, 14)), zeros(3)) @test isapprox(id_to_xf((10, 12, 14), (10, 12, 14)), ones(3)) @test isapprox(id_to_xf((2, 2, 2), (3, 3, 3)), [0.5, 0.5, 0.5]) - end end diff --git a/test/henry.jl b/test/henry.jl index 82b8dd55a..7290b368f 100644 --- a/test/henry.jl +++ b/test/henry.jl @@ -11,12 +11,18 @@ insertions_per_volume = 500 # Henry test 1: Xe in SBMOF-1 ### crystal = Crystal("SBMOF-1.cif") - ljff = LJForceField("Dreiding", r_cutoff=12.5) + ljff = LJForceField("Dreiding"; r_cutoff=12.5) molecule = Molecule("Xe") temperature = 298.0 - result = henry_coefficient(crystal, molecule, temperature, ljff, - insertions_per_volume=insertions_per_volume, verbose=true) + result = henry_coefficient( + crystal, + molecule, + temperature, + ljff; + insertions_per_volume=insertions_per_volume, + verbose=true + ) @test isapprox(result["henry coefficient [mol/(kg-Pa)]"], 0.00985348, rtol=0.025) @test isapprox(result["⟨U⟩ (kJ/mol)"], -39.6811, rtol=0.025) @@ -24,12 +30,18 @@ insertions_per_volume = 500 # Henry test 2: CO2 in CAXVII_clean.cif ### crystal = Crystal("CAXVII_clean.cif") - ljff = LJForceField("Dreiding", r_cutoff=12.5) + ljff = LJForceField("Dreiding"; r_cutoff=12.5) molecule = Molecule("CO2") temperature = 298.0 - result = henry_coefficient(crystal, molecule, temperature, ljff, - insertions_per_volume=insertions_per_volume, verbose=true) + result = henry_coefficient( + crystal, + molecule, + temperature, + ljff; + insertions_per_volume=insertions_per_volume, + verbose=true + ) @test isapprox(result["henry coefficient [mol/(kg-Pa)]"], 2.88317e-05, rtol=0.025) @test isapprox(result["⟨U⟩ (kJ/mol)"], -18.69582223, rtol=0.025) # should not change molecule passed... @@ -42,22 +54,50 @@ insertions_per_volume = 500 molecule = Molecule("CH4") ljff = LJForceField("UFF") temperature = 298.0 - result = henry_coefficient(crystal, molecule, temperature, ljff, - insertions_per_volume=insertions_per_volume, verbose=true) + result = henry_coefficient( + crystal, + molecule, + temperature, + ljff; + insertions_per_volume=insertions_per_volume, + verbose=true + ) - accessibility_grid, nb_blocked_pockets = compute_accessibility_grid(crystal, - molecule, ljff, n_pts=(50, 50, 50), energy_tol=3.0 * temperature, verbose=true, - write_b4_after_grids=false) + accessibility_grid, nb_blocked_pockets = compute_accessibility_grid( + crystal, + molecule, + ljff; + n_pts=(50, 50, 50), + energy_tol=3.0 * temperature, + verbose=true, + write_b4_after_grids=false + ) @test nb_blocked_pockets > 0 - - result = henry_coefficient(crystal, molecule, temperature, ljff, - insertions_per_volume=insertions_per_volume, verbose=true, - accessibility_grid=accessibility_grid) - + + result = henry_coefficient( + crystal, + molecule, + temperature, + ljff; + insertions_per_volume=insertions_per_volume, + verbose=true, + accessibility_grid=accessibility_grid + ) + crystal = Crystal("LTA_manually_blocked.cif") - result_manual_block = henry_coefficient(crystal, molecule, temperature, ljff, - insertions_per_volume=insertions_per_volume, verbose=true, - accessibility_grid=nothing) + result_manual_block = henry_coefficient( + crystal, + molecule, + temperature, + ljff; + insertions_per_volume=insertions_per_volume, + verbose=true, + accessibility_grid=nothing + ) @test isapprox(result["⟨U⟩ (kJ/mol)"], result_manual_block["⟨U⟩ (kJ/mol)"], rtol=0.01) - @test isapprox(result["henry coefficient [mmol/(g-bar)]"], result_manual_block["henry coefficient [mmol/(g-bar)]"], rtol=0.01) + @test isapprox( + result["henry coefficient [mmol/(g-bar)]"], + result_manual_block["henry coefficient [mmol/(g-bar)]"], + rtol=0.01 + ) end diff --git a/test/henry_test.jl b/test/henry_test.jl index 682c71edf..364acf1b1 100644 --- a/test/henry_test.jl +++ b/test/henry_test.jl @@ -11,12 +11,18 @@ insertions_per_volume = 1000 # Henry test 1: Xe in SBMOF-1 ### framework = Framework("SBMOF-1.cif") - ljff = LJForceField("Dreiding.csv", cutoffradius=12.5) + ljff = LJForceField("Dreiding.csv"; cutoffradius=12.5) molecule = Molecule("Xe") temperature = 298.0 - result = henry_coefficient(framework, molecule, temperature, ljff, - insertions_per_volume=insertions_per_volume, verbose=true) + result = henry_coefficient( + framework, + molecule, + temperature, + ljff; + insertions_per_volume=insertions_per_volume, + verbose=true + ) @test isapprox(result["henry coefficient [mol/(kg-Pa)]"], 0.00985348, atol=0.0002) @test isapprox(result["⟨U⟩ (kJ/mol)"], -39.6811, atol=0.1) @@ -24,12 +30,18 @@ insertions_per_volume = 1000 # Henry test 2: CO2 in CAXVII_clean.cif ### framework = Framework("CAXVII_clean.cif") - ljff = LJForceField("Dreiding.csv", cutoffradius=12.5) + ljff = LJForceField("Dreiding.csv"; cutoffradius=12.5) molecule = Molecule("CO2") temperature = 298.0 - result = henry_coefficient(framework, molecule, temperature, ljff, - insertions_per_volume=insertions_per_volume, verbose=true) + result = henry_coefficient( + framework, + molecule, + temperature, + ljff; + insertions_per_volume=insertions_per_volume, + verbose=true + ) @test isapprox(result["henry coefficient [mol/(kg-Pa)]"], 2.88317e-05, atol=1.5e-7) @test isapprox(result["⟨U⟩ (kJ/mol)"], -18.69582223, atol=0.1) # should not change molecule passed... @@ -42,22 +54,50 @@ insertions_per_volume = 1000 molecule = Molecule("CH4") ljff = LJForceField("UFF.csv") temperature = 298.0 - result = henry_coefficient(framework, molecule, temperature, ljff, - insertions_per_volume=insertions_per_volume, verbose=true) + result = henry_coefficient( + framework, + molecule, + temperature, + ljff; + insertions_per_volume=insertions_per_volume, + verbose=true + ) - accessibility_grid, some_pockets_were_blocked = compute_accessibility_grid(framework, - molecule, ljff, n_pts=(50, 50, 50), energy_tol=3.0 * temperature, verbose=true, - write_b4_after_grids=false) + accessibility_grid, some_pockets_were_blocked = compute_accessibility_grid( + framework, + molecule, + ljff; + n_pts=(50, 50, 50), + energy_tol=3.0 * temperature, + verbose=true, + write_b4_after_grids=false + ) @test some_pockets_were_blocked - - result = henry_coefficient(framework, molecule, temperature, ljff, - insertions_per_volume=insertions_per_volume, verbose=true, - accessibility_grid=accessibility_grid) - + + result = henry_coefficient( + framework, + molecule, + temperature, + ljff; + insertions_per_volume=insertions_per_volume, + verbose=true, + accessibility_grid=accessibility_grid + ) + framework = Framework("LTA_manually_blocked.cif") - result_manual_block = henry_coefficient(framework, molecule, temperature, ljff, - insertions_per_volume=insertions_per_volume, verbose=true, - accessibility_grid=nothing) + result_manual_block = henry_coefficient( + framework, + molecule, + temperature, + ljff; + insertions_per_volume=insertions_per_volume, + verbose=true, + accessibility_grid=nothing + ) @test isapprox(result["⟨U⟩ (kJ/mol)"], result_manual_block["⟨U⟩ (kJ/mol)"], rtol=0.01) - @test isapprox(result["henry coefficient [mmol/(g-bar)]"], result_manual_block["henry coefficient [mmol/(g-bar)]"], rtol=0.01) + @test isapprox( + result["henry coefficient [mmol/(g-bar)]"], + result_manual_block["henry coefficient [mmol/(g-bar)]"], + rtol=0.01 + ) end diff --git a/test/isotherm_fitting.jl b/test/isotherm_fitting.jl index 054f5c9ab..f1ac68eb0 100644 --- a/test/isotherm_fitting.jl +++ b/test/isotherm_fitting.jl @@ -9,20 +9,21 @@ using Random @testset "Adsorption isotherm fitting Tests" begin # Henry - df = DataFrame(P = [0.0, 0.56, 1.333], N = [0.0, 0.534, 1.295]) + df = DataFrame(; P=[0.0, 0.56, 1.333], N=[0.0, 0.534, 1.295]) henry = fit_adsorption_isotherm(df, :P, :N, :henry)["H"] @test isapprox(henry, 0.9688044280548711) - henry = fit_adsorption_isotherm(df, :P, :N, :henry, Optim.Options(iterations=100))["H"] + henry = + fit_adsorption_isotherm(df, :P, :N, :henry, Optim.Options(; iterations=100))["H"] @test isapprox(henry, 0.9688044280548711) - + # Langmuir - P = range(0, stop=1, length=100) + P = range(0; stop=1, length=100) M = 23.0 K = 11.9 - N = (M * K .* P) ./ (1 .+ K.*P) + N = (M * K .* P) ./ (1 .+ K .* P) ids_fit = [1, 6, 11, 21, 31, 36, 41, 51, 61, 71, 76, 81, 91] shuffle!(ids_fit) - df = DataFrame(P = P[ids_fit], N = N[ids_fit]) + df = DataFrame(; P=P[ids_fit], N=N[ids_fit]) x = fit_adsorption_isotherm(df, :P, :N, :langmuir) M_opt, K_opt = x["M"], x["K"] @test isapprox(M, M_opt, rtol=1e-6) diff --git a/test/mc_helpers.jl b/test/mc_helpers.jl index fcb5e704f..c980751e7 100644 --- a/test/mc_helpers.jl +++ b/test/mc_helpers.jl @@ -1,191 +1,198 @@ -module MCHelpers_Test - -using PorousMaterials -using OffsetArrays -using LinearAlgebra -using Test -using JLD2 -using Statistics -using Random - -SBMOF1 = Crystal("SBMOF-1.cif") -He = Molecule("He") -CO2 = Molecule("CO2") - -@testset "MCHelpers Tests" begin - box = SBMOF1.box - - ### - # random insertions - ### - insertion_inside_box = true # make sure insertions inside box - insertion_at_random_coords = true # make sure coords are different for each molecule - insertion_adds_molecule = true # make sure molecule vector increases in length - - molecules = Array{Molecule{Frac}}(undef, 0) - - m = deepcopy(He) - for i = 1:100 - random_insertion!(molecules, box, m) - if ! inside(molecules[i]) - insertion_inside_box = false - end - if ! (length(molecules) == i) - insertion_adds_molecule = false - end - if i > 1 - for j = 1:(i-1) - if isapprox(molecules[j], molecules[i]) - # by chance this could fail but highly unlikely! - insertion_at_random_coords = false - end - end - end - end - @test insertion_inside_box - @test insertion_at_random_coords - @test insertion_adds_molecule - - ### - # particle deletions - ### - molecule_24 = deepcopy(molecules[24]) - molecule_26 = deepcopy(molecules[26]) - remove_molecule!(25, molecules) - @test length(molecules) == 99 - @test isapprox(molecules[24], molecule_24) - @test isapprox(molecules[25], molecule_26) - remove_molecule!(24, molecules) - @test length(molecules) == 98 - @test isapprox(molecules[24], molecule_26) - - ### - # random translations - ### - adaptive_δ = AdaptiveTranslationStepSize(2.0) # default is 2 Å - - # first, test function to bring molecule inside a box. - box = Box(25.0, 25.0, 25.0, π/2, π/2, π/2) - molecule = deepcopy(He) - molecule = Frac(molecule, box) - translate_to!(molecule, Cart([26.0, -0.2, 12.]), box) - @test ! inside(molecule) - apply_periodic_boundary_condition!(molecule) - @test isapprox(box.f_to_c * molecule.com.xf, [1.0, 24.8, 12.0]) - @test isapprox(box.f_to_c * molecule.atoms.coords.xf[:, 1], [1.0, 24.8, 12.0]) - @test inside(molecule) - - molecule = deepcopy(CO2) - translate_to!(molecule, Cart([0.0, 0.0, 0.0])) - @test isapprox(molecule.atoms.coords, molecule.charges.coords) - box = Box(25.0, 5.0, 10.0, π/2, π/2, π/2) - molecule = Frac(molecule, box) - translate_to!(molecule, Cart([0.1, 7.0, -1.0]), box) - apply_periodic_boundary_condition!(molecule) - @test isapprox(molecule.com, Frac(Cart([0.1, 2.0, 9.0]), box)) - - translation_old_molecule_stored_properly = true - translation_coords_changed = true - translation_inside_box = true - molecules = Frac.([deepcopy(He), deepcopy(He)], box) - translate_to!(molecules[1], Frac([0.99, 0.99, 0.01])) - translate_to!(molecules[2], Cart(box.f_to_c * [0.99, 0.99, 0.01]), box) - old_molecule = random_translation!(molecules[1], box, adaptive_δ) - if ! isapprox(old_molecule, molecules[2]) # constructed to be identitical! - translation_old_molecule_stored_properly = false - end - if isapprox(molecules[1], molecules[2]) - translation_coords_changed = false - end - - for i = 1:100 - which_molecule = rand(1:2) # choose molecule to move - old_molecule_should_be = deepcopy(molecules[which_molecule]) - old_molecule = random_translation!(molecules[which_molecule], box, adaptive_δ) - if ! isapprox(old_molecule, old_molecule_should_be) - translation_coords_changed = false - end - if ! inside(molecules[which_molecule]) - translation_inside_box = false - end - end - @test translation_old_molecule_stored_properly - @test translation_coords_changed - @test translation_inside_box - - # now try for a molecule with both charges and atoms. - molecules = Molecule{Frac}[] - box = SBMOF1.box - m = Molecule("H2S") - @test needs_rotations(m) - for i = 1:10 - random_insertion!(molecules, box, m) - end - @test length(molecules) == 10 - other_molecules_untouched = true - translation_old_molecule_stored_properly = true - position_changed = true - translation_inside_box = true - charges_atoms_both_move_same = true - # many random translations... - for i = 1:5000 - which_molecule = rand(eachindex(molecules)) - old_molecules = deepcopy(molecules) - old_molecule = random_translation!(molecules[which_molecule], box, adaptive_δ) - # do atoms and charges move by same vector? - dx_a = molecules[which_molecule].atoms.coords.xf - old_molecules[which_molecule].atoms.coords.xf - dx_c = molecules[which_molecule].charges.coords.xf - old_molecules[which_molecule].charges.coords.xf - dx_c = dx_c .- dx_a[:, 1] - dx_a = dx_a .- dx_a[:, 1] - if ! (isapprox(dx_a, zeros(3, 3), atol=1e-8) && isapprox(dx_c, zeros(3, 3), atol=1e-8)) - charges_atoms_both_move_same = false - end - # is old molecule config stored properly? - if ! isapprox(old_molecule, old_molecules[which_molecule]) - translation_old_molecule_stored_properly = false - end - # have the coords changed? - if isapprox(old_molecule, molecules[which_molecule]) || isapprox( - old_molecule.com, molecules[which_molecule].com) || isapprox( - old_molecule.charges, molecules[which_molecule].charges) || isapprox( - old_molecule.atoms, molecules[which_molecule].atoms) - position_changed = false - end - # is it inside the box? have all other molecule been untouched? - apply_periodic_boundary_condition!(molecules[which_molecule]) - for j = eachindex(molecules) - if j == which_molecule - continue - end - if ! isapprox(molecules[j], old_molecules[j]) - other_molecules_untouched = false - end - end - for m in molecules - if ! inside(m) - translation_inside_box = false - end - end - end - @test other_molecules_untouched - @test translation_old_molecule_stored_properly - @test position_changed - @test translation_inside_box - @test charges_atoms_both_move_same - - ### - # random reinsertions - ### - box = Box(25.0, 25.0, 25.0, π/2, π/2, π/2) - molecules = Frac.([deepcopy(He), deepcopy(CO2), deepcopy(He), deepcopy(CO2)], box) - old_he = random_reinsertion!(molecules[1], box) - old_co2 = random_reinsertion!(molecules[2], box) - @test (inside(molecules[1]) && inside(molecules[2])) - @test isapprox(old_he, molecules[3]) - @test isapprox(old_co2, molecules[4]) - @test ! isapprox(molecules[1].com, molecules[3].com) - @test ! isapprox(molecules[2].com, molecules[4].com) - @test ! isapprox(molecules[2].charges.coords, molecules[4].charges.coords) - @test ! isapprox(molecules[2].atoms.coords, molecules[4].atoms.coords) -end -end +module MCHelpers_Test + +using PorousMaterials +using OffsetArrays +using LinearAlgebra +using Test +using JLD2 +using Statistics +using Random + +SBMOF1 = Crystal("SBMOF-1.cif") +He = Molecule("He") +CO2 = Molecule("CO2") + +@testset "MCHelpers Tests" begin + box = SBMOF1.box + + ### + # random insertions + ### + insertion_inside_box = true # make sure insertions inside box + insertion_at_random_coords = true # make sure coords are different for each molecule + insertion_adds_molecule = true # make sure molecule vector increases in length + + molecules = Array{Molecule{Frac}}(undef, 0) + + m = deepcopy(He) + for i in 1:100 + random_insertion!(molecules, box, m) + if !inside(molecules[i]) + insertion_inside_box = false + end + if !(length(molecules) == i) + insertion_adds_molecule = false + end + if i > 1 + for j in 1:(i - 1) + if isapprox(molecules[j], molecules[i]) + # by chance this could fail but highly unlikely! + insertion_at_random_coords = false + end + end + end + end + @test insertion_inside_box + @test insertion_at_random_coords + @test insertion_adds_molecule + + ### + # particle deletions + ### + molecule_24 = deepcopy(molecules[24]) + molecule_26 = deepcopy(molecules[26]) + remove_molecule!(25, molecules) + @test length(molecules) == 99 + @test isapprox(molecules[24], molecule_24) + @test isapprox(molecules[25], molecule_26) + remove_molecule!(24, molecules) + @test length(molecules) == 98 + @test isapprox(molecules[24], molecule_26) + + ### + # random translations + ### + adaptive_δ = AdaptiveTranslationStepSize(2.0) # default is 2 Å + + # first, test function to bring molecule inside a box. + box = Box(25.0, 25.0, 25.0, π / 2, π / 2, π / 2) + molecule = deepcopy(He) + molecule = Frac(molecule, box) + translate_to!(molecule, Cart([26.0, -0.2, 12.0]), box) + @test !inside(molecule) + apply_periodic_boundary_condition!(molecule) + @test isapprox(box.f_to_c * molecule.com.xf, [1.0, 24.8, 12.0]) + @test isapprox(box.f_to_c * molecule.atoms.coords.xf[:, 1], [1.0, 24.8, 12.0]) + @test inside(molecule) + + molecule = deepcopy(CO2) + translate_to!(molecule, Cart([0.0, 0.0, 0.0])) + @test isapprox(molecule.atoms.coords, molecule.charges.coords) + box = Box(25.0, 5.0, 10.0, π / 2, π / 2, π / 2) + molecule = Frac(molecule, box) + translate_to!(molecule, Cart([0.1, 7.0, -1.0]), box) + apply_periodic_boundary_condition!(molecule) + @test isapprox(molecule.com, Frac(Cart([0.1, 2.0, 9.0]), box)) + + translation_old_molecule_stored_properly = true + translation_coords_changed = true + translation_inside_box = true + molecules = Frac.([deepcopy(He), deepcopy(He)], box) + translate_to!(molecules[1], Frac([0.99, 0.99, 0.01])) + translate_to!(molecules[2], Cart(box.f_to_c * [0.99, 0.99, 0.01]), box) + old_molecule = random_translation!(molecules[1], box, adaptive_δ) + if !isapprox(old_molecule, molecules[2]) # constructed to be identitical! + translation_old_molecule_stored_properly = false + end + if isapprox(molecules[1], molecules[2]) + translation_coords_changed = false + end + + for i in 1:100 + which_molecule = rand(1:2) # choose molecule to move + old_molecule_should_be = deepcopy(molecules[which_molecule]) + old_molecule = random_translation!(molecules[which_molecule], box, adaptive_δ) + if !isapprox(old_molecule, old_molecule_should_be) + translation_coords_changed = false + end + if !inside(molecules[which_molecule]) + translation_inside_box = false + end + end + @test translation_old_molecule_stored_properly + @test translation_coords_changed + @test translation_inside_box + + # now try for a molecule with both charges and atoms. + molecules = Molecule{Frac}[] + box = SBMOF1.box + m = Molecule("H2S") + @test needs_rotations(m) + for i in 1:10 + random_insertion!(molecules, box, m) + end + @test length(molecules) == 10 + other_molecules_untouched = true + translation_old_molecule_stored_properly = true + position_changed = true + translation_inside_box = true + charges_atoms_both_move_same = true + # many random translations... + for i in 1:5000 + which_molecule = rand(eachindex(molecules)) + old_molecules = deepcopy(molecules) + old_molecule = random_translation!(molecules[which_molecule], box, adaptive_δ) + # do atoms and charges move by same vector? + dx_a = + molecules[which_molecule].atoms.coords.xf - + old_molecules[which_molecule].atoms.coords.xf + dx_c = + molecules[which_molecule].charges.coords.xf - + old_molecules[which_molecule].charges.coords.xf + dx_c = dx_c .- dx_a[:, 1] + dx_a = dx_a .- dx_a[:, 1] + if !( + isapprox(dx_a, zeros(3, 3); atol=1e-8) && + isapprox(dx_c, zeros(3, 3); atol=1e-8) + ) + charges_atoms_both_move_same = false + end + # is old molecule config stored properly? + if !isapprox(old_molecule, old_molecules[which_molecule]) + translation_old_molecule_stored_properly = false + end + # have the coords changed? + if isapprox(old_molecule, molecules[which_molecule]) || + isapprox(old_molecule.com, molecules[which_molecule].com) || + isapprox(old_molecule.charges, molecules[which_molecule].charges) || + isapprox(old_molecule.atoms, molecules[which_molecule].atoms) + position_changed = false + end + # is it inside the box? have all other molecule been untouched? + apply_periodic_boundary_condition!(molecules[which_molecule]) + for j in eachindex(molecules) + if j == which_molecule + continue + end + if !isapprox(molecules[j], old_molecules[j]) + other_molecules_untouched = false + end + end + for m in molecules + if !inside(m) + translation_inside_box = false + end + end + end + @test other_molecules_untouched + @test translation_old_molecule_stored_properly + @test position_changed + @test translation_inside_box + @test charges_atoms_both_move_same + + ### + # random reinsertions + ### + box = Box(25.0, 25.0, 25.0, π / 2, π / 2, π / 2) + molecules = Frac.([deepcopy(He), deepcopy(CO2), deepcopy(He), deepcopy(CO2)], box) + old_he = random_reinsertion!(molecules[1], box) + old_co2 = random_reinsertion!(molecules[2], box) + @test (inside(molecules[1]) && inside(molecules[2])) + @test isapprox(old_he, molecules[3]) + @test isapprox(old_co2, molecules[4]) + @test !isapprox(molecules[1].com, molecules[3].com) + @test !isapprox(molecules[2].com, molecules[4].com) + @test !isapprox(molecules[2].charges.coords, molecules[4].charges.coords) + @test !isapprox(molecules[2].atoms.coords, molecules[4].atoms.coords) +end +end diff --git a/test/misc.jl b/test/misc.jl index 350ebf351..6bba81e54 100644 --- a/test/misc.jl +++ b/test/misc.jl @@ -10,12 +10,13 @@ using Random @testset "Misc Tests" begin @test isapprox(rc[:atomic_masses][:H], 1.00794, atol=0.001) @test isapprox(rc[:atomic_masses][:Co], 58.9332, atol=0.001) - + test_xyz_filename = "atoms_test" - c = Cart([1.0 4.0; - 2.0 5.0; - 3.0 6.0] - ) + c = Cart([ + 1.0 4.0 + 2.0 5.0 + 3.0 6.0 + ]) s = [:C, :H] atoms = Atoms(s, c) write_xyz(atoms, test_xyz_filename) @@ -23,6 +24,6 @@ using Random @test isapprox(atoms, atoms_read) rm(test_xyz_filename * ".xyz") - @test rc[:cpk_colors][:Li] == (204,128,255) + @test rc[:cpk_colors][:Li] == (204, 128, 255) end end diff --git a/test/molecule.jl b/test/molecule.jl index 5e9bc746d..2cb1642df 100644 --- a/test/molecule.jl +++ b/test/molecule.jl @@ -1,330 +1,337 @@ -module Molecule_Test - -using PorousMaterials -using OffsetArrays -using LinearAlgebra -using Test -using JLD2 -using Statistics -using Random - -SBMOF1 = Crystal("SBMOF-1.cif") -CO2 = Molecule("CO2") -H2S = Molecule("H2S") -He = Molecule("He") - -function rand_point_on_unit_sphere() - u = randn(3) - u_norm = norm(u) - if u_norm < 1e-6 # avoid numerical error in division - return rand_point_on_unit_sphere() - end - return u / u_norm -end - -function pairwise_distances(coords::Frac, box::Box) - n = size(coords)[2] - pad = zeros(n, n) - for i = 1:n - for j = 1:n - pad[i, j] = distance(coords, box, i, j, false) - end - end - return pad -end - -function pairwise_distances(coords::Cart) - n = size(coords)[2] - pad = zeros(n, n) - box = unit_cube() - for i = 1:n - for j = 1:n - pad[i, j] = distance(coords, box, i, j, false) - end - end - return pad -end - -pairwise_distances(m::Molecule{Cart}) = [pairwise_distances(m.atoms.coords), pairwise_distances(m.charges.coords)] -pairwise_distances(m::Molecule{Frac}, box::Box) = [pairwise_distances(m.atoms.coords, box), pairwise_distances(m.charges.coords, box)] - -@testset "Molecules Tests" begin - ### - # molecule file reader - ### - molecule = deepcopy(CO2) - @test needs_rotations(molecule) - @test has_charges(molecule) - @test molecule.species == :CO2 - @test molecule.atoms.n == 3 - @test molecule.atoms.species[1] == :C_CO2 - @test molecule.atoms.species[2] == :O_CO2 - @test molecule.atoms.species[3] == :O_CO2 - @test all(molecule.atoms.coords.x[:,1] .≈ [0.0, 0.0, 0.0]) - @test all(molecule.atoms.coords.x[:,2] .≈ [-1.16, 0.0, 0.0]) - @test all(molecule.atoms.coords.x[:,3] .≈ [1.16, 0.0, 0.0]) - @test all(molecule.com.x .≈ [0.0, 0.0, 0.0]) - @test molecule.charges.n == 3 - @test molecule.charges.q[1] ≈ 0.7 - @test molecule.charges.q[2] ≈ -0.35 - @test molecule.charges.q[3] ≈ -0.35 - for i = 1:3 - @test all(molecule.charges.coords.x[i] ≈ molecule.atoms.coords.x[i]) - end - - ### - # extremely basic: make sure these functions change the molecule and don't screw up bond distances - ### - m = deepcopy(CO2) - pad = pairwise_distances(m) - dx = 4 * randn(3) - translate_by!(m, Cart(dx)) - @test ! isapprox(m.atoms, CO2.atoms) - @test ! isapprox(m.charges, CO2.charges) - @test ! isapprox(m.com, CO2.com) - @test isapprox(pad, pairwise_distances(m)) - translate_by!(m, Cart(-1 * dx)) - @test isapprox(m, CO2) - - m = deepcopy(CO2) - x_new_com = 4 * randn(3) - new_com = Cart(x_new_com) - translate_to!(m, new_com) - @test ! isapprox(m.atoms, CO2.atoms) - @test ! isapprox(m.charges, CO2.charges) - @test ! isapprox(m.com, CO2.com) - @test isapprox(m.com, new_com) - @test isapprox(pad, pairwise_distances(m)) - - m = deepcopy(CO2) - random_rotation!(m) - @test ! isapprox(m.atoms, CO2.atoms) - @test ! isapprox(m.charges, CO2.charges) - @test isapprox(m.com, CO2.com) - @test isapprox(pad, pairwise_distances(m)) - - # ... in frac coords - m = deepcopy(H2S) - pad = pairwise_distances(m) - box = SBMOF1.box - m = Frac(m, box) - for i = 1:200 - translate_by!(m, Frac([randn(), randn(), randn()])) - translate_by!(m, Cart([randn(), randn(), randn()]), box) - translate_to!(m, Frac([randn(), randn(), randn()])) - translate_to!(m, Cart([randn(), randn(), randn()]), box) - random_rotation!(m, box) - end - pad_after = pairwise_distances(m, box) - @test isapprox(pad, pad_after) - - ### - # Frac to Cart converter - ### - m = CO2 - box = SBMOF1.box - m = Frac(m, box) - m = Cart(m, box) - @test isapprox(m, CO2) # should restore. - - box = unit_cube() - m = CO2 - m = Frac(m, box) - @test isapprox(Cart(m, box), CO2) - - ### - # make sure translate by/to preserve orientation - ### - # ... in frac coords - m = deepcopy(CO2) - m = Frac(m, box) - for i = 1:10 - translate_by!(m, Frac([randn(), randn(), randn()])) - translate_by!(m, Cart([randn(), randn(), randn()]), box) - translate_to!(m, Frac([randn(), randn(), randn()])) - translate_to!(m, Cart([randn(), randn(), randn()]), box) - end - fresh_m = deepcopy(CO2) - m = Cart(m, box) - translate_to!(fresh_m, m.com) - @test isapprox(m, fresh_m) # should restore and preserve orientation - - # ... in cart coords - m = deepcopy(CO2) - for i = 1:10 - translate_by!(m, Cart([randn(), randn(), randn()])) - translate_by!(m, Frac([randn(), randn(), randn()]), box) - translate_to!(m, Cart([randn(), randn(), randn()])) - translate_to!(m, Frac([randn(), randn(), randn()]), box) - end - fresh_m = deepcopy(CO2) - translate_to!(fresh_m, m.com) - @test isapprox(m, fresh_m) # should restore and preserve orientation - - # test translate_to, translate_by - box = SBMOF1.box - m1 = deepcopy(H2S) - m2 = deepcopy(H2S) - m1 = Frac(m1, box) - m2 = Frac(m2, box) - @test isapprox(m1, m2) - translate_by!(m2, Frac([0.0, 0.0, 0.0])) - @test isapprox(m1, m2) - translate_by!(m2, Frac([0.0, 1.2, 0.0])) - @test ! isapprox(m1, m2) - translate_to!(m2, m1.com) - @test isapprox(m1, m2) - translate_to!(m2, Cart([50.0, 100.0, 150.0]), box) - @test isapprox(box.f_to_c * m2.com.xf, [50.0, 100.0, 150.0]) - - translate_to!(m1, Frac([0.1, 0.2, 1.4])) - translate_to!(m2, Cart(box.f_to_c * [0.1, 0.2, 1.4]), box) - @test isapprox(m1, m2) - - translate_by!(m1, Frac([-0.1, -0.2, -1.1])) - translate_by!(m2, Cart(box.f_to_c * [-0.1, -0.2, -1.1]), box) - @test isapprox(m1, m2) - random_rotation!(m2, box) - random_rotation!(m1, box) - - ### - # make sure random rotations preserve bond distances. - ### - # ... rotations in cartesian space - m = deepcopy(H2S) - com = Cart(10.0 * randn(3)) - translate_to!(m, com) - pad = pairwise_distances(m) - for i = 1:1000 - random_rotation!(m) - end - @test isapprox(pad, pairwise_distances(m)) - @test isapprox(m.com, com, atol=1e-12) # com should not change - - # ... rotations in frac space - m = deepcopy(H2S) - pad = pairwise_distances(m) - box = SBMOF1.box - m = Frac(m, box) - com = Frac(10.0 * randn(3)) - translate_to!(m, com) - for i = 1:1000 - random_rotation!(m, box) - end - @test isapprox(pad, pairwise_distances(m, box)) - @test isapprox(m.com, com, atol=1e-12) - - # test unit vector on sphere generator - ms = [He for i = 1:10000] - for m in ms - translate_to!(m, Cart(rand_point_on_unit_sphere())) - end - @test all(isapprox.([norm(m.atoms.coords.x[:, 1]) for m in ms], 1.0)) - write_xyz(ms, "random_vectors_on_sphere") - println("See random_vectors_on_sphere") - - # Test to see if random_rotation_matrix() is random and uniform on sphere surface - N = 1000000 - points = Array{Float64, 2}(undef, 3, N) - for i = 1:N - points[:, i] = random_rotation_matrix() * [0., 0., 1.] - end - - for i = 1:3 - r = rand() - count = zeros(10) - for j = 1:10 - for k = 1:N - if points[1, k] > 0 && points[2, k] ^ 2 + points[3, k] ^ 2 <= r ^ 2 - count[j] += 1 - end - end - points = random_rotation_matrix() * points - end - @test (maximum(count) - minimum(count)) / N < 0.01 - end - - ### - # random rotation should place H in each half-quadrant equal amount of time... - ### - m = deepcopy(H2S) - translate_to!(m, origin(Cart)) - @assert m.atoms.species[2] == :H_H2S - N = 10000000 - up = 0 # H in upper half-sphere - left = 0 # H in left half-sphere - back = 0 # H in back half-sphere - for i = 1:N - random_rotation!(m) - if m.atoms.coords.x[3, 2] > 0.0 - up += 1 - end - if m.atoms.coords.x[2, 2] < 0.0 - back += 1 - end - if m.atoms.coords.x[1, 2] < 0.0 - left += 1 - end - end - @test isapprox(up / N, 0.5, atol=0.01) - @test isapprox(left / N, 0.5, atol=0.01) - @test isapprox(back / N, 0.5, atol=0.01) - - ### - # rotation matrix should be orthogonal - ### - r_orthogonal = true - r_det_1 = true - for i = 1:300 - r = random_rotation_matrix() - if ! isapprox(r * transpose(r), Matrix{Float64}(I, 3, 3)) - r_orthogonal = false - end - if ! isapprox(det(r), 1.0) - r_det_1 = false - end - end - @test r_orthogonal - @test r_det_1 - - # visually inspection that rotations are random - ms = [deepcopy(CO2) for i = 1:1000] - for m in ms - random_rotation!(m) - end - write_xyz(ms, "co2s") - @info "see co2s.xyz for dist'n of rotations" - - @test ! needs_rotations(Molecule("Xe")) - - # frac and cart - m = H2S - box = Box(2.0, 4.0, 8.0) - m_f = Frac(m, box) - @test isapprox(m_f.atoms.coords.xf[1, :], m.atoms.coords.x[1, :] / 2.0) - @test isapprox(m_f.atoms.coords.xf[2, :], m.atoms.coords.x[2, :] / 4.0) - @test isapprox(m_f.charges.coords.xf[1, :], m.charges.coords.x[1, :] / 2.0) - @test isapprox(m_f.charges.coords.xf[2, :], m.charges.coords.x[2, :] / 4.0) - m_f_c = Cart(m_f, box) - @test isapprox(m_f_c.charges, m.charges) - @test isapprox(m_f_c.atoms, m.atoms) - - # distorted - adaptive_δ = AdaptiveTranslationStepSize(2.0) # default is 2 Å - box = SBMOF1.box - m = Frac(H2S, box) - m_ref = Frac(H2S, box) - random_translation!(m, box, adaptive_δ) - random_translation!(m_ref, box, adaptive_δ) - random_rotation!(m, box) - random_rotation!(m_ref, box) - @test ! PorousMaterials.distortion(m, m_ref, box) - m.atoms.coords.xf[:, 1] += randn(3) - @test PorousMaterials.distortion(m, m_ref, box) - - # "center of mass" for massless molecules - rc[:atomic_masses][:null] = 0.0 - molecule = Molecule("com_test") - @test isapprox(molecule.com, Cart([0.;0.;0.])) -end -end +module Molecule_Test + +using PorousMaterials +using OffsetArrays +using LinearAlgebra +using Test +using JLD2 +using Statistics +using Random + +SBMOF1 = Crystal("SBMOF-1.cif") +CO2 = Molecule("CO2") +H2S = Molecule("H2S") +He = Molecule("He") + +function rand_point_on_unit_sphere() + u = randn(3) + u_norm = norm(u) + if u_norm < 1e-6 # avoid numerical error in division + return rand_point_on_unit_sphere() + end + return u / u_norm +end + +function pairwise_distances(coords::Frac, box::Box) + n = size(coords)[2] + pad = zeros(n, n) + for i in 1:n + for j in 1:n + pad[i, j] = distance(coords, box, i, j, false) + end + end + return pad +end + +function pairwise_distances(coords::Cart) + n = size(coords)[2] + pad = zeros(n, n) + box = unit_cube() + for i in 1:n + for j in 1:n + pad[i, j] = distance(coords, box, i, j, false) + end + end + return pad +end + +function pairwise_distances(m::Molecule{Cart}) + return [pairwise_distances(m.atoms.coords), pairwise_distances(m.charges.coords)] +end +function pairwise_distances(m::Molecule{Frac}, box::Box) + return [ + pairwise_distances(m.atoms.coords, box), + pairwise_distances(m.charges.coords, box) + ] +end + +@testset "Molecules Tests" begin + ### + # molecule file reader + ### + molecule = deepcopy(CO2) + @test needs_rotations(molecule) + @test has_charges(molecule) + @test molecule.species == :CO2 + @test molecule.atoms.n == 3 + @test molecule.atoms.species[1] == :C_CO2 + @test molecule.atoms.species[2] == :O_CO2 + @test molecule.atoms.species[3] == :O_CO2 + @test all(molecule.atoms.coords.x[:, 1] .≈ [0.0, 0.0, 0.0]) + @test all(molecule.atoms.coords.x[:, 2] .≈ [-1.16, 0.0, 0.0]) + @test all(molecule.atoms.coords.x[:, 3] .≈ [1.16, 0.0, 0.0]) + @test all(molecule.com.x .≈ [0.0, 0.0, 0.0]) + @test molecule.charges.n == 3 + @test molecule.charges.q[1] ≈ 0.7 + @test molecule.charges.q[2] ≈ -0.35 + @test molecule.charges.q[3] ≈ -0.35 + for i in 1:3 + @test all(molecule.charges.coords.x[i] ≈ molecule.atoms.coords.x[i]) + end + + ### + # extremely basic: make sure these functions change the molecule and don't screw up bond distances + ### + m = deepcopy(CO2) + pad = pairwise_distances(m) + dx = 4 * randn(3) + translate_by!(m, Cart(dx)) + @test !isapprox(m.atoms, CO2.atoms) + @test !isapprox(m.charges, CO2.charges) + @test !isapprox(m.com, CO2.com) + @test isapprox(pad, pairwise_distances(m)) + translate_by!(m, Cart(-1 * dx)) + @test isapprox(m, CO2) + + m = deepcopy(CO2) + x_new_com = 4 * randn(3) + new_com = Cart(x_new_com) + translate_to!(m, new_com) + @test !isapprox(m.atoms, CO2.atoms) + @test !isapprox(m.charges, CO2.charges) + @test !isapprox(m.com, CO2.com) + @test isapprox(m.com, new_com) + @test isapprox(pad, pairwise_distances(m)) + + m = deepcopy(CO2) + random_rotation!(m) + @test !isapprox(m.atoms, CO2.atoms) + @test !isapprox(m.charges, CO2.charges) + @test isapprox(m.com, CO2.com) + @test isapprox(pad, pairwise_distances(m)) + + # ... in frac coords + m = deepcopy(H2S) + pad = pairwise_distances(m) + box = SBMOF1.box + m = Frac(m, box) + for i in 1:200 + translate_by!(m, Frac([randn(), randn(), randn()])) + translate_by!(m, Cart([randn(), randn(), randn()]), box) + translate_to!(m, Frac([randn(), randn(), randn()])) + translate_to!(m, Cart([randn(), randn(), randn()]), box) + random_rotation!(m, box) + end + pad_after = pairwise_distances(m, box) + @test isapprox(pad, pad_after) + + ### + # Frac to Cart converter + ### + m = CO2 + box = SBMOF1.box + m = Frac(m, box) + m = Cart(m, box) + @test isapprox(m, CO2) # should restore. + + box = unit_cube() + m = CO2 + m = Frac(m, box) + @test isapprox(Cart(m, box), CO2) + + ### + # make sure translate by/to preserve orientation + ### + # ... in frac coords + m = deepcopy(CO2) + m = Frac(m, box) + for i in 1:10 + translate_by!(m, Frac([randn(), randn(), randn()])) + translate_by!(m, Cart([randn(), randn(), randn()]), box) + translate_to!(m, Frac([randn(), randn(), randn()])) + translate_to!(m, Cart([randn(), randn(), randn()]), box) + end + fresh_m = deepcopy(CO2) + m = Cart(m, box) + translate_to!(fresh_m, m.com) + @test isapprox(m, fresh_m) # should restore and preserve orientation + + # ... in cart coords + m = deepcopy(CO2) + for i in 1:10 + translate_by!(m, Cart([randn(), randn(), randn()])) + translate_by!(m, Frac([randn(), randn(), randn()]), box) + translate_to!(m, Cart([randn(), randn(), randn()])) + translate_to!(m, Frac([randn(), randn(), randn()]), box) + end + fresh_m = deepcopy(CO2) + translate_to!(fresh_m, m.com) + @test isapprox(m, fresh_m) # should restore and preserve orientation + + # test translate_to, translate_by + box = SBMOF1.box + m1 = deepcopy(H2S) + m2 = deepcopy(H2S) + m1 = Frac(m1, box) + m2 = Frac(m2, box) + @test isapprox(m1, m2) + translate_by!(m2, Frac([0.0, 0.0, 0.0])) + @test isapprox(m1, m2) + translate_by!(m2, Frac([0.0, 1.2, 0.0])) + @test !isapprox(m1, m2) + translate_to!(m2, m1.com) + @test isapprox(m1, m2) + translate_to!(m2, Cart([50.0, 100.0, 150.0]), box) + @test isapprox(box.f_to_c * m2.com.xf, [50.0, 100.0, 150.0]) + + translate_to!(m1, Frac([0.1, 0.2, 1.4])) + translate_to!(m2, Cart(box.f_to_c * [0.1, 0.2, 1.4]), box) + @test isapprox(m1, m2) + + translate_by!(m1, Frac([-0.1, -0.2, -1.1])) + translate_by!(m2, Cart(box.f_to_c * [-0.1, -0.2, -1.1]), box) + @test isapprox(m1, m2) + random_rotation!(m2, box) + random_rotation!(m1, box) + + ### + # make sure random rotations preserve bond distances. + ### + # ... rotations in cartesian space + m = deepcopy(H2S) + com = Cart(10.0 * randn(3)) + translate_to!(m, com) + pad = pairwise_distances(m) + for i in 1:1000 + random_rotation!(m) + end + @test isapprox(pad, pairwise_distances(m)) + @test isapprox(m.com, com, atol=1e-12) # com should not change + + # ... rotations in frac space + m = deepcopy(H2S) + pad = pairwise_distances(m) + box = SBMOF1.box + m = Frac(m, box) + com = Frac(10.0 * randn(3)) + translate_to!(m, com) + for i in 1:1000 + random_rotation!(m, box) + end + @test isapprox(pad, pairwise_distances(m, box)) + @test isapprox(m.com, com, atol=1e-12) + + # test unit vector on sphere generator + ms = [He for i in 1:10000] + for m in ms + translate_to!(m, Cart(rand_point_on_unit_sphere())) + end + @test all(isapprox.([norm(m.atoms.coords.x[:, 1]) for m in ms], 1.0)) + write_xyz(ms, "random_vectors_on_sphere") + println("See random_vectors_on_sphere") + + # Test to see if random_rotation_matrix() is random and uniform on sphere surface + N = 1000000 + points = Array{Float64, 2}(undef, 3, N) + for i in 1:N + points[:, i] = random_rotation_matrix() * [0.0, 0.0, 1.0] + end + + for i in 1:3 + r = rand() + count = zeros(10) + for j in 1:10 + for k in 1:N + if points[1, k] > 0 && points[2, k]^2 + points[3, k]^2 <= r^2 + count[j] += 1 + end + end + points = random_rotation_matrix() * points + end + @test (maximum(count) - minimum(count)) / N < 0.01 + end + + ### + # random rotation should place H in each half-quadrant equal amount of time... + ### + m = deepcopy(H2S) + translate_to!(m, origin(Cart)) + @assert m.atoms.species[2] == :H_H2S + N = 10000000 + up = 0 # H in upper half-sphere + left = 0 # H in left half-sphere + back = 0 # H in back half-sphere + for i in 1:N + random_rotation!(m) + if m.atoms.coords.x[3, 2] > 0.0 + up += 1 + end + if m.atoms.coords.x[2, 2] < 0.0 + back += 1 + end + if m.atoms.coords.x[1, 2] < 0.0 + left += 1 + end + end + @test isapprox(up / N, 0.5, atol=0.01) + @test isapprox(left / N, 0.5, atol=0.01) + @test isapprox(back / N, 0.5, atol=0.01) + + ### + # rotation matrix should be orthogonal + ### + r_orthogonal = true + r_det_1 = true + for i in 1:300 + r = random_rotation_matrix() + if !isapprox(r * transpose(r), Matrix{Float64}(I, 3, 3)) + r_orthogonal = false + end + if !isapprox(det(r), 1.0) + r_det_1 = false + end + end + @test r_orthogonal + @test r_det_1 + + # visually inspection that rotations are random + ms = [deepcopy(CO2) for i in 1:1000] + for m in ms + random_rotation!(m) + end + write_xyz(ms, "co2s") + @info "see co2s.xyz for dist'n of rotations" + + @test !needs_rotations(Molecule("Xe")) + + # frac and cart + m = H2S + box = Box(2.0, 4.0, 8.0) + m_f = Frac(m, box) + @test isapprox(m_f.atoms.coords.xf[1, :], m.atoms.coords.x[1, :] / 2.0) + @test isapprox(m_f.atoms.coords.xf[2, :], m.atoms.coords.x[2, :] / 4.0) + @test isapprox(m_f.charges.coords.xf[1, :], m.charges.coords.x[1, :] / 2.0) + @test isapprox(m_f.charges.coords.xf[2, :], m.charges.coords.x[2, :] / 4.0) + m_f_c = Cart(m_f, box) + @test isapprox(m_f_c.charges, m.charges) + @test isapprox(m_f_c.atoms, m.atoms) + + # distorted + adaptive_δ = AdaptiveTranslationStepSize(2.0) # default is 2 Å + box = SBMOF1.box + m = Frac(H2S, box) + m_ref = Frac(H2S, box) + random_translation!(m, box, adaptive_δ) + random_translation!(m_ref, box, adaptive_δ) + random_rotation!(m, box) + random_rotation!(m_ref, box) + @test !PorousMaterials.distortion(m, m_ref, box) + m.atoms.coords.xf[:, 1] += randn(3) + @test PorousMaterials.distortion(m, m_ref, box) + + # "center of mass" for massless molecules + rc[:atomic_masses][:null] = 0.0 + molecule = Molecule("com_test") + @test isapprox(molecule.com, Cart([0.0; 0.0; 0.0])) +end +end diff --git a/test/runtests.jl b/test/runtests.jl index a37cab0b8..6a7cdb837 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -9,13 +9,13 @@ testfiles = [ "electrostatics.jl", "mc_helpers.jl", "eos.jl" - ] +] using Test, Documenter, PorousMaterials PorousMaterials.banner() -for testfile ∈ testfiles +for testfile in testfiles @info "Running test/$testfile" @time include(testfile) end diff --git a/test/vdw_energetics.jl b/test/vdw_energetics.jl index 4bc920897..645d35ddf 100644 --- a/test/vdw_energetics.jl +++ b/test/vdw_energetics.jl @@ -1,229 +1,244 @@ -module Vdw_Energetics_Test - -using PorousMaterials -using OffsetArrays -using LinearAlgebra -using Test -using JLD2 -using Statistics -using Random - -Xe = Molecule("Xe") -X = Molecule("X") -CO2 = Molecule("CO2") -He = Molecule("He") - -@testset "VdwEnergetics Tests" begin - # lennard jones function - σ² = 1.0 - ϵ = 3.0 - @test isapprox(PorousMaterials.lennard_jones(σ², σ², ϵ), 0.0) - @test isapprox(PorousMaterials.lennard_jones(2 ^ (2/6) * σ², σ², ϵ), -1.0 * ϵ) - - ### - # Xe in SBMOF-1 tests, comparing to RASPA - ### - ljff = LJForceField("Dreiding", r_cutoff=12.5, mixing_rules="Lorentz-Berthelot") - xtal = Crystal("SBMOF-1.cif") - rep_factors = replication_factors(xtal.box, ljff) - xtal = replicate(xtal, rep_factors) - mol = Xe - mol = Frac(mol, xtal.box) - @test ! has_charges(mol) - # point #1 - translate_to!(mol, origin(Frac)) - energy = vdw_energy(xtal, mol, ljff) - @test isapprox(energy, -5041.58, atol = 0.005) - # point #2 - translate_to!(mol, Frac(Cart([0.494265, 2.22668, 0.450354]), xtal.box)) - # xenon.atoms.xf[:, 1] = sbmof1.box.c_to_f * [0.494265, 2.22668, 0.450354] - energy = vdw_energy(xtal, mol, ljff) - @test isapprox(energy, 12945.838, atol = 0.005) - - random_rotation!(mol, xtal.box) - energy = vdw_energy(xtal, mol, ljff) - mol = Cart(mol, xtal.box) - @test isapprox(energy, vdw_energy(xtal, mol, ljff)) - - ### - # NIST data to test LJ potentials - # data from here: https://www.nist.gov/mml/csd/chemical-informatics-research-group/lennard-jones-fluid-reference-calculations - # created bogus atom X for this purpose. - ### - ljff = LJForceField("NIST", r_cutoff=3.0) - energies_should_be = [-4.3515E+03, -6.9000E+02, -1.1467E+03, -1.6790E+01] - for c = 1:4 # four configurations - # read in positions of atoms provided by NIST ("X" atoms) - posfile = open("nist/lennardjones/lj_sample_config_periodic$c.txt") - lines = readlines(posfile) - # first line is dims of unit cell box - dims = parse.(Float64, split(lines[1])) - box = Box(dims..., π/2, π/2, π/2) - # second line is # of molecules - n = parse(Int, lines[2]) - - # read in molecule positions, construct them - ms = Molecule[] - for i = 1:n - xyz = split(lines[2+i])[2:end] - x = parse.(Float64, xyz) - m = deepcopy(X) - m = Frac(m, box) - translate_to!(m, Frac(box.c_to_f * x)) - push!(ms, m) - end - close(posfile) - - # compute energy of the configuration - energy = PorousMaterials.total_vdw_energy(ms, ljff, box) - @test isapprox(energy, energies_should_be[c], atol=0.1) - end - # test vdw_energy_no_PBC, which is the vdw_energy function when no PBCs are applied. - # The following "framework" is a cage floating in space so no atoms are near the boundary - # of the unit cell box. So with cutoff should get same with or without PBCs. - box = Box(100.0, 100.0, 100.0, π/2, π/2, π/2) - co2 = deepcopy(CO2) - translate_to!(co2, Cart([50.0, 50.0, 50.0])) - f = Crystal("cage_in_space.cif") # same cage, but shifted to [50, 50, 50] in unit cell box 100 by 100 by 100. - ljff = LJForceField("UFF") - # energy with PBC but padded so effetive periodic interactions are zero, bc beyond cutoff - energy = vdw_energy(f, co2, ljff) - atoms = read_xyz("data/crystals/CB5.xyz") # raw .xyz of cage - @test isapprox(energy, vdw_energy_no_PBC(atoms, CO2.atoms, ljff)) - - ### - # guest-guest energetics - ### - ljff = LJForceField("Dreiding", r_cutoff=12.5) - box = Box(25.0, 25.0, 25.0, π/2, π/2, π/2) - # a He and Xe a distance of 6.0 away - xe = Frac(Xe, box) - he = Frac(He, box) - translate_to!(xe, Cart([5.0, 12.0, 12.0]), box) - translate_to!(he, Cart([11.0, 12.0, 12.0]), box) - molecules = [xe, he] - r² = (11.0 - 5.0) ^ 2 # duh - energy = lennard_jones(r², ljff.σ²[:Xe][:He], ljff.ϵ[:Xe][:He]) - @test energy ≈ vdw_energy(1, molecules, ljff, box) - @test energy ≈ vdw_energy(2, molecules, ljff, box) # symmetry - - # via PBC, a distance (24.0 - 5.0) > (1+5) - translate_to!(molecules[2], Cart([24.0, 12.0, 12.0]), box) - r² = (1.0 + 5.0) ^ 2 # PBC - energy = lennard_jones(r², ljff.σ²[:Xe][:He], ljff.ϵ[:He][:Xe]) - @test energy ≈ vdw_energy(2, molecules, ljff, box) - @test energy ≈ vdw_energy(1, molecules, ljff, box) # symmetry again. - - # put a molecule on top of first one. - push!(molecules, deepcopy(molecules[1])) - @test vdw_energy(2, molecules, ljff, box) ≈ 2 * energy - - @test vdw_energy(1, molecules, ljff, box) == Inf - @test vdw_energy(3, molecules, ljff, box) == Inf - - # interaction energy between first and second should be same via PBC - molecules_a = Frac.([Xe, He], box) - translate_to!(molecules_a[1], Cart([11.0, 1.0, 12.0]), box) - translate_to!(molecules_a[2], Cart([11.0, 4.0, 12.0]), box) - molecules_b = Frac.([Xe, He], box) - translate_to!(molecules_b[1], Cart([11.0, 1.0, 12.0]), box) - translate_to!(molecules_b[2], Cart([11.0, 23.0, 12.0]), box) - @test vdw_energy(1, molecules_a, ljff, box) ≈ vdw_energy(1, molecules_b, ljff, box) - - # another PBC one where three coords are different. - molecules = Frac.([Xe, He], box) - translate_to!(molecules[1], Cart([24.0, 23.0, 11.0]), box) - translate_to!(molecules[2], Cart([22.0, 2.0, 12.0]), box) - r² = 4.0^2 + 2.0^2 + 1.0^2 - energy = lennard_jones(r², ljff.σ²[:He][:Xe], ljff.ϵ[:He][:Xe]) - @test vdw_energy(1, molecules, ljff, box) ≈ energy - @test vdw_energy(2, molecules, ljff, box) ≈ energy - - # test cutoff radius. molecules here are too far to interact - translate_to!(molecules[1], Cart([0.0, 0.0, 0.0]), box) - translate_to!(molecules[2], Cart([12.0, 12.0, 12.0]), box) - @test vdw_energy(1, molecules, ljff, box) ≈ 0.0 - @test vdw_energy(2, molecules, ljff, box) ≈ 0.0 - # the position of a molecule should not change inside vdw_energy. - @test all(molecules[1].atoms.coords.xf[:, 1] .== box.c_to_f * [0.0, 0.0, 0.0]) - @test all(molecules[2].atoms.coords.xf[:, 1] .== box.c_to_f * [12.0, 12.0, 12.0]) - # TODO write tests for CO2 where there are more than one beads - - # Molecules with more than one ljsphere - - # two CO2 molecules 6.0 units apart - molecules_co2 = Frac.([deepcopy(CO2), deepcopy(CO2)], box) - translate_to!(molecules_co2[1], Cart([12.0, 9.0, 12.0]), box) - translate_to!(molecules_co2[2], Cart([12.0, 15.0, 12.0]), box) - # because the molecules have not been rotated, all corresponding beads are same - # distance apart when they are separated along the y-axis - r²_com = (15.0 - 9.0)^2 - # distance between teh central carbon and an oxygen in one molecule this - # takes advantage of the fact that the carbon is the central atom, and that - # all three atoms are in a line - r²_co = 1.16^2 - # distance between the two oxygens in one molecule - r²_oo = (2.0 * 1.16)^2 - energy = (2.0 * lennard_jones(r²_com, ljff.σ²[:O_CO2][:O_CO2], ljff.ϵ[:O_CO2][:O_CO2]) - + 4.0 * lennard_jones(r²_com + r²_co, ljff.σ²[:O_CO2][:C_CO2], ljff.ϵ[:O_CO2][:C_CO2]) - + 2.0 * lennard_jones(r²_com + r²_oo, ljff.σ²[:O_CO2][:O_CO2], ljff.ϵ[:O_CO2][:O_CO2]) - + lennard_jones(r²_com, ljff.σ²[:C_CO2][:C_CO2], ljff.ϵ[:C_CO2][:C_CO2])) - @test vdw_energy(1, molecules_co2, ljff, box) ≈ energy - @test vdw_energy(2, molecules_co2, ljff, box) ≈ energy - - # PBC placing one at 2.0 and the other at 21.0 - translate_to!(molecules_co2[1], Cart([12.0, 2.0, 12.0]), box) - translate_to!(molecules_co2[2], Cart([12.0, 21.0, 12.0]), box) - r²_com = (4.0 + 2.0)^2 - energy = (2.0 * lennard_jones(r²_com, ljff.σ²[:O_CO2][:O_CO2], ljff.ϵ[:O_CO2][:O_CO2]) - + 4.0 * lennard_jones(r²_com + r²_co, ljff.σ²[:O_CO2][:C_CO2], ljff.ϵ[:O_CO2][:C_CO2]) - + 2.0 * lennard_jones(r²_com + r²_oo, ljff.σ²[:O_CO2][:O_CO2], ljff.ϵ[:O_CO2][:O_CO2]) - + lennard_jones(r²_com, ljff.σ²[:C_CO2][:C_CO2], ljff.ϵ[:C_CO2][:C_CO2])) - @test vdw_energy(1, molecules_co2, ljff, box) ≈ energy - @test vdw_energy(2, molecules_co2, ljff, box) ≈ energy - - # testing cutoff radius, so only one oxygen from each will be able to interact - # making a larger box so that only a few.atoms from each CO2 will be able to interact - box_large = Box(50.0, 50.0, 50.0, π/2, π/2, π/2) - molecules_co2 = Frac.([deepcopy(CO2), deepcopy(CO2)], box_large) - # placed 12.6 units apart so the C atoms will be outside the cutoff radius, - # but one O atom from each will be inside, so these will interact - translate_to!(molecules_co2[1], Cart([0.0, 0.0, 0.0]), box_large) - translate_to!(molecules_co2[2], Cart([13.0, 0.0, 0.0]), box_large) - r²_com = (13.0)^2 - r²_o = (13.0 - (2.0 * 1.16))^2 - r²_co = (13.0 - 1.16)^2 - energy = (lennard_jones(r²_o, ljff.σ²[:O_CO2][:O_CO2], ljff.ϵ[:O_CO2][:O_CO2]) - + 2 * lennard_jones(r²_co, ljff.σ²[:O_CO2][:C_CO2], ljff.ϵ[:O_CO2][:C_CO2])) - @test vdw_energy(1, molecules_co2, ljff, box_large) ≈ energy - @test vdw_energy(2, molecules_co2, ljff, box_large) ≈ energy - - - ### - # Mixture Tests - ### - # test the vdW guest-guest interation between two species - # set up the system - molecules = [[Molecule("Xe"), Molecule("Xe")], [Molecule("Kr")]] - ljff = LJForceField("UFF", r_cutoff=6.0) - box = Box(10.0, 10.0, 10.0) - # convert molecules array to fractional using this box. - molecules = [Frac.(mols, box) for mols in molecules] - # position the molecules - translate_to!(molecules[1][1], Frac([0.1, 0.1, 0.1])) - translate_to!(molecules[1][2], Frac([0.1, 0.5, 0.1])) - translate_to!(molecules[2][1], Frac([0.6, 0.1, 0.1])) - # calculate vdW_energy interaction - r12_sqr = 4.0 ^ 2 - r13_sqr = 5.0 ^ 2 - energy_12 = 4.0 * ljff.ϵ[:Xe][:Xe] * ((ljff.σ²[:Xe][:Xe] / r12_sqr) ^ 6 - (ljff.σ²[:Xe][:Xe] / r12_sqr) ^ 3) - energy_13 = 4.0 * ljff.ϵ[:Xe][:Kr] * ((ljff.σ²[:Xe][:Kr] / r13_sqr) ^ 6 - (ljff.σ²[:Xe][:Kr] / r13_sqr) ^ 3) - @test (energy_12 + energy_13) ≈ vdw_energy(1, 1, molecules, ljff, box) - - # test for edge case (use ljff and box from previous test) - molecules = [[Molecule("Xe")], Molecule[]] - molecules = [Frac.(mols, box) for mols in molecules] - translate_to!(molecules[1][1], Frac([0.1, 0.1, 0.1])) - @test 0.0 == vdw_energy(1, 1, molecules, ljff, box) -end -end +module Vdw_Energetics_Test + +using PorousMaterials +using OffsetArrays +using LinearAlgebra +using Test +using JLD2 +using Statistics +using Random + +Xe = Molecule("Xe") +X = Molecule("X") +CO2 = Molecule("CO2") +He = Molecule("He") + +@testset "VdwEnergetics Tests" begin + # lennard jones function + σ² = 1.0 + ϵ = 3.0 + @test isapprox(PorousMaterials.lennard_jones(σ², σ², ϵ), 0.0) + @test isapprox(PorousMaterials.lennard_jones(2^(2 / 6) * σ², σ², ϵ), -1.0 * ϵ) + + ### + # Xe in SBMOF-1 tests, comparing to RASPA + ### + ljff = LJForceField("Dreiding"; r_cutoff=12.5, mixing_rules="Lorentz-Berthelot") + xtal = Crystal("SBMOF-1.cif") + rep_factors = replication_factors(xtal.box, ljff) + xtal = replicate(xtal, rep_factors) + mol = Xe + mol = Frac(mol, xtal.box) + @test !has_charges(mol) + # point #1 + translate_to!(mol, origin(Frac)) + energy = vdw_energy(xtal, mol, ljff) + @test isapprox(energy, -5041.58, atol=0.005) + # point #2 + translate_to!(mol, Frac(Cart([0.494265, 2.22668, 0.450354]), xtal.box)) + # xenon.atoms.xf[:, 1] = sbmof1.box.c_to_f * [0.494265, 2.22668, 0.450354] + energy = vdw_energy(xtal, mol, ljff) + @test isapprox(energy, 12945.838, atol=0.005) + + random_rotation!(mol, xtal.box) + energy = vdw_energy(xtal, mol, ljff) + mol = Cart(mol, xtal.box) + @test isapprox(energy, vdw_energy(xtal, mol, ljff)) + + ### + # NIST data to test LJ potentials + # data from here: https://www.nist.gov/mml/csd/chemical-informatics-research-group/lennard-jones-fluid-reference-calculations + # created bogus atom X for this purpose. + ### + ljff = LJForceField("NIST"; r_cutoff=3.0) + energies_should_be = [-4.3515E+03, -6.9000E+02, -1.1467E+03, -1.6790E+01] + for c in 1:4 # four configurations + # read in positions of atoms provided by NIST ("X" atoms) + posfile = open("nist/lennardjones/lj_sample_config_periodic$c.txt") + lines = readlines(posfile) + # first line is dims of unit cell box + dims = parse.(Float64, split(lines[1])) + box = Box(dims..., π / 2, π / 2, π / 2) + # second line is # of molecules + n = parse(Int, lines[2]) + + # read in molecule positions, construct them + ms = Molecule[] + for i in 1:n + xyz = split(lines[2 + i])[2:end] + x = parse.(Float64, xyz) + m = deepcopy(X) + m = Frac(m, box) + translate_to!(m, Frac(box.c_to_f * x)) + push!(ms, m) + end + close(posfile) + + # compute energy of the configuration + energy = PorousMaterials.total_vdw_energy(ms, ljff, box) + @test isapprox(energy, energies_should_be[c], atol=0.1) + end + # test vdw_energy_no_PBC, which is the vdw_energy function when no PBCs are applied. + # The following "framework" is a cage floating in space so no atoms are near the boundary + # of the unit cell box. So with cutoff should get same with or without PBCs. + box = Box(100.0, 100.0, 100.0, π / 2, π / 2, π / 2) + co2 = deepcopy(CO2) + translate_to!(co2, Cart([50.0, 50.0, 50.0])) + f = Crystal("cage_in_space.cif") # same cage, but shifted to [50, 50, 50] in unit cell box 100 by 100 by 100. + ljff = LJForceField("UFF") + # energy with PBC but padded so effetive periodic interactions are zero, bc beyond cutoff + energy = vdw_energy(f, co2, ljff) + atoms = read_xyz("data/crystals/CB5.xyz") # raw .xyz of cage + @test isapprox(energy, vdw_energy_no_PBC(atoms, CO2.atoms, ljff)) + + ### + # guest-guest energetics + ### + ljff = LJForceField("Dreiding"; r_cutoff=12.5) + box = Box(25.0, 25.0, 25.0, π / 2, π / 2, π / 2) + # a He and Xe a distance of 6.0 away + xe = Frac(Xe, box) + he = Frac(He, box) + translate_to!(xe, Cart([5.0, 12.0, 12.0]), box) + translate_to!(he, Cart([11.0, 12.0, 12.0]), box) + molecules = [xe, he] + r² = (11.0 - 5.0)^2 # duh + energy = lennard_jones(r², ljff.σ²[:Xe][:He], ljff.ϵ[:Xe][:He]) + @test energy ≈ vdw_energy(1, molecules, ljff, box) + @test energy ≈ vdw_energy(2, molecules, ljff, box) # symmetry + + # via PBC, a distance (24.0 - 5.0) > (1+5) + translate_to!(molecules[2], Cart([24.0, 12.0, 12.0]), box) + r² = (1.0 + 5.0)^2 # PBC + energy = lennard_jones(r², ljff.σ²[:Xe][:He], ljff.ϵ[:He][:Xe]) + @test energy ≈ vdw_energy(2, molecules, ljff, box) + @test energy ≈ vdw_energy(1, molecules, ljff, box) # symmetry again. + + # put a molecule on top of first one. + push!(molecules, deepcopy(molecules[1])) + @test vdw_energy(2, molecules, ljff, box) ≈ 2 * energy + + @test vdw_energy(1, molecules, ljff, box) == Inf + @test vdw_energy(3, molecules, ljff, box) == Inf + + # interaction energy between first and second should be same via PBC + molecules_a = Frac.([Xe, He], box) + translate_to!(molecules_a[1], Cart([11.0, 1.0, 12.0]), box) + translate_to!(molecules_a[2], Cart([11.0, 4.0, 12.0]), box) + molecules_b = Frac.([Xe, He], box) + translate_to!(molecules_b[1], Cart([11.0, 1.0, 12.0]), box) + translate_to!(molecules_b[2], Cart([11.0, 23.0, 12.0]), box) + @test vdw_energy(1, molecules_a, ljff, box) ≈ vdw_energy(1, molecules_b, ljff, box) + + # another PBC one where three coords are different. + molecules = Frac.([Xe, He], box) + translate_to!(molecules[1], Cart([24.0, 23.0, 11.0]), box) + translate_to!(molecules[2], Cart([22.0, 2.0, 12.0]), box) + r² = 4.0^2 + 2.0^2 + 1.0^2 + energy = lennard_jones(r², ljff.σ²[:He][:Xe], ljff.ϵ[:He][:Xe]) + @test vdw_energy(1, molecules, ljff, box) ≈ energy + @test vdw_energy(2, molecules, ljff, box) ≈ energy + + # test cutoff radius. molecules here are too far to interact + translate_to!(molecules[1], Cart([0.0, 0.0, 0.0]), box) + translate_to!(molecules[2], Cart([12.0, 12.0, 12.0]), box) + @test vdw_energy(1, molecules, ljff, box) ≈ 0.0 + @test vdw_energy(2, molecules, ljff, box) ≈ 0.0 + # the position of a molecule should not change inside vdw_energy. + @test all(molecules[1].atoms.coords.xf[:, 1] .== box.c_to_f * [0.0, 0.0, 0.0]) + @test all(molecules[2].atoms.coords.xf[:, 1] .== box.c_to_f * [12.0, 12.0, 12.0]) + # TODO write tests for CO2 where there are more than one beads + + # Molecules with more than one ljsphere + + # two CO2 molecules 6.0 units apart + molecules_co2 = Frac.([deepcopy(CO2), deepcopy(CO2)], box) + translate_to!(molecules_co2[1], Cart([12.0, 9.0, 12.0]), box) + translate_to!(molecules_co2[2], Cart([12.0, 15.0, 12.0]), box) + # because the molecules have not been rotated, all corresponding beads are same + # distance apart when they are separated along the y-axis + r²_com = (15.0 - 9.0)^2 + # distance between teh central carbon and an oxygen in one molecule this + # takes advantage of the fact that the carbon is the central atom, and that + # all three atoms are in a line + r²_co = 1.16^2 + # distance between the two oxygens in one molecule + r²_oo = (2.0 * 1.16)^2 + energy = ( + 2.0 * lennard_jones(r²_com, ljff.σ²[:O_CO2][:O_CO2], ljff.ϵ[:O_CO2][:O_CO2]) + + 4.0 * + lennard_jones(r²_com + r²_co, ljff.σ²[:O_CO2][:C_CO2], ljff.ϵ[:O_CO2][:C_CO2]) + + 2.0 * + lennard_jones(r²_com + r²_oo, ljff.σ²[:O_CO2][:O_CO2], ljff.ϵ[:O_CO2][:O_CO2]) + + lennard_jones(r²_com, ljff.σ²[:C_CO2][:C_CO2], ljff.ϵ[:C_CO2][:C_CO2]) + ) + @test vdw_energy(1, molecules_co2, ljff, box) ≈ energy + @test vdw_energy(2, molecules_co2, ljff, box) ≈ energy + + # PBC placing one at 2.0 and the other at 21.0 + translate_to!(molecules_co2[1], Cart([12.0, 2.0, 12.0]), box) + translate_to!(molecules_co2[2], Cart([12.0, 21.0, 12.0]), box) + r²_com = (4.0 + 2.0)^2 + energy = ( + 2.0 * lennard_jones(r²_com, ljff.σ²[:O_CO2][:O_CO2], ljff.ϵ[:O_CO2][:O_CO2]) + + 4.0 * + lennard_jones(r²_com + r²_co, ljff.σ²[:O_CO2][:C_CO2], ljff.ϵ[:O_CO2][:C_CO2]) + + 2.0 * + lennard_jones(r²_com + r²_oo, ljff.σ²[:O_CO2][:O_CO2], ljff.ϵ[:O_CO2][:O_CO2]) + + lennard_jones(r²_com, ljff.σ²[:C_CO2][:C_CO2], ljff.ϵ[:C_CO2][:C_CO2]) + ) + @test vdw_energy(1, molecules_co2, ljff, box) ≈ energy + @test vdw_energy(2, molecules_co2, ljff, box) ≈ energy + + # testing cutoff radius, so only one oxygen from each will be able to interact + # making a larger box so that only a few.atoms from each CO2 will be able to interact + box_large = Box(50.0, 50.0, 50.0, π / 2, π / 2, π / 2) + molecules_co2 = Frac.([deepcopy(CO2), deepcopy(CO2)], box_large) + # placed 12.6 units apart so the C atoms will be outside the cutoff radius, + # but one O atom from each will be inside, so these will interact + translate_to!(molecules_co2[1], Cart([0.0, 0.0, 0.0]), box_large) + translate_to!(molecules_co2[2], Cart([13.0, 0.0, 0.0]), box_large) + r²_com = (13.0)^2 + r²_o = (13.0 - (2.0 * 1.16))^2 + r²_co = (13.0 - 1.16)^2 + energy = ( + lennard_jones(r²_o, ljff.σ²[:O_CO2][:O_CO2], ljff.ϵ[:O_CO2][:O_CO2]) + + 2 * lennard_jones(r²_co, ljff.σ²[:O_CO2][:C_CO2], ljff.ϵ[:O_CO2][:C_CO2]) + ) + @test vdw_energy(1, molecules_co2, ljff, box_large) ≈ energy + @test vdw_energy(2, molecules_co2, ljff, box_large) ≈ energy + + ### + # Mixture Tests + ### + # test the vdW guest-guest interation between two species + # set up the system + molecules = [[Molecule("Xe"), Molecule("Xe")], [Molecule("Kr")]] + ljff = LJForceField("UFF"; r_cutoff=6.0) + box = Box(10.0, 10.0, 10.0) + # convert molecules array to fractional using this box. + molecules = [Frac.(mols, box) for mols in molecules] + # position the molecules + translate_to!(molecules[1][1], Frac([0.1, 0.1, 0.1])) + translate_to!(molecules[1][2], Frac([0.1, 0.5, 0.1])) + translate_to!(molecules[2][1], Frac([0.6, 0.1, 0.1])) + # calculate vdW_energy interaction + r12_sqr = 4.0^2 + r13_sqr = 5.0^2 + energy_12 = + 4.0 * + ljff.ϵ[:Xe][:Xe] * + ((ljff.σ²[:Xe][:Xe] / r12_sqr)^6 - (ljff.σ²[:Xe][:Xe] / r12_sqr)^3) + energy_13 = + 4.0 * + ljff.ϵ[:Xe][:Kr] * + ((ljff.σ²[:Xe][:Kr] / r13_sqr)^6 - (ljff.σ²[:Xe][:Kr] / r13_sqr)^3) + @test (energy_12 + energy_13) ≈ vdw_energy(1, 1, molecules, ljff, box) + + # test for edge case (use ljff and box from previous test) + molecules = [[Molecule("Xe")], Molecule[]] + molecules = [Frac.(mols, box) for mols in molecules] + translate_to!(molecules[1][1], Frac([0.1, 0.1, 0.1])) + @test 0.0 == vdw_energy(1, 1, molecules, ljff, box) +end +end diff --git a/test/vdw_energy_timing.jl b/test/vdw_energy_timing.jl index e63e41019..223b0a42f 100644 --- a/test/vdw_energy_timing.jl +++ b/test/vdw_energy_timing.jl @@ -4,7 +4,7 @@ using Test using Random using Printf -ljforcefield = LJForceField("Dreiding", r_cutoff=12.5, mixing_rules="Lorentz-Berthelot") # Dreiding +ljforcefield = LJForceField("Dreiding"; r_cutoff=12.5, mixing_rules="Lorentz-Berthelot") # Dreiding # Xe in SBMOF-1 tests, comparing to RASPA sbmof1 = Crystal("SBMOF-1.cif") @@ -13,14 +13,16 @@ sbmof1 = replicate(sbmof1, rep_factors_sbmof1) xenon = Molecule("Xe") xenon = Frac(xenon, sbmof1.box) -@test ! has_charges(xenon) +@test !has_charges(xenon) xenon.atoms.coords.xf[:, 1] = sbmof1.box.c_to_f * zeros(3) energy = vdw_energy(sbmof1, xenon, ljforcefield) -@test isapprox(energy, -5041.58, atol = 0.005) -xenon.atoms.coords.xf[1, 1] = 0.494265; xenon.atoms.coords.xf[2, 1] = 2.22668; xenon.atoms.coords.xf[3, 1] = 0.450354; +@test isapprox(energy, -5041.58, atol=0.005) +xenon.atoms.coords.xf[1, 1] = 0.494265; +xenon.atoms.coords.xf[2, 1] = 2.22668; +xenon.atoms.coords.xf[3, 1] = 0.450354; xenon.atoms.coords.xf[:, 1] = sbmof1.box.c_to_f * xenon.atoms.coords.xf[:, 1] energy = vdw_energy(sbmof1, xenon, ljforcefield) -@test isapprox(energy, 12945.838, atol = 0.005) +@test isapprox(energy, 12945.838, atol=0.005) # guest-host vdw_energy timing vdw_energy(sbmof1, xenon, ljforcefield) @@ -29,7 +31,7 @@ println("Guest-Host Van der Waals energy computation:") # guest-guest vdw_energy timing Random.seed!(1234) # so that every trial will be the same -box = Box(100.0, 100.0, 100.0, π/2, π/2, π/2) +box = Box(100.0, 100.0, 100.0, π / 2, π / 2, π / 2) co2 = Molecule("CO2") ms = Array{Molecule{Frac}, 1}() num_molecules = 100 @@ -45,9 +47,12 @@ vdw_energy(1, ms, ljforcefield, box) println("Guest-Guest Van der Waals energy computation for a single molecule:") println("\t# of molecules: ", length(ms)) @btime vdw_energy(1, ms, ljforcefield, box) -@printf("Guest-Guest Van der Waals energy computation for all %i molecules:\n", num_molecules) +@printf( + "Guest-Guest Van der Waals energy computation for all %i molecules:\n", + num_molecules +) @btime begin - for i = eachindex(ms) + for i in eachindex(ms) vdw_energy(i, ms, ljforcefield, box) end end diff --git a/viz/IRMOF-1.cif b/viz/IRMOF-1.cif deleted file mode 100644 index ecde442e1..000000000 --- a/viz/IRMOF-1.cif +++ /dev/null @@ -1,445 +0,0 @@ -data_IRMOF-1_PM -_symmetry_space_group_name_H-M 'P1' -_symmetry_cell_setting triclinic -_cell_length_a 25.832000 -_cell_length_b 25.832000 -_cell_length_c 25.832000 -_cell_angle_alpha 90.000000 -_cell_angle_beta 90.000000 -_cell_angle_gamma 90.000000 -_symmetry_Int_Tables_number 1 - -loop_ -_symmetry_equiv_pos_as_xyz -'x,y,z' - -loop_ -_atom_site_label -_atom_site_type_symbol -_atom_site_fract_x -_atom_site_fract_y -_atom_site_fract_z -Zn Zn 0.293380 0.206620 0.206620 -Zn Zn 0.706620 0.793380 0.206620 -Zn Zn 0.706620 0.206620 0.793380 -Zn Zn 0.293380 0.793380 0.793380 -Zn Zn 0.206620 0.293380 0.206620 -Zn Zn 0.206620 0.706620 0.793380 -Zn Zn 0.793380 0.706620 0.206620 -Zn Zn 0.793380 0.293380 0.793380 -Zn Zn 0.206620 0.206620 0.293380 -Zn Zn 0.793380 0.206620 0.706620 -Zn Zn 0.206620 0.793380 0.706620 -Zn Zn 0.793380 0.793380 0.293380 -Zn Zn 0.206620 0.293380 0.793380 -Zn Zn 0.793380 0.706620 0.793380 -Zn Zn 0.206620 0.706620 0.206620 -Zn Zn 0.793380 0.293380 0.206620 -Zn Zn 0.293380 0.206620 0.793380 -Zn Zn 0.706620 0.206620 0.206620 -Zn Zn 0.706620 0.793380 0.793380 -Zn Zn 0.293380 0.793380 0.206620 -Zn Zn 0.206620 0.206620 0.706620 -Zn Zn 0.206620 0.793380 0.293380 -Zn Zn 0.793380 0.206620 0.293380 -Zn Zn 0.793380 0.793380 0.706620 -Zn Zn 0.293380 0.706620 0.706620 -Zn Zn 0.706620 0.293380 0.706620 -Zn Zn 0.706620 0.706620 0.293380 -Zn Zn 0.293380 0.293380 0.293380 -Zn Zn 0.293380 0.706620 0.293380 -Zn Zn 0.706620 0.706620 0.706620 -Zn Zn 0.706620 0.293380 0.293380 -Zn Zn 0.293380 0.293380 0.706620 -O O 0.250000 0.250000 0.250000 -O O 0.750000 0.750000 0.250000 -O O 0.750000 0.250000 0.750000 -O O 0.250000 0.750000 0.750000 -O O 0.250000 0.250000 0.750000 -O O 0.750000 0.750000 0.750000 -O O 0.250000 0.750000 0.250000 -O O 0.750000 0.250000 0.250000 -O O 0.281870 0.218130 0.134000 -O O 0.718130 0.781870 0.134000 -O O 0.718130 0.218130 0.866000 -O O 0.281870 0.781870 0.866000 -O O 0.134000 0.281870 0.218130 -O O 0.134000 0.718130 0.781870 -O O 0.866000 0.718130 0.218130 -O O 0.866000 0.281870 0.781870 -O O 0.218130 0.134000 0.281870 -O O 0.781870 0.134000 0.718130 -O O 0.218130 0.866000 0.718130 -O O 0.781870 0.866000 0.281870 -O O 0.218130 0.281870 0.866000 -O O 0.781870 0.718130 0.866000 -O O 0.218130 0.718130 0.134000 -O O 0.781870 0.281870 0.134000 -O O 0.281870 0.134000 0.781870 -O O 0.718130 0.134000 0.218130 -O O 0.718130 0.866000 0.781870 -O O 0.281870 0.866000 0.218130 -O O 0.134000 0.218130 0.718130 -O O 0.134000 0.781870 0.281870 -O O 0.866000 0.218130 0.281870 -O O 0.866000 0.781870 0.718130 -O O 0.281870 0.718130 0.634000 -O O 0.718130 0.281870 0.634000 -O O 0.718130 0.718130 0.366000 -O O 0.281870 0.281870 0.366000 -O O 0.134000 0.781870 0.718130 -O O 0.134000 0.218130 0.281870 -O O 0.866000 0.218130 0.718130 -O O 0.866000 0.781870 0.281870 -O O 0.218130 0.634000 0.781870 -O O 0.781870 0.634000 0.218130 -O O 0.218130 0.366000 0.218130 -O O 0.781870 0.366000 0.781870 -O O 0.218130 0.781870 0.366000 -O O 0.781870 0.218130 0.366000 -O O 0.218130 0.218130 0.634000 -O O 0.781870 0.781870 0.634000 -O O 0.281870 0.634000 0.281870 -O O 0.718130 0.634000 0.718130 -O O 0.718130 0.366000 0.281870 -O O 0.281870 0.366000 0.718130 -O O 0.134000 0.718130 0.218130 -O O 0.134000 0.281870 0.781870 -O O 0.866000 0.718130 0.781870 -O O 0.866000 0.281870 0.218130 -O O 0.781870 0.218130 0.634000 -O O 0.218130 0.781870 0.634000 -O O 0.218130 0.218130 0.366000 -O O 0.781870 0.781870 0.366000 -O O 0.634000 0.281870 0.718130 -O O 0.634000 0.718130 0.281870 -O O 0.366000 0.718130 0.718130 -O O 0.366000 0.281870 0.281870 -O O 0.718130 0.134000 0.781870 -O O 0.281870 0.134000 0.218130 -O O 0.718130 0.866000 0.218130 -O O 0.281870 0.866000 0.781870 -O O 0.718130 0.281870 0.366000 -O O 0.281870 0.718130 0.366000 -O O 0.718130 0.718130 0.634000 -O O 0.281870 0.281870 0.634000 -O O 0.781870 0.134000 0.281870 -O O 0.218130 0.134000 0.718130 -O O 0.218130 0.866000 0.281870 -O O 0.781870 0.866000 0.718130 -O O 0.634000 0.218130 0.218130 -O O 0.634000 0.781870 0.781870 -O O 0.366000 0.218130 0.781870 -O O 0.366000 0.781870 0.218130 -O O 0.781870 0.718130 0.134000 -O O 0.218130 0.281870 0.134000 -O O 0.218130 0.718130 0.866000 -O O 0.781870 0.281870 0.866000 -O O 0.634000 0.781870 0.218130 -O O 0.634000 0.218130 0.781870 -O O 0.366000 0.218130 0.218130 -O O 0.366000 0.781870 0.781870 -O O 0.718130 0.634000 0.281870 -O O 0.281870 0.634000 0.718130 -O O 0.718130 0.366000 0.718130 -O O 0.281870 0.366000 0.281870 -O O 0.718130 0.781870 0.866000 -O O 0.281870 0.218130 0.866000 -O O 0.718130 0.218130 0.134000 -O O 0.281870 0.781870 0.134000 -O O 0.781870 0.634000 0.781870 -O O 0.218130 0.634000 0.218130 -O O 0.218130 0.366000 0.781870 -O O 0.781870 0.366000 0.218130 -O O 0.634000 0.718130 0.718130 -O O 0.634000 0.281870 0.281870 -O O 0.366000 0.718130 0.281870 -O O 0.366000 0.281870 0.718130 -C C 0.250000 0.250000 0.111300 -C C 0.750000 0.750000 0.111300 -C C 0.750000 0.250000 0.888700 -C C 0.250000 0.750000 0.888700 -C C 0.111300 0.250000 0.250000 -C C 0.111300 0.750000 0.750000 -C C 0.888700 0.750000 0.250000 -C C 0.888700 0.250000 0.750000 -C C 0.250000 0.111300 0.250000 -C C 0.750000 0.111300 0.750000 -C C 0.250000 0.888700 0.750000 -C C 0.750000 0.888700 0.250000 -C C 0.250000 0.250000 0.888700 -C C 0.750000 0.750000 0.888700 -C C 0.250000 0.750000 0.111300 -C C 0.750000 0.250000 0.111300 -C C 0.250000 0.111300 0.750000 -C C 0.750000 0.111300 0.250000 -C C 0.750000 0.888700 0.750000 -C C 0.250000 0.888700 0.250000 -C C 0.111300 0.250000 0.750000 -C C 0.111300 0.750000 0.250000 -C C 0.888700 0.250000 0.250000 -C C 0.888700 0.750000 0.750000 -C C 0.250000 0.750000 0.611300 -C C 0.750000 0.250000 0.611300 -C C 0.750000 0.750000 0.388700 -C C 0.250000 0.250000 0.388700 -C C 0.250000 0.611300 0.750000 -C C 0.750000 0.611300 0.250000 -C C 0.250000 0.388700 0.250000 -C C 0.750000 0.388700 0.750000 -C C 0.250000 0.750000 0.388700 -C C 0.750000 0.250000 0.388700 -C C 0.250000 0.250000 0.611300 -C C 0.750000 0.750000 0.611300 -C C 0.250000 0.611300 0.250000 -C C 0.750000 0.611300 0.750000 -C C 0.750000 0.388700 0.250000 -C C 0.250000 0.388700 0.750000 -C C 0.611300 0.250000 0.750000 -C C 0.611300 0.750000 0.250000 -C C 0.388700 0.750000 0.750000 -C C 0.388700 0.250000 0.250000 -C C 0.611300 0.250000 0.250000 -C C 0.611300 0.750000 0.750000 -C C 0.388700 0.250000 0.750000 -C C 0.388700 0.750000 0.250000 -C C 0.250000 0.250000 0.053800 -C C 0.750000 0.750000 0.053800 -C C 0.750000 0.250000 0.946200 -C C 0.250000 0.750000 0.946200 -C C 0.053800 0.250000 0.250000 -C C 0.053800 0.750000 0.750000 -C C 0.946200 0.750000 0.250000 -C C 0.946200 0.250000 0.750000 -C C 0.250000 0.053800 0.250000 -C C 0.750000 0.053800 0.750000 -C C 0.250000 0.946200 0.750000 -C C 0.750000 0.946200 0.250000 -C C 0.250000 0.250000 0.946200 -C C 0.750000 0.750000 0.946200 -C C 0.250000 0.750000 0.053800 -C C 0.750000 0.250000 0.053800 -C C 0.250000 0.053800 0.750000 -C C 0.750000 0.053800 0.250000 -C C 0.750000 0.946200 0.750000 -C C 0.250000 0.946200 0.250000 -C C 0.053800 0.250000 0.750000 -C C 0.053800 0.750000 0.250000 -C C 0.946200 0.250000 0.250000 -C C 0.946200 0.750000 0.750000 -C C 0.250000 0.750000 0.553800 -C C 0.750000 0.250000 0.553800 -C C 0.750000 0.750000 0.446200 -C C 0.250000 0.250000 0.446200 -C C 0.250000 0.553800 0.750000 -C C 0.750000 0.553800 0.250000 -C C 0.250000 0.446200 0.250000 -C C 0.750000 0.446200 0.750000 -C C 0.250000 0.750000 0.446200 -C C 0.750000 0.250000 0.446200 -C C 0.250000 0.250000 0.553800 -C C 0.750000 0.750000 0.553800 -C C 0.250000 0.553800 0.250000 -C C 0.750000 0.553800 0.750000 -C C 0.750000 0.446200 0.250000 -C C 0.250000 0.446200 0.750000 -C C 0.553800 0.250000 0.750000 -C C 0.553800 0.750000 0.250000 -C C 0.446200 0.750000 0.750000 -C C 0.446200 0.250000 0.250000 -C C 0.553800 0.250000 0.250000 -C C 0.553800 0.750000 0.750000 -C C 0.446200 0.250000 0.750000 -C C 0.446200 0.750000 0.250000 -C C 0.282900 0.217100 0.026900 -C C 0.717100 0.782900 0.026900 -C C 0.717100 0.217100 0.973100 -C C 0.282900 0.782900 0.973100 -C C 0.026900 0.282900 0.217100 -C C 0.026900 0.717100 0.782900 -C C 0.973100 0.717100 0.217100 -C C 0.973100 0.282900 0.782900 -C C 0.217100 0.026900 0.282900 -C C 0.782900 0.026900 0.717100 -C C 0.217100 0.973100 0.717100 -C C 0.782900 0.973100 0.282900 -C C 0.217100 0.282900 0.973100 -C C 0.782900 0.717100 0.973100 -C C 0.217100 0.717100 0.026900 -C C 0.782900 0.282900 0.026900 -C C 0.282900 0.026900 0.782900 -C C 0.717100 0.026900 0.217100 -C C 0.717100 0.973100 0.782900 -C C 0.282900 0.973100 0.217100 -C C 0.026900 0.217100 0.717100 -C C 0.026900 0.782900 0.282900 -C C 0.973100 0.217100 0.282900 -C C 0.973100 0.782900 0.717100 -C C 0.282900 0.717100 0.526900 -C C 0.717100 0.282900 0.526900 -C C 0.717100 0.717100 0.473100 -C C 0.282900 0.282900 0.473100 -C C 0.026900 0.782900 0.717100 -C C 0.026900 0.217100 0.282900 -C C 0.973100 0.217100 0.717100 -C C 0.973100 0.782900 0.282900 -C C 0.217100 0.526900 0.782900 -C C 0.782900 0.526900 0.217100 -C C 0.217100 0.473100 0.217100 -C C 0.782900 0.473100 0.782900 -C C 0.217100 0.782900 0.473100 -C C 0.782900 0.217100 0.473100 -C C 0.217100 0.217100 0.526900 -C C 0.782900 0.782900 0.526900 -C C 0.282900 0.526900 0.282900 -C C 0.717100 0.526900 0.717100 -C C 0.717100 0.473100 0.282900 -C C 0.282900 0.473100 0.717100 -C C 0.026900 0.717100 0.217100 -C C 0.026900 0.282900 0.782900 -C C 0.973100 0.717100 0.782900 -C C 0.973100 0.282900 0.217100 -C C 0.782900 0.217100 0.526900 -C C 0.217100 0.782900 0.526900 -C C 0.217100 0.217100 0.473100 -C C 0.782900 0.782900 0.473100 -C C 0.526900 0.282900 0.717100 -C C 0.526900 0.717100 0.282900 -C C 0.473100 0.717100 0.717100 -C C 0.473100 0.282900 0.282900 -C C 0.717100 0.026900 0.782900 -C C 0.282900 0.026900 0.217100 -C C 0.717100 0.973100 0.217100 -C C 0.282900 0.973100 0.782900 -C C 0.717100 0.282900 0.473100 -C C 0.282900 0.717100 0.473100 -C C 0.717100 0.717100 0.526900 -C C 0.282900 0.282900 0.526900 -C C 0.782900 0.026900 0.282900 -C C 0.217100 0.026900 0.717100 -C C 0.217100 0.973100 0.282900 -C C 0.782900 0.973100 0.717100 -C C 0.526900 0.217100 0.217100 -C C 0.526900 0.782900 0.782900 -C C 0.473100 0.217100 0.782900 -C C 0.473100 0.782900 0.217100 -C C 0.782900 0.717100 0.026900 -C C 0.217100 0.282900 0.026900 -C C 0.217100 0.717100 0.973100 -C C 0.782900 0.282900 0.973100 -C C 0.526900 0.782900 0.217100 -C C 0.526900 0.217100 0.782900 -C C 0.473100 0.217100 0.217100 -C C 0.473100 0.782900 0.782900 -C C 0.717100 0.526900 0.282900 -C C 0.282900 0.526900 0.717100 -C C 0.717100 0.473100 0.717100 -C C 0.282900 0.473100 0.282900 -C C 0.717100 0.782900 0.973100 -C C 0.282900 0.217100 0.973100 -C C 0.717100 0.217100 0.026900 -C C 0.282900 0.782900 0.026900 -C C 0.782900 0.526900 0.782900 -C C 0.217100 0.526900 0.217100 -C C 0.217100 0.473100 0.782900 -C C 0.782900 0.473100 0.217100 -C C 0.526900 0.717100 0.717100 -C C 0.526900 0.282900 0.282900 -C C 0.473100 0.717100 0.282900 -C C 0.473100 0.282900 0.717100 -H H 0.304900 0.195100 0.044800 -H H 0.695100 0.804900 0.044800 -H H 0.695100 0.195100 0.955200 -H H 0.304900 0.804900 0.955200 -H H 0.044800 0.304900 0.195100 -H H 0.044800 0.695100 0.804900 -H H 0.955200 0.695100 0.195100 -H H 0.955200 0.304900 0.804900 -H H 0.195100 0.044800 0.304900 -H H 0.804900 0.044800 0.695100 -H H 0.195100 0.955200 0.695100 -H H 0.804900 0.955200 0.304900 -H H 0.195100 0.304900 0.955200 -H H 0.804900 0.695100 0.955200 -H H 0.195100 0.695100 0.044800 -H H 0.804900 0.304900 0.044800 -H H 0.304900 0.044800 0.804900 -H H 0.695100 0.044800 0.195100 -H H 0.695100 0.955200 0.804900 -H H 0.304900 0.955200 0.195100 -H H 0.044800 0.195100 0.695100 -H H 0.044800 0.804900 0.304900 -H H 0.955200 0.195100 0.304900 -H H 0.955200 0.804900 0.695100 -H H 0.304900 0.695100 0.544800 -H H 0.695100 0.304900 0.544800 -H H 0.695100 0.695100 0.455200 -H H 0.304900 0.304900 0.455200 -H H 0.044800 0.804900 0.695100 -H H 0.044800 0.195100 0.304900 -H H 0.955200 0.195100 0.695100 -H H 0.955200 0.804900 0.304900 -H H 0.195100 0.544800 0.804900 -H H 0.804900 0.544800 0.195100 -H H 0.195100 0.455200 0.195100 -H H 0.804900 0.455200 0.804900 -H H 0.195100 0.804900 0.455200 -H H 0.804900 0.195100 0.455200 -H H 0.195100 0.195100 0.544800 -H H 0.804900 0.804900 0.544800 -H H 0.304900 0.544800 0.304900 -H H 0.695100 0.544800 0.695100 -H H 0.695100 0.455200 0.304900 -H H 0.304900 0.455200 0.695100 -H H 0.044800 0.695100 0.195100 -H H 0.044800 0.304900 0.804900 -H H 0.955200 0.695100 0.804900 -H H 0.955200 0.304900 0.195100 -H H 0.804900 0.195100 0.544800 -H H 0.195100 0.804900 0.544800 -H H 0.195100 0.195100 0.455200 -H H 0.804900 0.804900 0.455200 -H H 0.544800 0.304900 0.695100 -H H 0.544800 0.695100 0.304900 -H H 0.455200 0.695100 0.695100 -H H 0.455200 0.304900 0.304900 -H H 0.695100 0.044800 0.804900 -H H 0.304900 0.044800 0.195100 -H H 0.695100 0.955200 0.195100 -H H 0.304900 0.955200 0.804900 -H H 0.695100 0.304900 0.455200 -H H 0.304900 0.695100 0.455200 -H H 0.695100 0.695100 0.544800 -H H 0.304900 0.304900 0.544800 -H H 0.804900 0.044800 0.304900 -H H 0.195100 0.044800 0.695100 -H H 0.195100 0.955200 0.304900 -H H 0.804900 0.955200 0.695100 -H H 0.544800 0.195100 0.195100 -H H 0.544800 0.804900 0.804900 -H H 0.455200 0.195100 0.804900 -H H 0.455200 0.804900 0.195100 -H H 0.804900 0.695100 0.044800 -H H 0.195100 0.304900 0.044800 -H H 0.195100 0.695100 0.955200 -H H 0.804900 0.304900 0.955200 -H H 0.544800 0.804900 0.195100 -H H 0.544800 0.195100 0.804900 -H H 0.455200 0.195100 0.195100 -H H 0.455200 0.804900 0.804900 -H H 0.695100 0.544800 0.304900 -H H 0.304900 0.544800 0.695100 -H H 0.695100 0.455200 0.695100 -H H 0.304900 0.455200 0.304900 -H H 0.695100 0.804900 0.955200 -H H 0.304900 0.195100 0.955200 -H H 0.695100 0.195100 0.044800 -H H 0.304900 0.804900 0.044800 -H H 0.804900 0.544800 0.804900 -H H 0.195100 0.544800 0.195100 -H H 0.195100 0.455200 0.804900 -H H 0.804900 0.455200 0.195100 -H H 0.544800 0.695100 0.695100 -H H 0.544800 0.304900 0.304900 -H H 0.455200 0.695100 0.304900 -H H 0.455200 0.304900 0.695100 diff --git a/viz/generate_files_to_viz.ipynb b/viz/generate_files_to_viz.ipynb deleted file mode 100644 index 5866a04f1..000000000 --- a/viz/generate_files_to_viz.ipynb +++ /dev/null @@ -1,82 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "general data folder: ../test/data\n", - "\tcrystal structures (.cif, .cssr): ../test/data/crystals\n", - "\tforce field files (.csv): ../test/data/forcefields\n", - "\tmolecule input files: ../test/data/molecules\n", - "\tsimulation output files: ../test/data/simulations\n", - "\tgrids (.cube): ../test/data/grids\n", - "See IRMOF-1.vtk\n", - "Computing energy grid of CH4 in IRMOF-1.cif\n", - "\tRegular grid (in fractional space) of 20 by 20 by 20 points superimposed over the unit cell.\n", - "\tSee /home/cokes/.julia/dev/PorousMaterials/viz/CH4_in_IRMOF-1.cube\n" - ] - } - ], - "source": [ - "using PorousMaterials\n", - "\n", - "set_path_to_data(joinpath(\"..\", \"test\", \"data\"))\n", - "@eval PorousMaterials PATH_TO_CRYSTALS = pwd()\n", - "@eval PorousMaterials PATH_TO_GRIDS = pwd()\n", - "\n", - "xtal = Crystal(\"IRMOF-1.cif\")\n", - "write_xyz(xtal)\n", - "write_vtk(xtal)\n", - "mol = Molecule(\"CH4\")\n", - "ljff = LJForceField(\"UFF\")\n", - "grid = energy_grid(xtal, mol, ljff, n_pts=(20, 20, 20))\n", - "\n", - "write_cube(grid, \"CH4_in_IRMOF-1.cube\", length_units=\"Bohr\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "py3Dmol can view .cif's but not ones written from PorousMaterials.jl!" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "xtal = Crystal(\"FIQCEN_clean.cif\")\n", - "write_cif(xtal, \"FIQCEN_pm.cif\", number_atoms=false)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Julia 1.5.0", - "language": "julia", - "name": "julia-1.5" - }, - "language_info": { - "file_extension": ".jl", - "mimetype": "application/julia", - "name": "julia", - "version": "1.5.0" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/viz/viz.ipynb b/viz/viz.ipynb deleted file mode 100644 index 4d676e9e5..000000000 --- a/viz/viz.ipynb +++ /dev/null @@ -1,199 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "import py3Dmol" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "def read_file(filename):\n", - " f = open(filename)\n", - " lines = f.read()\n", - " f.close()\n", - " return lines\n", - "\n", - "cif = read_file(\"IRMOF-1.cif\")\n", - "xyz = read_file(\"IRMOF-1.xyz\")\n", - "cube = read_file(\"CH4_in_IRMOF-1.cube\")" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "application/3dmoljs_load.v0": "
\n

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n jupyter labextension install jupyterlab_3dmol

\n
\n", - "text/html": [ - "
\n", - "

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n", - " jupyter labextension install jupyterlab_3dmol

\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "v = py3Dmol.view(width=400,height=400)\n", - "v.addModel(xyz, 'xyz')\n", - "v.setStyle({'sphere': {'colorscheme':'Jmol','scale':.5}, 'stick':{'colorscheme': 'Jmol'}})\n", - "v.addVolumetricData(cube, \"cube\", {'isoval': 0.0, 'color': \"yellow\", 'opacity': 0.6})\n", - "v.zoomTo()\n", - "v.setBackgroundColor('0xeeeeee')\n", - "# hack to get the unit cell\n", - "v.addModel(cif, 'cif')\n", - "v.addUnitCell()\n", - "# v.animate()\n", - "# v.addUnitCell(vtk, \"vtk\") # wish this worked\n", - "v.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "application/3dmoljs_load.v0": "
\n

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n jupyter labextension install jupyterlab_3dmol

\n
\n", - "text/html": [ - "
\n", - "

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n", - " jupyter labextension install jupyterlab_3dmol

\n", - "
\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "cif = open('IRMOF-1.cif').read() # doesn't work for .cif's written with PorousMaterials.jl\n", - "viewer = py3Dmol.view()\n", - "viewer.addModel(cif, 'cif')\n", - "viewer.setStyle({'sphere':{'colorscheme':'Jmol','scale':.5},'stick':{'colorscheme':'Jmol'}})\n", - "viewer.addUnitCell()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.9" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -}