imperial-materials-simulation 0.0.1__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.
- imperial_materials_simulation-0.0.1/PKG-INFO +10 -0
- imperial_materials_simulation-0.0.1/README.md +1 -0
- imperial_materials_simulation-0.0.1/imperial_materials_simulation/__init__.py +29 -0
- imperial_materials_simulation-0.0.1/imperial_materials_simulation/display.py +158 -0
- imperial_materials_simulation-0.0.1/imperial_materials_simulation/main.py +398 -0
- imperial_materials_simulation-0.0.1/imperial_materials_simulation/physics.py +146 -0
- imperial_materials_simulation-0.0.1/imperial_materials_simulation.egg-info/PKG-INFO +10 -0
- imperial_materials_simulation-0.0.1/imperial_materials_simulation.egg-info/SOURCES.txt +11 -0
- imperial_materials_simulation-0.0.1/imperial_materials_simulation.egg-info/dependency_links.txt +1 -0
- imperial_materials_simulation-0.0.1/imperial_materials_simulation.egg-info/requires.txt +9 -0
- imperial_materials_simulation-0.0.1/imperial_materials_simulation.egg-info/top_level.txt +1 -0
- imperial_materials_simulation-0.0.1/setup.cfg +4 -0
- imperial_materials_simulation-0.0.1/setup.py +31 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: imperial_materials_simulation
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Molecular simulation tool made for the undergraduate materials science and engineering theory and simulation module at Imperial College London
|
|
5
|
+
Home-page: https://github.com/AyhamSaffar/imperial_materials_simulation
|
|
6
|
+
Author: Ayham Al-Saffar
|
|
7
|
+
Requires-Python: ~=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
This is a test to make sure this comes up
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This is a test to make sure this comes up
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# __init__.py
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from imperial_materials_simulation.main import Simulation, load_simulation
|
|
4
|
+
|
|
5
|
+
starting_microstructure = pd.DataFrame(
|
|
6
|
+
[[ 1.26130966, -3.6279824 , 3.22874575],
|
|
7
|
+
[ 0.01109185, -3.81654351, 2.36666413],
|
|
8
|
+
[-1.35980652, -3.29266717, 1.93055455],
|
|
9
|
+
[-2.37977009, -2.15076958, 1.87652048],
|
|
10
|
+
[-2.57604888, -0.67746009, 2.24917055],
|
|
11
|
+
[-1.87620203, 0.5914393 , 2.74767705],
|
|
12
|
+
[-0.5004315 , 1.22611982, 2.97495106],
|
|
13
|
+
[ 1.00344892, 1.14070135, 2.69818462],
|
|
14
|
+
[ 2.12602653, 0.50009417, 1.87770294],
|
|
15
|
+
[ 2.5645804 , -0.39591704, 0.7168456 ],
|
|
16
|
+
[ 2.18172369, -1.32983529, -0.43350902],
|
|
17
|
+
[ 1.10831734, -1.9090663 , -1.35772197],
|
|
18
|
+
[-0.30107549, -1.83240466, -1.94918892],
|
|
19
|
+
[-1.58628718, -1.03144403, -2.17155873],
|
|
20
|
+
[-2.40576488, 0.23981749, -1.93470708],
|
|
21
|
+
[-2.43673373, 1.66539118, -1.37583959],
|
|
22
|
+
[-1.61465889, 2.84516096, -0.8471113 ],
|
|
23
|
+
[-0.1902823 , 3.40079245, -0.74306813],
|
|
24
|
+
[ 1.24620565, 3.26671434, -1.25936233],
|
|
25
|
+
[ 2.09305162, 2.57081936, -2.32976298],
|
|
26
|
+
[ 2.17803079, 1.66144767, -3.55851113],
|
|
27
|
+
[ 1.45327508, 0.95559197, -4.70667552]],
|
|
28
|
+
columns=['x', 'y', 'z']
|
|
29
|
+
)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Methods for creating interactive visualisations of the simulated molecule and its measurements
|
|
3
|
+
within Jupyter Notebook using ipywidgets.
|
|
4
|
+
'''
|
|
5
|
+
import ipywidgets as ipy
|
|
6
|
+
import py3Dmol
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import matplotlib
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from IPython.display import display, clear_output
|
|
11
|
+
|
|
12
|
+
class SimulationDashboard():
|
|
13
|
+
'''
|
|
14
|
+
Class for creating inline Jupter Notebook visualisations of how the measurements and microstructure of simulated
|
|
15
|
+
molecules vary live with time. This is built on top of the library's Simulation object.
|
|
16
|
+
'''
|
|
17
|
+
|
|
18
|
+
def __init__(self, sim) -> None:
|
|
19
|
+
'''Initiates internal methods, attributes, and dashboard widgets'''
|
|
20
|
+
#TODO replace matplotlib with plotly for speed
|
|
21
|
+
matplotlib.use('module://ipympl.backend_nbagg')
|
|
22
|
+
self.sim = sim
|
|
23
|
+
|
|
24
|
+
self.mol_viewer = py3Dmol.view(width=300, height=300)
|
|
25
|
+
with plt.ioff():
|
|
26
|
+
self.fig, self.left_ax = plt.subplots(figsize=(6, 3))
|
|
27
|
+
self.fig.tight_layout(pad=2)
|
|
28
|
+
self.right_ax = self.left_ax.twinx()
|
|
29
|
+
self.line = self.right_ax.axvline(x=0.5, color='black', linestyle='--')
|
|
30
|
+
self.fig.canvas.header_visible = False
|
|
31
|
+
self.fig.canvas.toolbar_visible = False
|
|
32
|
+
self.fig.canvas.footer_visible = False
|
|
33
|
+
|
|
34
|
+
self.observers_enabled = False
|
|
35
|
+
self.run_slider = ipy.IntSlider(value=0, min=0, max=0, description='Run', orientation='vertical')
|
|
36
|
+
self.table_widget = ipy.Output(layout=ipy.Layout(height='200px', overflow='auto'))
|
|
37
|
+
with self.table_widget:
|
|
38
|
+
display(self.sim.run_data)
|
|
39
|
+
top_box = ipy.HBox(children=[self.run_slider, self.table_widget])
|
|
40
|
+
self.step_slider = ipy.IntSlider(value=0, min=0, max=0, step=self.sim.microstructure_logging_interval,
|
|
41
|
+
description='Step', orientation='Horizontal', layout=ipy.Layout(width='900px'))
|
|
42
|
+
self.left_axis_selector = ipy.Dropdown(options=(), description='left (red)')
|
|
43
|
+
self.right_axis_selector = ipy.Dropdown(options=(), description='right (blue)')
|
|
44
|
+
selector_box = ipy.HBox(children=[self.left_axis_selector, self.right_axis_selector])
|
|
45
|
+
plot_box = ipy.Output()
|
|
46
|
+
with plot_box:
|
|
47
|
+
self.fig.show()
|
|
48
|
+
plot_box = ipy.VBox(children=[plot_box, selector_box])
|
|
49
|
+
self.molecule_box = ipy.Output()
|
|
50
|
+
self._redraw_molecule()
|
|
51
|
+
bottom_box = ipy.HBox(children=[plot_box, self.molecule_box], layout=ipy.Layout(align_items='center'))
|
|
52
|
+
self.display_widget = ipy.VBox(children=[top_box, self.step_slider, bottom_box])
|
|
53
|
+
self._enable_observers()
|
|
54
|
+
|
|
55
|
+
def display(self, sim) -> None:
|
|
56
|
+
'''Creates an instance of the dashboard in the output of the notebook cell it is called in.'''
|
|
57
|
+
self.sim = sim
|
|
58
|
+
if self.sim.run_data['run'].max() > 0:
|
|
59
|
+
self.run_slider.max = self.sim.run_data['run'].max()
|
|
60
|
+
self.run_slider.min = 1
|
|
61
|
+
display(self.display_widget)
|
|
62
|
+
|
|
63
|
+
def live_update(self, sim, step: int, run_type: str, n_steps: int, temperature: float):
|
|
64
|
+
'''Updates the dashboard live after new runs are started'''
|
|
65
|
+
self.sim = sim
|
|
66
|
+
if step > 0:
|
|
67
|
+
self.step_slider.value = step
|
|
68
|
+
self._redraw_molecule()
|
|
69
|
+
self._redraw_plot()
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
#TODO freeze selectors during live update
|
|
73
|
+
current_run_data = {'run': self.sim.run, 'type': run_type, 'n_steps': n_steps, 'T': temperature}
|
|
74
|
+
with self.table_widget:
|
|
75
|
+
clear_output()
|
|
76
|
+
display(pd.concat([self.sim.run_data, pd.DataFrame(current_run_data, index=[0])]))
|
|
77
|
+
self._disable_observers()
|
|
78
|
+
self.run_slider.max += 1
|
|
79
|
+
self.step_slider.max = ((n_steps-1) // self.sim.microstructure_logging_interval) * self.sim.microstructure_logging_interval
|
|
80
|
+
self.run_slider.disabled = True
|
|
81
|
+
self.step_slider.disabled = True
|
|
82
|
+
self.run_slider.value = self.run_slider.max
|
|
83
|
+
self._reset_axis_selectors()
|
|
84
|
+
|
|
85
|
+
def reset(self, sim):
|
|
86
|
+
'''Resets the dashboard at the end of a live display run so it can continue to be used'''
|
|
87
|
+
self.sim = sim
|
|
88
|
+
self.run_slider.min = 1
|
|
89
|
+
self.run_slider.disabled = False
|
|
90
|
+
self.step_slider.disabled = False
|
|
91
|
+
with self.table_widget:
|
|
92
|
+
clear_output()
|
|
93
|
+
display(self.sim.run_data)
|
|
94
|
+
self._enable_observers()
|
|
95
|
+
self._redraw_plot()
|
|
96
|
+
self.step_slider.value = self.step_slider.max
|
|
97
|
+
|
|
98
|
+
def _step_slider_moved(self, _= None) -> None: #placeholder arguement as widget observe calls method with redundant arguement
|
|
99
|
+
self.step_slider.value = self.step_slider.value
|
|
100
|
+
self._redraw_molecule()
|
|
101
|
+
self.line.set_xdata([self.step_slider.value])
|
|
102
|
+
self.fig.canvas.draw()
|
|
103
|
+
|
|
104
|
+
def _run_slider_moved(self, _= None) -> None:
|
|
105
|
+
self.step_slider.value = 0
|
|
106
|
+
self._reset_axis_selectors()
|
|
107
|
+
self.step_slider.max = self.sim.step_data[self.run_slider.value].shape[0]-self.sim.microstructure_logging_interval
|
|
108
|
+
self._redraw_plot()
|
|
109
|
+
self._redraw_molecule()
|
|
110
|
+
|
|
111
|
+
def _reset_axis_selectors(self):
|
|
112
|
+
self._disable_observers()
|
|
113
|
+
self.left_axis_selector.options = self.sim.step_data[self.run_slider.value].columns[1:]
|
|
114
|
+
self.right_axis_selector.options = self.sim.step_data[self.run_slider.value].columns[1:]
|
|
115
|
+
self.left_axis_selector.value, self.right_axis_selector.value = self.sim.step_data[self.run_slider.value].columns[-2:]
|
|
116
|
+
self._enable_observers()
|
|
117
|
+
|
|
118
|
+
def _disable_observers(self):
|
|
119
|
+
if self.observers_enabled == False:
|
|
120
|
+
return
|
|
121
|
+
self.observers_enabled = False
|
|
122
|
+
self.step_slider.unobserve(self._step_slider_moved, names='value')
|
|
123
|
+
self.run_slider.unobserve(self._run_slider_moved, names='value')
|
|
124
|
+
self.left_axis_selector.unobserve(self._redraw_plot, names='value')
|
|
125
|
+
self.right_axis_selector.unobserve(self._redraw_plot, names='value')
|
|
126
|
+
|
|
127
|
+
def _enable_observers(self):
|
|
128
|
+
if self.observers_enabled:
|
|
129
|
+
return
|
|
130
|
+
self.observers_enabled = True
|
|
131
|
+
self.step_slider.observe(self._step_slider_moved, names='value')
|
|
132
|
+
self.run_slider.observe(self._run_slider_moved, names='value')
|
|
133
|
+
self.left_axis_selector.observe(self._redraw_plot, names='value')
|
|
134
|
+
self.right_axis_selector.observe(self._redraw_plot, names='value')
|
|
135
|
+
|
|
136
|
+
def _redraw_molecule(self, _=None) -> None:
|
|
137
|
+
structure = self.sim.microstructures[self.run_slider.value][self.step_slider.value].copy()
|
|
138
|
+
structure.insert(loc=0, column='element', value='C')
|
|
139
|
+
molecule_xyz = f'{len(structure)}\n some comment\n {structure.to_string(header=False, index=False)}'
|
|
140
|
+
self.mol_viewer.clear()
|
|
141
|
+
self.mol_viewer.addModel(molecule_xyz, 'xyz')
|
|
142
|
+
self.mol_viewer.setStyle({'stick': {'colorscheme': 'default'}, 'sphere': {'scale': 0.3, 'colorscheme': 'cyanCarbon'}})
|
|
143
|
+
self.mol_viewer.zoomTo()
|
|
144
|
+
with self.molecule_box:
|
|
145
|
+
self.mol_viewer.update()
|
|
146
|
+
|
|
147
|
+
def _redraw_plot(self, _=None) -> None:
|
|
148
|
+
left_data = self.sim.step_data[self.run_slider.value][self.left_axis_selector.value]
|
|
149
|
+
right_data = self.sim.step_data[self.run_slider.value][self.right_axis_selector.value]
|
|
150
|
+
self.left_ax.clear()
|
|
151
|
+
self.right_ax.clear()
|
|
152
|
+
self.left_ax.set_yscale('log' if left_data.max() > left_data.min()*100 and left_data.min() > 0 else 'linear')
|
|
153
|
+
self.right_ax.set_yscale('log' if right_data.max() > right_data.min()*100 and right_data.min() > 0 else 'linear')
|
|
154
|
+
self.right_ax.ticklabel_format(axis='x', style='sci', scilimits=(0,4))
|
|
155
|
+
self.left_ax.plot(left_data, color='red')
|
|
156
|
+
self.right_ax.plot(right_data, color='blue')
|
|
157
|
+
self.line = self.right_ax.axvline(x=self.step_slider.value, color='black', linestyle='--')
|
|
158
|
+
self.fig.canvas.draw()
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
from . import physics, display
|
|
2
|
+
import numpy as np
|
|
3
|
+
import numpy.random as rand
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import pickle
|
|
6
|
+
import warnings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Simulation():
|
|
10
|
+
'''
|
|
11
|
+
A script for atomistic simulations of toy polymers. Each polymer is a linear string of beads
|
|
12
|
+
(CH2 units) with no side chains. Bonds are springs and nonbonded atoms interact via a 12-6
|
|
13
|
+
Lennard-Jones potential.
|
|
14
|
+
|
|
15
|
+
Four methods of atomistic simulation are implemented:
|
|
16
|
+
|
|
17
|
+
(1) Steepest descent structural relaxation / energy minimization.
|
|
18
|
+
|
|
19
|
+
(2) Constant temperature ('NVT') dynamics with a Langevin thermostat.
|
|
20
|
+
|
|
21
|
+
(3) Hamiltonian (aka `constant energy' or `NVE') molecular dynamics.
|
|
22
|
+
|
|
23
|
+
(4) Metropolis Monte Carlo ('MMC') stochastic model.
|
|
24
|
+
|
|
25
|
+
Paul Tangney, Ayham Al-Saffar 2024
|
|
26
|
+
'''
|
|
27
|
+
|
|
28
|
+
def __init__(self, n_atoms: int, starting_temperature: float, microstructure: pd.DataFrame = None,
|
|
29
|
+
microstructure_logging_interval: int = 100) -> None:
|
|
30
|
+
'''
|
|
31
|
+
Initializes internal microstate, physics, and display variables.
|
|
32
|
+
Units are in electron volts, femto-seconds, Angstroms, and Kelvin.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
n_atoms : int
|
|
37
|
+
Number of atoms in the simluated carbon chain.
|
|
38
|
+
|
|
39
|
+
starting_temperature : int
|
|
40
|
+
Temperature used when initiating atom velocities.
|
|
41
|
+
|
|
42
|
+
microstructure : pd.DataFrame
|
|
43
|
+
Dataframe specifying the intitial positions of each atom. Dataframe must have n_atoms rows
|
|
44
|
+
and the columns 'x', 'y', 'z'. If not given, a straight linear chain is created.
|
|
45
|
+
|
|
46
|
+
microstructure_logging_interval : int
|
|
47
|
+
How many steps are run between each time the microstrucutre is saved. This can be reduced
|
|
48
|
+
to as low as 10 without significantly impacting run speed, however this significantly
|
|
49
|
+
increases the size of the saved simulation file.
|
|
50
|
+
'''
|
|
51
|
+
self.n_atoms = n_atoms
|
|
52
|
+
self.microstructure_logging_interval = microstructure_logging_interval
|
|
53
|
+
self.is_being_displayed = False
|
|
54
|
+
self.time_step = 0.8
|
|
55
|
+
self.mass = 1451.0 # 14 AMU (CH2 Mr) in eV fs^2/Å^2
|
|
56
|
+
|
|
57
|
+
#force parameters taken from https://doi.org/10.1063/1.476826
|
|
58
|
+
self.sigma = 4.5 #equilibrium Lennard Jones atomic seperation
|
|
59
|
+
self.epsilon = 0.00485678 #Lennard Jones coefficient
|
|
60
|
+
self.bond_length = 1.53 #equilibrium neighbour bond length
|
|
61
|
+
self.spring_constant = 15.18
|
|
62
|
+
|
|
63
|
+
#increases initial Boltzmann distribution velocities when generating a new microstate as some
|
|
64
|
+
#of this energy is converted to potential energy. Its value should be 2.0 for a harmonic potential.
|
|
65
|
+
T_factor = 1.5 if microstructure is None else 1.0 #! Will discuss what this value should be on Friday
|
|
66
|
+
self.kB = 8.617333262e-05
|
|
67
|
+
self.target_kT = starting_temperature * self.kB
|
|
68
|
+
self.velocity_sigma = np.sqrt(self.target_kT/self.mass) #variance of Boltzmann distrubution velocities
|
|
69
|
+
self.velocities = rand.normal(loc=0.0, scale=np.sqrt(T_factor)*self.velocity_sigma, size=(self.n_atoms, 3))
|
|
70
|
+
|
|
71
|
+
self.forces = np.zeros(shape=(self.n_atoms, 3))
|
|
72
|
+
if microstructure is None:
|
|
73
|
+
self.positions = np.zeros(shape=(self.n_atoms, 3))
|
|
74
|
+
self.positions[:, 1] = np.linspace(start=0.0, stop=(self.n_atoms-1)*self.bond_length, num=self.n_atoms)
|
|
75
|
+
else:
|
|
76
|
+
assert microstructure.shape[0] == self.n_atoms
|
|
77
|
+
assert all([col in microstructure.columns for col in ['x', 'y', 'z']])
|
|
78
|
+
self.positions = microstructure[['x', 'y', 'z']].values
|
|
79
|
+
|
|
80
|
+
self.positions -= np.mean(self.positions, axis=0, keepdims=True) #centre molecule
|
|
81
|
+
self.velocities -= np.mean(self.velocities, axis=0, keepdims=True) #remove overall molecule movement
|
|
82
|
+
|
|
83
|
+
#list with a dict {step: microstructure} for each run
|
|
84
|
+
self.microstructures = [{0: pd.DataFrame(self.positions, columns=['x', 'y', 'z']).copy()}]
|
|
85
|
+
self.run = 0
|
|
86
|
+
data_structure = {'run': int, 'type': str, 'n_steps': int, 'T': float, 'KE': float, 'PE_bonding': float,
|
|
87
|
+
'PE_non_bonding': float, 'PE_total': float, 'F_rms': float, 'L_end_to_end': float}
|
|
88
|
+
self.run_data = pd.DataFrame({name: pd.Series(dtype=dtype) for name, dtype in data_structure.items()})
|
|
89
|
+
self._log_run_data(run_type='init', n_steps=0, temperature=starting_temperature)
|
|
90
|
+
self.step_data = {} #{run: dataframe}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def relax_run(self, n_steps: int, step_size: float = 0.01) -> None:
|
|
94
|
+
'''
|
|
95
|
+
Structural relaxation through steepest descent energy minimisation. This is a non-physical simulation
|
|
96
|
+
where the molecule is modelled as not experiencing any thermal forces. Each atom moves in the direction
|
|
97
|
+
that minimizes is potential energy the most, which is the net (bonding + non-bonding) force vector.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
n_steps : int
|
|
102
|
+
Number of steps of steepest descent energy minimisation.
|
|
103
|
+
|
|
104
|
+
step_size: float
|
|
105
|
+
How far each atom should move with each step. The total movement vector is the relax_step_size * force
|
|
106
|
+
vector. 0.01 is a good starting point, but could be ~10x higher for the first few steps and ~10x smaller
|
|
107
|
+
for the last few steps.
|
|
108
|
+
|
|
109
|
+
'''
|
|
110
|
+
self.run += 1
|
|
111
|
+
self.microstructures.append({0: pd.DataFrame(self.positions, columns=['x', 'y', 'z'])})
|
|
112
|
+
step_data = []
|
|
113
|
+
|
|
114
|
+
for step in range(n_steps):
|
|
115
|
+
F_b, PE_b = physics.get_bonding_interactions(self.positions, self.bond_length, self.spring_constant)
|
|
116
|
+
F_nb, PE_nb = physics.get_non_bonding_interactions(self.positions, self.epsilon, self.sigma)
|
|
117
|
+
self.positions += (F_nb + F_b) * step_size
|
|
118
|
+
step_data.append({
|
|
119
|
+
'step': step,
|
|
120
|
+
'PE_bonding': PE_b,
|
|
121
|
+
'PE_non_bonding': PE_nb,
|
|
122
|
+
'PE_total': PE_b + PE_nb,
|
|
123
|
+
'F_rms': np.mean((F_b + F_nb)**2) ** 0.5,
|
|
124
|
+
'L_end_to_end': np.sum((self.positions[0] - self.positions[-1]) ** 2) ** 0.5,
|
|
125
|
+
})
|
|
126
|
+
self._logging_step(step, step_data, run_type='relax', n_steps=n_steps, temperature=0.0)
|
|
127
|
+
|
|
128
|
+
self.step_data[self.run] = pd.DataFrame(step_data)
|
|
129
|
+
self._log_run_data(run_type='relax', n_steps=n_steps, temperature=0.0)
|
|
130
|
+
self.dashboard.reset(self) if self.is_being_displayed else print(f'{n_steps:,} step relax run completed')
|
|
131
|
+
|
|
132
|
+
def NVT_run(self, n_steps: int, temperature: float, gamma: float = 0.005, integrator: str = 'OVRVO'):
|
|
133
|
+
'''
|
|
134
|
+
Constant number of atoms, volume, and temperature (NVT) simulation with a Langevin thermostat. Simple
|
|
135
|
+
model that approximates the physical influence of a solute bath at a given temperature. It adds a drag
|
|
136
|
+
and random force onto the energy minimization force. https://doi.org/10.1021/jp411770f details different
|
|
137
|
+
techniques for integrating these forces over time.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
n_steps : int
|
|
142
|
+
Number of NVT steps to perform.
|
|
143
|
+
|
|
144
|
+
temperature: float
|
|
145
|
+
Temperature of simulation. A higher temperature leads to higher solute forces and so velocities. Note
|
|
146
|
+
the actual temperature of the simulation at each step (dictated by atom velocities) may vary slightly.
|
|
147
|
+
|
|
148
|
+
gamma: float
|
|
149
|
+
Solvent interaction strength. Higher values lead to higher solute drag and random solute forces. 0 is
|
|
150
|
+
no solvent, 0.001 is weak interaction, 0.005 is medium interaction, and 0.1 is strong interaction
|
|
151
|
+
|
|
152
|
+
integrator: str
|
|
153
|
+
How forces are intergrated over time. Either 'OVRVO' or 'VRORV'. See paper for more details.
|
|
154
|
+
#TODO make all available
|
|
155
|
+
'''
|
|
156
|
+
assert integrator in ('OVRVO', 'VRORV'), f'{integrator} integrator not supported'
|
|
157
|
+
self.run += 1
|
|
158
|
+
self.microstructures.append({0: pd.DataFrame(self.positions, columns=['x', 'y', 'z'])})
|
|
159
|
+
step_data = []
|
|
160
|
+
|
|
161
|
+
self.target_kT = temperature * self.kB
|
|
162
|
+
self.velocity_sigma = np.sqrt(self.target_kT/self.mass) #variance of Boltzmann distrubution velocities
|
|
163
|
+
|
|
164
|
+
F_b, PE_b = physics.get_bonding_interactions(self.positions, self.bond_length, self.spring_constant)
|
|
165
|
+
F_nb, PE_nb = physics.get_non_bonding_interactions(self.positions, self.epsilon, self.sigma)
|
|
166
|
+
a = np.exp(-gamma / (self.time_step*2)) if integrator == 'OVRVO' else np.exp(-gamma/self.time_step)
|
|
167
|
+
b = np.sqrt(1 - a**2)
|
|
168
|
+
for step in range(n_steps):
|
|
169
|
+
if integrator == 'OVRVO':
|
|
170
|
+
self.velocities = self.velocities*a + rand.standard_normal((self.n_atoms, 3))*self.velocity_sigma*b
|
|
171
|
+
self.velocities += (F_b + F_nb)*self.time_step / (2*self.mass)
|
|
172
|
+
self.positions += self.velocities*self.time_step
|
|
173
|
+
F_b, PE_b = physics.get_bonding_interactions(self.positions, self.bond_length, self.spring_constant)
|
|
174
|
+
F_nb, PE_nb = physics.get_non_bonding_interactions(self.positions, self.epsilon, self.sigma)
|
|
175
|
+
self.velocities += (F_b + F_nb)*self.time_step / (2*self.mass)
|
|
176
|
+
self.velocities = self.velocities*a + rand.standard_normal((self.n_atoms, 3))*self.velocity_sigma*b
|
|
177
|
+
elif integrator == 'VRORV':
|
|
178
|
+
self.velocities += (F_b + F_nb)*self.time_step / (2*self.mass)
|
|
179
|
+
self.positions += self.velocities*self.time_step / 2
|
|
180
|
+
self.velocities = self.velocities*a + rand.standard_normal((self.n_atoms, 3))*self.velocity_sigma*b
|
|
181
|
+
self.positions += self.velocities*self.time_step / 2
|
|
182
|
+
F_b, PE_b = physics.get_bonding_interactions(self.positions, self.bond_length, self.spring_constant)
|
|
183
|
+
F_nb, PE_nb = physics.get_non_bonding_interactions(self.positions, self.epsilon, self.sigma)
|
|
184
|
+
self.velocities += (F_b + F_nb)*self.time_step / (2*self.mass)
|
|
185
|
+
|
|
186
|
+
##TODO make sure it is okay that velocities are not re-normalised at this point
|
|
187
|
+
KE_total = physics.get_kintetic_energy(self.velocities, self.mass)
|
|
188
|
+
T_actual = physics.get_temperature(KE_total, self.n_atoms)
|
|
189
|
+
step_data.append({
|
|
190
|
+
'step': step,
|
|
191
|
+
'T_actual': T_actual,
|
|
192
|
+
'PE_total': PE_b + PE_nb,
|
|
193
|
+
'KE_total': KE_total,
|
|
194
|
+
'F_rms': np.mean((F_b + F_nb)**2) ** 0.5,
|
|
195
|
+
'L_end_to_end': np.sum((self.positions[0] - self.positions[-1]) ** 2) ** 0.5,
|
|
196
|
+
})
|
|
197
|
+
self._logging_step(step, step_data, run_type='NVT', n_steps=n_steps, temperature=temperature)
|
|
198
|
+
|
|
199
|
+
self.step_data[self.run] = pd.DataFrame(step_data)
|
|
200
|
+
self._log_run_data(run_type='NVT', n_steps=n_steps, temperature=temperature)
|
|
201
|
+
self.dashboard.reset(self) if self.is_being_displayed else print(f'{n_steps:,} step NVT run completed')
|
|
202
|
+
|
|
203
|
+
def NVE_run(self, n_steps: int, temperature: float = None, integrator: str = 'step'):
|
|
204
|
+
'''
|
|
205
|
+
Constant number of atoms, volume, and energy (NVE) simulation. Used for modelling molecules in the
|
|
206
|
+
gas phase or finding an acceptable simulation timestep. Larger timesteps speed up the simulation
|
|
207
|
+
but will eventually break conservation of energy if too large. https://doi.org/10.1021/jp411770f
|
|
208
|
+
details different techniques for integrating the simulation forces over time.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
n_steps : int
|
|
213
|
+
Number of NVE steps to perform.
|
|
214
|
+
|
|
215
|
+
temperature: float
|
|
216
|
+
If given, resets the atom velocities to follow a boltzmann distribtuion at the
|
|
217
|
+
new tempearture. Should be given if this run is not preceded by an NVT run at
|
|
218
|
+
the desired temperature.
|
|
219
|
+
|
|
220
|
+
integrator : str
|
|
221
|
+
How forces are intergrated over time. Either 'continuous' or 'step'.
|
|
222
|
+
#TODO make all available
|
|
223
|
+
'''
|
|
224
|
+
assert integrator in ['continuous', 'step'], f'integrator {integrator} not supported'
|
|
225
|
+
self.run += 1
|
|
226
|
+
self.microstructures.append({0: pd.DataFrame(self.positions, columns=['x', 'y', 'z'])})
|
|
227
|
+
step_data = []
|
|
228
|
+
|
|
229
|
+
if temperature is not None:
|
|
230
|
+
self.target_kT = temperature * self.kB
|
|
231
|
+
self.velocity_sigma = np.sqrt(self.target_kT/self.mass) #variance of Boltzmann distrubution velocities
|
|
232
|
+
self.velocities = rand.normal(loc=0.0, scale=self.velocity_sigma, size=(self.n_atoms, 3))
|
|
233
|
+
else:
|
|
234
|
+
temperature = self.run_data.loc[self.run_data['run'] == (self.run-1), 'T'].values[0] #previous temperature
|
|
235
|
+
F_b, PE_b = physics.get_bonding_interactions(self.positions, self.bond_length, self.spring_constant)
|
|
236
|
+
F_nb, PE_nb = physics.get_non_bonding_interactions(self.positions, self.epsilon, self.sigma)
|
|
237
|
+
|
|
238
|
+
for step in range(n_steps):
|
|
239
|
+
if integrator == 'continuous':
|
|
240
|
+
average_velocities = self.velocities + (F_b + F_nb)*self.time_step / (2*self.mass)
|
|
241
|
+
self.positions += average_velocities*self.time_step
|
|
242
|
+
F_b, PE_b = physics.get_bonding_interactions(self.positions, self.bond_length, self.spring_constant)
|
|
243
|
+
F_nb, PE_nb = physics.get_non_bonding_interactions(self.positions, self.epsilon, self.sigma)
|
|
244
|
+
self.velocities = average_velocities + (F_b + F_nb)*self.time_step / (2*self.mass)
|
|
245
|
+
if integrator == 'step':
|
|
246
|
+
self.velocities += (F_b + F_nb)*self.time_step / (2*self.mass)
|
|
247
|
+
self.positions += self.velocities*self.time_step
|
|
248
|
+
F_b, PE_b = physics.get_bonding_interactions(self.positions, self.bond_length, self.spring_constant)
|
|
249
|
+
F_nb, PE_nb = physics.get_non_bonding_interactions(self.positions, self.epsilon, self.sigma)
|
|
250
|
+
self.velocities += (F_b + F_nb)*self.time_step / (2*self.mass)
|
|
251
|
+
|
|
252
|
+
KE_total = physics.get_kintetic_energy(self.velocities, self.mass)
|
|
253
|
+
step_data.append({
|
|
254
|
+
'step': step,
|
|
255
|
+
'PE_total': PE_b + PE_nb,
|
|
256
|
+
'KE_total': KE_total,
|
|
257
|
+
'F_rms': np.mean((F_b + F_nb)**2) ** 0.5,
|
|
258
|
+
'L_end_to_end': np.sum((self.positions[0] - self.positions[-1]) ** 2) ** 0.5,
|
|
259
|
+
})
|
|
260
|
+
self._logging_step(step, step_data, run_type='NVE', n_steps=n_steps, temperature=temperature)
|
|
261
|
+
|
|
262
|
+
self.step_data[self.run] = pd.DataFrame(step_data)
|
|
263
|
+
self._log_run_data(run_type='NVE', n_steps=n_steps, temperature=temperature)
|
|
264
|
+
self.dashboard.reset(self) if self.is_being_displayed else print(f'{n_steps:,} step NVE run completed')
|
|
265
|
+
|
|
266
|
+
def MMC_run(self, n_steps: int, temperature: float, random_scale: float = 0.05):
|
|
267
|
+
'''
|
|
268
|
+
Metropolis Monte Carlo (MMC) Simulation. Randomly displaces atoms by a small amount and accepts the new
|
|
269
|
+
structure if it reduces total potential energy or has a chance of accepting the new structure ∝ exp(-ΔPE)
|
|
270
|
+
if it increases total potential energy. Un-physical but useful for sampling a variety of fairly low energy
|
|
271
|
+
microstructures for thermodynamic property prediction.
|
|
272
|
+
|
|
273
|
+
Parameters
|
|
274
|
+
----------
|
|
275
|
+
n_steps : int
|
|
276
|
+
Number of MMC steps to perform.
|
|
277
|
+
|
|
278
|
+
temperature: float
|
|
279
|
+
Temperature of simulation. A higher temperature means higher thermal energy, increasing tha chance
|
|
280
|
+
of an atom moving to a new random position.
|
|
281
|
+
|
|
282
|
+
random_scale: float
|
|
283
|
+
Size limit of random displacement at each step. Will be +/- bond_length * random_scale / 2 in each direction.
|
|
284
|
+
'''
|
|
285
|
+
self.run += 1
|
|
286
|
+
self.microstructures.append({0: pd.DataFrame(self.positions, columns=['x', 'y', 'z'])})
|
|
287
|
+
step_data = []
|
|
288
|
+
|
|
289
|
+
self.target_kT = temperature * self.kB
|
|
290
|
+
self.velocity_sigma = np.sqrt(self.target_kT/self.mass) #variance of Boltzmann distrubution velocities
|
|
291
|
+
|
|
292
|
+
energy_tracker = physics.PotentialEnergyTracker(self.positions, self.epsilon, self.sigma,
|
|
293
|
+
self.bond_length, self.spring_constant)
|
|
294
|
+
PE = energy_tracker.get_total_potential_energy()
|
|
295
|
+
max_displacement_size = self.bond_length * random_scale
|
|
296
|
+
atom_indexes = rand.randint(low=0, high=self.n_atoms-1, size=n_steps)
|
|
297
|
+
displacements = rand.uniform(low=-max_displacement_size/2, high=max_displacement_size/2, size=(n_steps, 3))
|
|
298
|
+
for step in range(n_steps):
|
|
299
|
+
is_displacement_accepted = False
|
|
300
|
+
PE_change = energy_tracker.test_displacement(atom_indexes[step], displacements[step])
|
|
301
|
+
if PE_change < 0 or np.exp(-PE_change / self.target_kT) > rand.random():
|
|
302
|
+
is_displacement_accepted = True
|
|
303
|
+
energy_tracker.accept_last_displacement()
|
|
304
|
+
PE += PE_change
|
|
305
|
+
self.positions[atom_indexes[step]] += displacements[step]
|
|
306
|
+
|
|
307
|
+
step_data.append({
|
|
308
|
+
'step': step,
|
|
309
|
+
'displacement_accepted': is_displacement_accepted,
|
|
310
|
+
'PE_total': PE,
|
|
311
|
+
'L_end_to_end': np.sum((self.positions[0] - self.positions[-1]) ** 2) ** 0.5,
|
|
312
|
+
})
|
|
313
|
+
self._logging_step(step, step_data, run_type='MMC', n_steps=n_steps, temperature=temperature)
|
|
314
|
+
|
|
315
|
+
self.step_data[self.run] = pd.DataFrame(step_data)
|
|
316
|
+
self._log_run_data(run_type='MMC', n_steps=n_steps, temperature=temperature)
|
|
317
|
+
self.dashboard.reset(self) if self.is_being_displayed else print(f'{n_steps:,} step MMC run completed')
|
|
318
|
+
|
|
319
|
+
def display(self, display_interval: int = 1_000):
|
|
320
|
+
'''
|
|
321
|
+
Create an interactive dashboard that displays how the molecule's conformation and physical properties
|
|
322
|
+
change throughout the simulation. The dashboard can only be displayed in Jupyter Notebook. The dashboard
|
|
323
|
+
will update live if this method is called before a simulation run is started.
|
|
324
|
+
|
|
325
|
+
Parameters
|
|
326
|
+
----------
|
|
327
|
+
display_interval : int
|
|
328
|
+
How many steps are run between each live display update. A larger interval will significantly speed
|
|
329
|
+
up longer runs. Note MMC runs have a 5x longer interval due to their higher run speed.
|
|
330
|
+
'''
|
|
331
|
+
assert display_interval % self.microstructure_logging_interval == 0,\
|
|
332
|
+
f'please select a display interval that is a multiple of {self.microstructure_logging_interval}'
|
|
333
|
+
self.display_interval = display_interval
|
|
334
|
+
try:
|
|
335
|
+
shell = get_ipython().__class__.__name__
|
|
336
|
+
if shell == 'ZMQInteractiveShell':
|
|
337
|
+
self.is_being_displayed = True
|
|
338
|
+
self.dashboard = display.SimulationDashboard(self)
|
|
339
|
+
self.dashboard.display(self)
|
|
340
|
+
else:
|
|
341
|
+
warnings.warn(f'This functionality is only available in a Jupyter Notebook. {shell} enviroment detected')
|
|
342
|
+
except NameError:
|
|
343
|
+
warnings.warn('This functionality is only available in a Jupyter Notebook')
|
|
344
|
+
|
|
345
|
+
def save(self, path: str) -> None:
|
|
346
|
+
'''
|
|
347
|
+
Save simulation object as a btye file.
|
|
348
|
+
|
|
349
|
+
Parameters
|
|
350
|
+
----------
|
|
351
|
+
path : str
|
|
352
|
+
File location where Simulation object will be stored.
|
|
353
|
+
'''
|
|
354
|
+
with open(path, mode='wb') as file:
|
|
355
|
+
pickle.dump(self, file)
|
|
356
|
+
|
|
357
|
+
def _log_run_data(self, run_type: str, n_steps: int, temperature: float) -> None:
|
|
358
|
+
'''logs the final state of molecule after each run and stores it in the self.run_data dataframe'''
|
|
359
|
+
F_b, PE_b = physics.get_bonding_interactions(self.positions, self.bond_length, self.spring_constant)
|
|
360
|
+
F_nb, PE_nb = physics.get_non_bonding_interactions(self.positions, self.epsilon, self.sigma)
|
|
361
|
+
current_state = {
|
|
362
|
+
'run': self.run,
|
|
363
|
+
'type': run_type,
|
|
364
|
+
'n_steps': n_steps,
|
|
365
|
+
'T': temperature,
|
|
366
|
+
'KE': physics.get_kintetic_energy(self.velocities, self.mass),
|
|
367
|
+
'PE_bonding': PE_b,
|
|
368
|
+
'PE_non_bonding': PE_nb,
|
|
369
|
+
'PE_total': PE_b + PE_nb,
|
|
370
|
+
'F_rms': np.mean((F_b + F_nb)**2) ** 0.5,
|
|
371
|
+
'L_end_to_end': np.sum((self.positions[0] - self.positions[-1]) ** 2) ** 0.5,
|
|
372
|
+
}
|
|
373
|
+
self.run_data = pd.concat([self.run_data, pd.DataFrame([current_state])])
|
|
374
|
+
|
|
375
|
+
def _logging_step(self, step: int, step_data: list[dict], run_type: str, n_steps: int, temperature: float) -> None:
|
|
376
|
+
'''Saves microstructure and updates display at appropriate steps'''
|
|
377
|
+
if step % self.microstructure_logging_interval == 0:
|
|
378
|
+
self.positions -= np.mean(self.positions, axis=0, keepdims=True) #centre molecule
|
|
379
|
+
self.microstructures[self.run][step] = pd.DataFrame(self.positions, columns=['x', 'y', 'z']).copy()
|
|
380
|
+
|
|
381
|
+
if self.is_being_displayed == False:
|
|
382
|
+
return
|
|
383
|
+
display_interval = 5 * self.display_interval if run_type == 'MMC' else self.display_interval
|
|
384
|
+
if step % display_interval == 0:
|
|
385
|
+
self.step_data[self.run] = pd.DataFrame(step_data)
|
|
386
|
+
self.dashboard.live_update(self, step, run_type, n_steps, temperature)
|
|
387
|
+
|
|
388
|
+
def load_simulation(path: str) -> Simulation:
|
|
389
|
+
'''
|
|
390
|
+
Load in a simulation object from a file. For safety reasons, only load files you have created.
|
|
391
|
+
|
|
392
|
+
Parameters
|
|
393
|
+
----------
|
|
394
|
+
path : str
|
|
395
|
+
Simulation file location to be read in.
|
|
396
|
+
'''
|
|
397
|
+
with open(path, mode='rb') as file:
|
|
398
|
+
return pickle.load(file)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Vectorised functions and classes for efficiently calculating kintetic energy, temperature, spring bonding
|
|
3
|
+
interactions, and Lennard Jones long range interactions.
|
|
4
|
+
'''
|
|
5
|
+
import numpy as np
|
|
6
|
+
#compiles functions for much higher speed when called many times. Limited compatibility with numpy
|
|
7
|
+
import numba as nb
|
|
8
|
+
|
|
9
|
+
@nb.jit(nopython=True, fastmath=True)
|
|
10
|
+
def get_kintetic_energy(velocities, mass) -> np.float64:
|
|
11
|
+
'''returns total kinetic energy of all atoms'''
|
|
12
|
+
return np.sum(mass/2 * velocities**2)
|
|
13
|
+
|
|
14
|
+
@nb.jit(nopython=True, fastmath=True)
|
|
15
|
+
def get_temperature(kinetic_energy: np.float64, n_atoms: int) -> np.float64:
|
|
16
|
+
'''returns the average temperature of all the atoms'''
|
|
17
|
+
kb = 8.617333262e-05
|
|
18
|
+
return 2 * kinetic_energy / (3 * (n_atoms-1) * kb)
|
|
19
|
+
|
|
20
|
+
@nb.jit(nopython=True, fastmath=True) #fastmath increases float64 innaccuracy by 4x (1.1e-16 to 4.4e-16) which is acceptable here
|
|
21
|
+
def get_bonding_interactions(positions: np.ndarray, equilibrium_bond_length: float, spring_constant: float) \
|
|
22
|
+
-> list[np.ndarray, float]:
|
|
23
|
+
'''returns bonding forces for each atom and the total bonding potential of all atoms'''
|
|
24
|
+
bond_displacements = positions[1:] - positions[:-1]
|
|
25
|
+
bond_lengths = np.sum(bond_displacements**2, axis=1) ** 0.5
|
|
26
|
+
bond_lengths = bond_lengths.reshape(bond_lengths.shape[0], 1) #enables broadcasting in bond direction calculation
|
|
27
|
+
bond_extensions = bond_lengths - equilibrium_bond_length
|
|
28
|
+
bond_directions = bond_displacements / bond_lengths
|
|
29
|
+
|
|
30
|
+
bond_forces = -spring_constant * bond_extensions * bond_directions
|
|
31
|
+
atom_forces = np.zeros(shape=positions.shape)
|
|
32
|
+
atom_forces[:-1] -= bond_forces #each bond exerts a negative force on the left atom
|
|
33
|
+
atom_forces[1:] += bond_forces #each bond exerts a positive force on the right atom
|
|
34
|
+
total_bond_potential = np.sum(spring_constant/2 * (bond_extensions**2))
|
|
35
|
+
return atom_forces, total_bond_potential
|
|
36
|
+
|
|
37
|
+
#numpy broadcasting can be used to replace the for loop and increase speed. This however requires boolean indexing
|
|
38
|
+
#which is not supported by numba and so the proper numpy implementation actually ends up slower.
|
|
39
|
+
@nb.jit(nopython=True, fastmath=True)
|
|
40
|
+
def get_non_bonding_interactions(positions: np.ndarray, epsilon: float, sigma: float) -> list[np.ndarray, float]:
|
|
41
|
+
'''
|
|
42
|
+
returns the Lennard Jones (LJ) force for each atom and the total LJ potential of all atoms. This approximates
|
|
43
|
+
long range Van Der Valls attraction as well as the hard sphere repulsion of each atom to its more distant neighbours
|
|
44
|
+
(equilibrium LJ seperation is ~3x the equilibrium bond seperation)
|
|
45
|
+
'''
|
|
46
|
+
n_atoms = positions.shape[0]
|
|
47
|
+
forces = np.zeros((n_atoms,3))
|
|
48
|
+
potential = 0.0
|
|
49
|
+
for i in range(n_atoms-1):
|
|
50
|
+
#displacements from atom i to its 3rd nearest neighbour and beyond (LJ models long range interactions). Looks
|
|
51
|
+
#like it ignores rightmost atoms but only find the displacement between relevant pairs once for efficiency
|
|
52
|
+
displacements = positions[i+3:] - positions[i]
|
|
53
|
+
lengths = np.sum(displacements**2, axis=1) ** 0.5
|
|
54
|
+
lengths = lengths.reshape(lengths.shape[0], 1) #allows broadcasting in following calculations
|
|
55
|
+
directions = displacements / lengths
|
|
56
|
+
sixpowers = (sigma/lengths) ** 6
|
|
57
|
+
twelvepowers = sixpowers ** 2
|
|
58
|
+
potential += np.sum(epsilon * (twelvepowers - 2*sixpowers))
|
|
59
|
+
|
|
60
|
+
#forces exerted by atom i on its relevant neighbours
|
|
61
|
+
atom_forces = epsilon * 12/lengths * (twelvepowers - sixpowers) * directions
|
|
62
|
+
forces[i+3:] += atom_forces
|
|
63
|
+
forces[i] -= np.sum(atom_forces, axis=0) #equal and opposite reaction on atom
|
|
64
|
+
|
|
65
|
+
return forces, potential
|
|
66
|
+
|
|
67
|
+
@nb.jit(nopython=True, fastmath=True)
|
|
68
|
+
def get_energy_change(positions: np.ndarray, displacements: np.ndarray, lengths: np.ndarray, atom_index: int,
|
|
69
|
+
displacement: np.ndarray, epsilon: float, sigma: float, equilibrium_bond_length: float,
|
|
70
|
+
spring_constant: float) -> list[float, np.ndarray, np.ndarray, np.ndarray]:
|
|
71
|
+
'''
|
|
72
|
+
calculates only affected atom seperations to find how total potential energy changes. Really requires
|
|
73
|
+
you to draw out a mock displacement matrix and pick an atom index to understand the array indexing.
|
|
74
|
+
returns energy_change, new_positions, new_displacements, new_lengths
|
|
75
|
+
'''
|
|
76
|
+
i = atom_index
|
|
77
|
+
new_positions = positions.copy()
|
|
78
|
+
new_displacements = displacements.copy()
|
|
79
|
+
new_lengths = lengths.copy()
|
|
80
|
+
n_atoms = new_positions.shape[0]
|
|
81
|
+
energy_change = 0.0
|
|
82
|
+
|
|
83
|
+
new_positions[i] += displacement
|
|
84
|
+
new_displacements[i, i+1: ] = new_positions[i: i+1] - new_positions[i+1: ]
|
|
85
|
+
new_lengths[i, i+1: ] = np.sum(new_displacements[i, i+1: ] ** 2, axis=1) ** 0.5
|
|
86
|
+
new_displacements[:i, i] = new_positions[:i] - new_positions[i: i+1]
|
|
87
|
+
new_lengths[:i, i] = np.sum(new_displacements[:i, i] ** 2, axis=1) ** 0.5
|
|
88
|
+
|
|
89
|
+
if i != 0:
|
|
90
|
+
extension = np.abs(lengths[i-1, i] - equilibrium_bond_length)
|
|
91
|
+
new_extension = np.abs(new_lengths[i-1, i] - equilibrium_bond_length)
|
|
92
|
+
energy_change += 0.5 * spring_constant * (new_extension**2 - extension**2)
|
|
93
|
+
if i != n_atoms-1:
|
|
94
|
+
extension = np.abs(lengths[i, i+1] - equilibrium_bond_length)
|
|
95
|
+
new_extension = np.abs(new_lengths[i, i+1] - equilibrium_bond_length)
|
|
96
|
+
energy_change += 0.5 * spring_constant * (new_extension**2 - extension**2)
|
|
97
|
+
|
|
98
|
+
#Lennard Jones Potentials ignore 2 nearest neighbours of each atom
|
|
99
|
+
sixpowers = (sigma / lengths[i, i+3: ]) ** 6
|
|
100
|
+
new_sixpowers = (sigma / new_lengths[i, i+3: ]) ** 6
|
|
101
|
+
energy_change += epsilon * (np.sum(new_sixpowers**2 - 2*new_sixpowers) - np.sum(sixpowers**2 - 2*sixpowers))
|
|
102
|
+
if i > 1: #lengths[:i-2, i] returns unintended values if (i-2) is negative
|
|
103
|
+
sixpowers = (sigma / lengths[:i-2, i]) ** 6
|
|
104
|
+
new_sixpowers = (sigma / new_lengths[:i-2, i]) ** 6
|
|
105
|
+
energy_change += epsilon * (np.sum(new_sixpowers**2 - 2*new_sixpowers) - np.sum(sixpowers**2 - 2*sixpowers))
|
|
106
|
+
|
|
107
|
+
return energy_change, new_positions, new_displacements, new_lengths
|
|
108
|
+
|
|
109
|
+
class PotentialEnergyTracker():
|
|
110
|
+
'''
|
|
111
|
+
Utility class for efficiently tracking how total potential energy (bonding + Lennard Jones non-bonding) changes
|
|
112
|
+
when a single atom is displaced.
|
|
113
|
+
'''
|
|
114
|
+
|
|
115
|
+
def __init__(self, positions: np.ndarray, epsilon: float, sigma: float, equilibrium_bond_length: float,
|
|
116
|
+
spring_constant: float) -> None:
|
|
117
|
+
'calculates key distances, lengths and total potential energy'
|
|
118
|
+
self.positions = positions
|
|
119
|
+
self.epsilon = epsilon
|
|
120
|
+
self.sigma = sigma
|
|
121
|
+
self.equilibrium_bond_length = equilibrium_bond_length
|
|
122
|
+
self.spring_constant = spring_constant
|
|
123
|
+
self.n_atoms = len(positions)
|
|
124
|
+
|
|
125
|
+
# (n_atoms x n_atoms) array where array[i, j] is the displacement going from j to i
|
|
126
|
+
self.displacements = self.positions.reshape(self.n_atoms, 1, 3) - self.positions.reshape(1, self.n_atoms, 3)
|
|
127
|
+
self.lengths = np.sum(self.displacements ** 2, axis=2) ** 0.5
|
|
128
|
+
|
|
129
|
+
def get_total_potential_energy(self):
|
|
130
|
+
'''return total potential energy of molecule'''
|
|
131
|
+
_, bonding_potential = get_bonding_interactions(self.positions, self.equilibrium_bond_length, self.spring_constant)
|
|
132
|
+
_, non_bonding_potential = get_non_bonding_interactions(self.positions, self.epsilon, self.sigma)
|
|
133
|
+
return bonding_potential + non_bonding_potential
|
|
134
|
+
|
|
135
|
+
def test_displacement(self, atom_index: int, displacement: np.ndarray) -> float:
|
|
136
|
+
'''stores temporary new positions, displacements, and lengths. Returns change in potential energy'''
|
|
137
|
+
result = get_energy_change(self.positions, self.displacements, self.lengths, atom_index, displacement, self.epsilon,
|
|
138
|
+
self.sigma, self.equilibrium_bond_length, self.spring_constant)
|
|
139
|
+
energy_change, self.new_positions, self.new_displacements, self.new_lengths = result
|
|
140
|
+
return energy_change
|
|
141
|
+
|
|
142
|
+
def accept_last_displacement(self) -> None:
|
|
143
|
+
'''replaces internal positions, displacements, and lengths with values from last test displacement'''
|
|
144
|
+
self.positions = self.new_positions
|
|
145
|
+
self.displacements = self.new_displacements
|
|
146
|
+
self.lengths = self.new_lengths
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: imperial-materials-simulation
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Molecular simulation tool made for the undergraduate materials science and engineering theory and simulation module at Imperial College London
|
|
5
|
+
Home-page: https://github.com/AyhamSaffar/imperial_materials_simulation
|
|
6
|
+
Author: Ayham Al-Saffar
|
|
7
|
+
Requires-Python: ~=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
This is a test to make sure this comes up
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
imperial_materials_simulation/__init__.py
|
|
4
|
+
imperial_materials_simulation/display.py
|
|
5
|
+
imperial_materials_simulation/main.py
|
|
6
|
+
imperial_materials_simulation/physics.py
|
|
7
|
+
imperial_materials_simulation.egg-info/PKG-INFO
|
|
8
|
+
imperial_materials_simulation.egg-info/SOURCES.txt
|
|
9
|
+
imperial_materials_simulation.egg-info/dependency_links.txt
|
|
10
|
+
imperial_materials_simulation.egg-info/requires.txt
|
|
11
|
+
imperial_materials_simulation.egg-info/top_level.txt
|
imperial_materials_simulation-0.0.1/imperial_materials_simulation.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
imperial_materials_simulation
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#python setup.py sdist bdist_wheel
|
|
2
|
+
#twine upload --skip-existing dist/*
|
|
3
|
+
#twine upload --skip-existing --repository testpypi dist/*
|
|
4
|
+
|
|
5
|
+
from setuptools import setup, find_packages
|
|
6
|
+
|
|
7
|
+
with open('README.md', mode='r') as file:
|
|
8
|
+
description = file.read()
|
|
9
|
+
|
|
10
|
+
setup(
|
|
11
|
+
name = 'imperial_materials_simulation',
|
|
12
|
+
version = '0.0.1',
|
|
13
|
+
description = 'Molecular simulation tool made for the undergraduate materials science and engineering theory and simulation module at Imperial College London',
|
|
14
|
+
author = 'Ayham Al-Saffar',
|
|
15
|
+
url = 'https://github.com/AyhamSaffar/imperial_materials_simulation',
|
|
16
|
+
packages = find_packages(),
|
|
17
|
+
python_requires = '~=3.10',
|
|
18
|
+
install_requires = [
|
|
19
|
+
'ipykernel',
|
|
20
|
+
'ipympl',
|
|
21
|
+
'ipywidgets',
|
|
22
|
+
'matplotlib',
|
|
23
|
+
'numba>=0.60.0',
|
|
24
|
+
'numpy',
|
|
25
|
+
'pandas',
|
|
26
|
+
'py3dmol',
|
|
27
|
+
'scipy',
|
|
28
|
+
],
|
|
29
|
+
long_description = description,
|
|
30
|
+
long_description_content_type = 'text/markdown',
|
|
31
|
+
)
|