cgkmc 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cgkmc-0.0.2/.gitignore ADDED
@@ -0,0 +1,178 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
+ .pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
+ __pypackages__/
122
+
123
+ # Celery stuff
124
+ celerybeat-schedule
125
+ celerybeat.pid
126
+
127
+ # SageMath parsed files
128
+ *.sage.py
129
+
130
+ # Environments
131
+ .env
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # PyCharm
164
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
167
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
+ .idea/
169
+
170
+ # Ruff stuff:
171
+ .ruff_cache/
172
+
173
+ # PyPI configuration file
174
+ .pypirc
175
+
176
+ _build/
177
+ scratch.py
178
+ .kmc_cache/
cgkmc-0.0.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jacob Jeffries
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
cgkmc-0.0.2/PKG-INFO ADDED
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: cgkmc
3
+ Version: 0.0.2
4
+ Summary: Crystal Growth Kinetic Monte Carlo
5
+ Project-URL: Homepage, https://github.com/jwjeffr/cgkmc
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: crystal,graph,growth,kmc,morphology,nanocrystal,surface
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Requires-Python: >=3.9
14
+ Requires-Dist: numpy~=2.0.2
15
+ Requires-Dist: scipy~=1.13.1
16
+ Provides-Extra: dev
17
+ Requires-Dist: hatchling~=1.27.0; extra == 'dev'
18
+ Requires-Dist: mypy~=1.13.0; extra == 'dev'
19
+ Requires-Dist: pdoc~=15.0.1; extra == 'dev'
20
+ Requires-Dist: pytest~=8.0.2; extra == 'dev'
21
+ Requires-Dist: ruff~=0.9.4; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # cgkmc
25
+
26
+ [![Custom shields.io](https://img.shields.io/badge/docs-brightgreen?logo=github&logoColor=green&label=gh-pages)](https://jwjeffr.github.io/cgkmc/)
27
+
28
+ [![PyPI version shields.io](https://img.shields.io/pypi/v/cgkmc.svg)](https://pypi.python.org/pypi/cgkmc/)
29
+ [![PyPI pyversions shields.io](https://img.shields.io/pypi/pyversions/cgkmc.svg)](https://pypi.python.org/pypi/cgkmc/)
30
+
31
+ ## 💎 What is cgkmc?
32
+
33
+ `cgkmc` is a package for performing crystal growth simulations using the
34
+ [Kinetic Monte Carlo](https://en.wikipedia.org/wiki/Kinetic_Monte_Carlo) method. `cgkmc`'s namesake is shorthand for
35
+ Crystal Growth Kinetic Monte Carlo.
36
+
37
+ ## 📥 Installation
38
+
39
+ ```bash
40
+ pip install cgkmc
41
+ ```
42
+
43
+ 📃 License
44
+
45
+ `cgkmc` is released under the MIT License.
cgkmc-0.0.2/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # cgkmc
2
+
3
+ [![Custom shields.io](https://img.shields.io/badge/docs-brightgreen?logo=github&logoColor=green&label=gh-pages)](https://jwjeffr.github.io/cgkmc/)
4
+
5
+ [![PyPI version shields.io](https://img.shields.io/pypi/v/cgkmc.svg)](https://pypi.python.org/pypi/cgkmc/)
6
+ [![PyPI pyversions shields.io](https://img.shields.io/pypi/pyversions/cgkmc.svg)](https://pypi.python.org/pypi/cgkmc/)
7
+
8
+ ## 💎 What is cgkmc?
9
+
10
+ `cgkmc` is a package for performing crystal growth simulations using the
11
+ [Kinetic Monte Carlo](https://en.wikipedia.org/wiki/Kinetic_Monte_Carlo) method. `cgkmc`'s namesake is shorthand for
12
+ Crystal Growth Kinetic Monte Carlo.
13
+
14
+ ## 📥 Installation
15
+
16
+ ```bash
17
+ pip install cgkmc
18
+ ```
19
+
20
+ 📃 License
21
+
22
+ `cgkmc` is released under the MIT License.
@@ -0,0 +1,23 @@
1
+ """
2
+ .. include:: ../README.md
3
+
4
+ # Examples
5
+
6
+ ## 📦 Simple cubic growth
7
+
8
+ Below is an example of the growth of a small simple cubic lattice, printing a LAMMPS-style dump string
9
+
10
+ ```py
11
+ .. include:: ../examples/simple_cubic.py
12
+ ```
13
+
14
+ """
15
+
16
+ __version__ = "0.0.2"
17
+ __authors__ = ["Jacob Jeffries"]
18
+ __author_emails__ = ["jwjeffr@clemson.edu"]
19
+ __url__ = "https://github.com/jwjeffr/cgkmc"
20
+
21
+ from . import containers as containers
22
+ from . import simulations as simulations
23
+ from . import utils as utils
@@ -0,0 +1,177 @@
1
+ from dataclasses import dataclass
2
+ from typing import Tuple, Optional
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ import scipy # type: ignore
8
+
9
+ from .utils import array_to_hex
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ @dataclass
15
+ class Solvent:
16
+ r"""
17
+ Solvent container class.
18
+ Temperature (or $\beta$), diffusivity, and solubility limit fully define the solvent.
19
+ $\beta$ should match the units of the interaction energies you specify.
20
+ See `utils.Units` class for available energy units, and use temp_to_beta() to get $\beta$ from temperature in K
21
+ """
22
+
23
+ beta: float
24
+ diffusivity: float
25
+ solubility_limit: float
26
+
27
+
28
+ @dataclass
29
+ class Growth:
30
+ """
31
+ Growth container class.
32
+ Mimics experimental controls, i.e. initial crystal size, amount of time we grow, and the final size we want.
33
+ """
34
+
35
+ initial_radius: float
36
+ num_steps: int
37
+ desired_size: int
38
+
39
+ @property
40
+ def initial_surface_area(self) -> float:
41
+ r"""
42
+ Shortcut property for computing surface area.
43
+ Crystal is assumed to be spherical, so surface area = $4\pi\times (\text{radius})^2$
44
+ """
45
+
46
+ return 4.0 * np.pi * self.initial_radius ** 2
47
+
48
+
49
+ @dataclass
50
+ class CubicLattice:
51
+ """
52
+ CubicLattice
53
+ """
54
+
55
+ dimensions: np.typing.NDArray[np.integer]
56
+ lattice_parameters: np.typing.NDArray[np.floating]
57
+ atomic_basis: np.typing.NDArray[np.floating]
58
+
59
+ def __post_init__(self):
60
+ # turn objects into tensors if they're not already tensors
61
+ if not isinstance(self.dimensions, np.ndarray):
62
+ self.dimensions = np.array(self.dimensions, dtype=int)
63
+ if not isinstance(self.lattice_parameters, np.ndarray):
64
+ self.lattice_parameters = np.array(self.lattice_parameters, dtype=float)
65
+ if not isinstance(self.atomic_basis, np.ndarray):
66
+ self.atomic_basis = np.array(self.atomic_basis, dtype=float)
67
+
68
+ @property
69
+ def density(self) -> float:
70
+
71
+ return self.atomic_basis.shape[0] / np.prod(self.lattice_parameters)
72
+
73
+ @property
74
+ def molecular_volume(self) -> float:
75
+
76
+ return 1.0 / self.density
77
+
78
+ def initialize_simulation(self) -> Tuple[np.typing.NDArray[np.floating], np.typing.NDArray[np.floating]]:
79
+
80
+ x = np.arange(self.dimensions[0]) * self.lattice_parameters[0]
81
+ y = np.arange(self.dimensions[1]) * self.lattice_parameters[1]
82
+ z = np.arange(self.dimensions[2]) * self.lattice_parameters[2]
83
+
84
+ x_grid, y_grid, z_grid = np.meshgrid(x, y, z, indexing='ij')
85
+ unit_cell_points = np.vstack([x_grid.ravel(), y_grid.ravel(), z_grid.ravel()]).T
86
+
87
+ lattice_points = unit_cell_points[:, None, :] + self.atomic_basis[None, :, :] * self.lattice_parameters
88
+ lattice_points = lattice_points.reshape(-1, 3)
89
+ bounds = self.lattice_parameters * self.dimensions
90
+
91
+ logger.debug("lattice sites initialized", extra={"num_sites": len(lattice_points), "bounds": bounds.tolist()})
92
+
93
+ return lattice_points, bounds
94
+
95
+
96
+ @dataclass
97
+ class KthNearest:
98
+
99
+ cutoffs: np.typing.NDArray[np.floating]
100
+ interaction_energies: np.typing.NDArray[np.floating]
101
+ maxint: Optional[int] = None
102
+ use_cache: Optional[bool] = False
103
+
104
+ def __post_init__(self):
105
+
106
+ if not self.maxint:
107
+ self.maxint = 10_000_000
108
+
109
+ if not isinstance(self.cutoffs, np.ndarray):
110
+ self.cutoffs = np.array(self.cutoffs)
111
+
112
+ if not isinstance(self.interaction_energies, np.ndarray):
113
+ self.interaction_energies = np.array(self.interaction_energies)
114
+
115
+ def compute_hamiltonian(
116
+ self,
117
+ lattice_points: np.typing.NDArray[np.floating],
118
+ bounds: np.typing.NDArray[np.floating]
119
+ ) -> scipy.sparse.csr_matrix:
120
+
121
+ tree = scipy.spatial.KDTree(lattice_points, leafsize=self.maxint, boxsize=bounds)
122
+
123
+ distance_matrix = tree.sparse_distance_matrix(tree, max_distance=self.cutoffs[-1]).tocsr()
124
+ distance_matrix.eliminate_zeros()
125
+
126
+ interaction_types = np.searchsorted(self.cutoffs, distance_matrix.data, side="left")
127
+ interaction_energies = self.interaction_energies[interaction_types]
128
+
129
+ return scipy.sparse.csr_matrix(
130
+ (interaction_energies, distance_matrix.indices, distance_matrix.indptr),
131
+ shape=distance_matrix.shape
132
+ )
133
+
134
+ def get_hamiltonian(
135
+ self,
136
+ lattice_points: np.typing.NDArray[np.floating],
137
+ bounds: np.typing.NDArray[np.floating]
138
+ ) -> scipy.sparse.csr_matrix:
139
+
140
+ if not self.use_cache:
141
+ hamiltonian = self.compute_hamiltonian(lattice_points, bounds)
142
+
143
+ logger.debug("hamiltonian initialized", extra={
144
+ "num_interactions": hamiltonian.nnz, "cohesive_energy": 0.5 * hamiltonian.sum(axis=0).mean()
145
+ })
146
+
147
+ return hamiltonian
148
+
149
+ cache_folder = Path(".kmc_cache")
150
+ cache_folder.mkdir(exist_ok=True)
151
+ hexes = [
152
+ array_to_hex(self.cutoffs),
153
+ array_to_hex(self.interaction_energies),
154
+ array_to_hex(lattice_points),
155
+ array_to_hex(bounds)
156
+ ]
157
+ hamiltonian_path = cache_folder / Path(f"{'_'.join(hexes)}.npz")
158
+
159
+ if not hamiltonian_path.exists():
160
+ hamiltonian = self.compute_hamiltonian(lattice_points, bounds)
161
+ scipy.sparse.save_npz(hamiltonian_path, hamiltonian)
162
+
163
+ logger.debug("hamiltonian initialized and saved", extra={
164
+ "num_interactions": hamiltonian.nnz, "cohesive_energy": 0.5 * hamiltonian.sum(axis=0).mean(),
165
+ "cache_path": hamiltonian_path.name
166
+ })
167
+
168
+ return hamiltonian
169
+
170
+ hamiltonian = scipy.sparse.load_npz(hamiltonian_path)
171
+
172
+ logger.debug("hamiltonian loaded from cache", extra={
173
+ "num_interactions": hamiltonian.nnz, "cohesive_energy": 0.5 * hamiltonian.sum(axis=0).mean(),
174
+ "cache_path": hamiltonian_path.name
175
+ })
176
+
177
+ return hamiltonian
@@ -0,0 +1,225 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Optional, Tuple, IO
3
+ from functools import cached_property
4
+ from io import StringIO
5
+ import logging
6
+
7
+ import numpy as np
8
+ import scipy # type: ignore
9
+
10
+ from .containers import Solvent, Growth, CubicLattice, KthNearest
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @dataclass
16
+ class Simulation:
17
+ """
18
+ Simulation class.
19
+ Main method is `Simulation.perform`, which performs a crystal growth simulation using Kinetic Monte Carlo
20
+ """
21
+
22
+ lattice: CubicLattice
23
+ interactions: KthNearest
24
+ solvent: Solvent
25
+ growth: Growth
26
+ seed: Optional[int] = None
27
+ generator: np.random.Generator = field(init=False)
28
+ lattice_points: np.typing.NDArray[np.floating] = field(init=False)
29
+ bounds: np.typing.NDArray[np.floating] = field(init=False)
30
+ surface_density: float = field(init=False)
31
+
32
+ def __post_init__(self):
33
+
34
+ self.seed = self.seed or 0
35
+ self.generator = np.random.default_rng(seed=self.seed)
36
+ self.lattice_points, self.bounds = self.lattice.initialize_simulation()
37
+
38
+ @cached_property
39
+ def hamiltonian(self) -> scipy.sparse.csr_matrix:
40
+
41
+ r"""
42
+ Defines the Hamiltonian matrix $\mathbf{Q}$, which defines the energy function:
43
+ $$E(x) = \frac{1}{2}\mathbf{x}^\intercal\mathbf{Q}\mathbf{x}$$
44
+ """
45
+
46
+ return self.interactions.get_hamiltonian(self.lattice_points, self.bounds)
47
+
48
+ @cached_property
49
+ def adjacency_matrix(self) -> scipy.sparse.csr_matrix:
50
+
51
+ r"""
52
+ Defines the adjacency matrix $\mathbf{A}$, where $A_{ij} = 1$ if sites $i$ and $j$ have an interaction.
53
+ """
54
+
55
+ return scipy.sparse.csr_matrix(
56
+ (np.ones_like(self.hamiltonian.data), self.hamiltonian.indices, self.hamiltonian.indptr),
57
+ shape=self.hamiltonian.shape
58
+ )
59
+
60
+ @cached_property
61
+ def num_neighbors(self) -> int:
62
+
63
+ num_neighbors_per_site = self.adjacency_matrix.sum(axis=0)
64
+ if not np.isclose(num_neighbors_per_site.std(), 0.0):
65
+ raise ValueError("non-periodic lattice")
66
+ return num_neighbors_per_site.mean()
67
+
68
+ def get_lammps_dump_str(self, types: np.typing.NDArray[np.floating], step: int, t: float) -> str:
69
+
70
+ """
71
+ Shortcut method for getting a LAMMPS-style dump str, which looks like:
72
+
73
+ ```
74
+ ITEM: TIMESTEP
75
+ 0 0.0
76
+ ITEM: NUMBER OF ATOMS
77
+ 1874
78
+ ITEM: BOX BOUNDS xy xz xx yy zz
79
+ 0.0 318.08 0.0
80
+ 0.0 318.08 0.0
81
+ 0.0 303.165 0.0
82
+ ITEM: ATOMS id type x y z
83
+ 39105 1 113.6000 131.7760 151.5825
84
+ 39189 1 113.6000 140.8640 131.3715
85
+ 39191 1 113.6000 140.8640 138.1085
86
+ 39193 1 113.6000 140.8640 144.8455
87
+ 39195 1 113.6000 140.8640 151.5825
88
+ 39197 1 113.6000 140.8640 158.3195
89
+ ...
90
+ ```
91
+
92
+ at each timestep
93
+ """
94
+
95
+ mask = types == 1
96
+
97
+ ids = np.arange(len(self.lattice_points))
98
+ occupied_ids = ids[mask].reshape(-1, 1)
99
+ occupied_sites = self.lattice_points[mask, :]
100
+
101
+ header = "\n".join([
102
+ "ITEM: TIMESTEP",
103
+ f"{step:.0f} {t}",
104
+ "ITEM: NUMBER OF ATOMS",
105
+ f"{len(occupied_ids):.0f}",
106
+ "ITEM: BOX BOUNDS xy xz xx yy zz",
107
+ f"0.0 {self.bounds[0]} 0.0",
108
+ f"0.0 {self.bounds[1]} 0.0",
109
+ f"0.0 {self.bounds[2]} 0.0",
110
+ "ITEM: ATOMS id type x y z"
111
+ ])
112
+
113
+ data = np.concatenate((occupied_ids, np.ones_like(occupied_ids), occupied_sites), axis=1)
114
+
115
+ with StringIO() as string_io:
116
+ np.savetxt(string_io, data, fmt=("%.0f", "%.0f", "%.4f", "%.4f", "%.4f"), comments="", header=header) # type: ignore
117
+ return string_io.getvalue()
118
+
119
+ @property
120
+ def kappa(self) -> float:
121
+
122
+ r"""
123
+ Shorthand when computing dynamic evaporation prefactor, i.e.:
124
+
125
+ $$\nu_t = \kappa / \left\langle \exp\left(\beta\Delta E_\text{evap}\right)\right\rangle$$
126
+ """
127
+
128
+ # only defined if the surface density has been calculated or not
129
+ if not hasattr(self, "surface_density"):
130
+ raise ValueError("surface density not yet calculated")
131
+
132
+ return (4.0 / 3.0 * np.pi * self.lattice.density) ** (1 / 3) * self.solvent.diffusivity * \
133
+ self.solvent.solubility_limit / (self.surface_density * self.growth.desired_size ** (1 / 3))
134
+
135
+ def get_interface(
136
+ self,
137
+ types: np.typing.NDArray[np.floating]
138
+ ) -> Tuple[np.typing.NDArray[np.integer], np.typing.NDArray[np.integer]]:
139
+
140
+ """
141
+ Function for computing interfacial solvent and solid sites.
142
+ Ax counts the number of currently occupied neighbors.
143
+ """
144
+
145
+ occupied_neighbor_count = self.adjacency_matrix @ types
146
+ solid_sites, = np.where(types * (self.num_neighbors - occupied_neighbor_count) > 0)
147
+ solvent_sites, = np.where((1.0 - types) * occupied_neighbor_count > 0)
148
+
149
+ return solid_sites, solvent_sites
150
+
151
+ def perform(self, dump_file: IO, dump_every: int):
152
+
153
+ """
154
+ Main method for performing a Kinetic Monte Carlo simulation. This is performable by calling something like:
155
+
156
+ ```py
157
+ simulation = Simulation(...)
158
+ with open("kmc.dump", "w") as file:
159
+ simulation.perform(dump_file=file, dump_every=100)
160
+ ```
161
+
162
+ or any other IO-like object, such as StringIO or BytesIO
163
+ """
164
+
165
+ # initialize a spherical crystal with specified radius
166
+ center = self.lattice_points.mean(axis=0)
167
+ types = (np.linalg.norm(self.lattice_points - center, axis=1) <= self.growth.initial_radius).astype(float)
168
+
169
+ solid_sites, _ = self.get_interface(types)
170
+ self.surface_density = len(solid_sites) / self.growth.initial_surface_area
171
+
172
+ t = 0.0
173
+ total_energy = 0.5 * types.T @ self.hamiltonian @ types
174
+ for step in range(self.growth.num_steps):
175
+
176
+ # if types.mean() is 0, entire cell is unoccupied, so no simulation
177
+ # similarly, if types.mean() is 1, entire cell is occupied
178
+ occupancy = types.mean()
179
+ if not 0.0 < occupancy < 1.0:
180
+ logging.error("simulation killed, no interface detected")
181
+ return
182
+
183
+ # only want dumps at a small frequency
184
+ if not step % dump_every:
185
+ positions = self.get_lammps_dump_str(types, step, t)
186
+ print(positions, file=dump_file, end="")
187
+ logging.info("simulation info", extra={
188
+ "step": step, "t": t, "total_energy": total_energy, "occupancy": occupancy
189
+ })
190
+ dump_file.flush()
191
+
192
+ solid_sites, solvent_sites = self.get_interface(types)
193
+
194
+ # need to rescale evaporation rates after calculating
195
+ # this is the dynamically updated prefactor
196
+ evaporation_barriers = -self.hamiltonian[solid_sites, :] @ types
197
+ evaporation_rates = np.exp(-self.solvent.beta * evaporation_barriers)
198
+ rate_prefactor = self.kappa / evaporation_rates.mean()
199
+ evaporation_rates = rate_prefactor * evaporation_rates
200
+
201
+ radius = (0.75 * self.lattice.molecular_volume * types.sum() / np.pi) ** (1 / 3)
202
+ adsorption_rates = np.ones_like(solvent_sites) * self.solvent.diffusivity * \
203
+ self.solvent.solubility_limit / (self.surface_density * radius)
204
+
205
+ # concatenate events, so we can pick event according to residence time algorithm
206
+ events = np.concatenate((solid_sites, solvent_sites))
207
+ rates = np.concatenate((evaporation_rates, adsorption_rates))
208
+ total_rate = rates.sum()
209
+
210
+ # pick event and advance time according to residence time algorithm
211
+ event = self.generator.choice(events, p=rates / total_rate)
212
+
213
+ # if there was an evaporation event, we've already computed \Delta E_evap
214
+ if types[event]:
215
+ change_in_energy_evap = evaporation_barriers[np.where(events == event)].item()
216
+ total_energy += change_in_energy_evap
217
+ # if it was an adsorption event, need to compute it
218
+ elif not types[event]:
219
+ change_in_energy_evap = -(self.hamiltonian[event, :] @ types).item()
220
+ total_energy += -change_in_energy_evap
221
+ else:
222
+ raise ValueError
223
+
224
+ types[event] = 1.0 - types[event]
225
+ t += self.generator.exponential(scale=1.0 / total_rate)
@@ -0,0 +1,56 @@
1
+ from enum import Enum, auto
2
+ from hashlib import sha1
3
+
4
+ import numpy as np
5
+
6
+
7
+ class Units(Enum):
8
+ """
9
+ Available units. See https://docs.lammps.org/units.html for details
10
+ """
11
+
12
+ real = auto()
13
+ metal = auto()
14
+ si = auto()
15
+ cgs = auto()
16
+ electron = auto()
17
+ micro = auto()
18
+ nano = auto()
19
+
20
+ def boltzmann_constant(self):
21
+
22
+ if self == Units.real:
23
+ return 1.987e-3
24
+
25
+ if self == Units.metal:
26
+ return 8.617e-5
27
+
28
+ if self == Units.si:
29
+ return 1.381e-23
30
+
31
+ if self == Units.cgs:
32
+ return 1.381e-16
33
+
34
+ if self == Units.electron:
35
+ return 3.167e-6
36
+
37
+ if self == Units.micro:
38
+ return 1.381e-6
39
+
40
+ if self == Units.nano:
41
+ return 1.381e-2
42
+
43
+ raise ValueError("invalid unit system")
44
+
45
+
46
+ def temp_to_beta(temperature: float, units: Units):
47
+ r"""
48
+ helper class to convert temperature to thermodynamic $\beta$ for a given choice of units
49
+ """
50
+
51
+ return 1.0 / (units.boltzmann_constant() * temperature)
52
+
53
+
54
+ def array_to_hex(x: np.typing.NDArray[np.floating]) -> str:
55
+
56
+ return sha1(x.tobytes()).hexdigest()
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cgkmc"
7
+ dynamic = ["version"]
8
+ description = "Crystal Growth Kinetic Monte Carlo"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ author = { name = "Jacob Jeffries", email = "jwjeffr@clemson.edu" }
13
+ keywords = [
14
+ "kmc",
15
+ "crystal",
16
+ "nanocrystal",
17
+ "morphology",
18
+ "graph",
19
+ "surface",
20
+ "growth"
21
+ ]
22
+ classifiers = [
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ ]
28
+ dependencies = [
29
+ "numpy ~= 2.0.2",
30
+ "scipy ~= 1.13.1"
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/jwjeffr/cgkmc"
35
+
36
+ [project.optional-dependencies]
37
+ dev = [
38
+ "hatchling~=1.27.0",
39
+ "pytest~=8.0.2",
40
+ "ruff~=0.9.4",
41
+ "mypy~=1.13.0",
42
+ "pdoc~=15.0.1"
43
+ ]
44
+
45
+ [tool.hatch.version]
46
+ path = "cgkmc/__init__.py"
47
+
48
+ [tool.hatch.build.targets.sdist]
49
+ include = [
50
+ "/cgkmc",
51
+ ]