ParM  parm
A molecular dynamics library
polymer.py
1 # -*- coding: utf-8 -*-
2 
3 # A basic example of a polymer simulation.
4 
5 # Statistics such as energy, (instantaneous) temperature, and (instantaneous) pressure are
6 # output to "polymer-example.npz", and coordinates are output to "polymer-example.xyz".
7 # These xyz files can be read by VMD.
8 
9 # This simulation demonstrates usage of simcli.py, as well as the XYZreader.
10 
11 # This was written for Python 3.4+; it may take some small effort to use a lower version of Python.
12 
13 # ------------------------------------------------------------------------------
14 # std library
15 import sys
16 import argparse
17 
18 # Third-party imports
19 import numpy as np
20 
21 # pyparm imports
22 import pyparm.d3 as sim
23 import pyparm.util as util
24 # ------------------------------------------------------------------------------
25 # Parameters
26 parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
27 
28 scigroup = parser.add_argument_group('Scientific Parameters')
29 scigroup.add_argument('-T', '--temp', type=float, default=0.01, help='Simulation temperature')
30 scigroup.add_argument('-N', '--npoly', type=int, default=20, help='Number of polymers')
31 scigroup.add_argument('-n', '--perpoly', type=int, default=10,
32  help='Number of beads per polymer')
33 scigroup.add_argument('-d', '--density', type=float, default=0.05,
34  help='Number of polymers per unit volume')
35 scigroup.add_argument('-a', '--angle', type=float, default=120,
36  help='Bond angle, in degrees, with 180 being a straight chain')
37 scigroup.add_argument('-t', '--time', type=float, default=1000.0,
38  help='Number of time units to run the simulation')
39 
40 simgroup = parser.add_argument_group('Simulation Parameters')
41 simgroup.add_argument('--damping', type=float, default=1.0,
42  help='The damping coefficient of the integrator, related to viscosity')
43 simgroup.add_argument('--dt', type=float, default=1e-2, help='Timestep')
44 simgroup.add_argument('--mass', type=float, default=1.0, help='Mass per bead')
45 simgroup.add_argument('--width', type=float, default=1.0, help='Width of beads')
46 simgroup.add_argument('--anglek', type=float, default=100.0, help='Strength of bond angle')
47 simgroup.add_argument('--bondk', type=float, default=100.0, help='Strength of bond potential')
48 
49 outgroup = parser.add_argument_group('Output Parameters')
50 outgroup.add_argument('--xyz', default='polymer-example.xyz')
51 outgroup.add_argument('--stats', default='polymer-example.npz')
52 parser.add_argument('--statt', type=float, default=10.0,
53  help="how often to save statistics (time units)")
54 parser.add_argument('--xyzt', type=float, default=1.0,
55  help="how often to output a frame to the XYZ file (time units)")
56 outgroup.add_argument('--printn', type=int, default=200)
57 
58 opts = parser.parse_args()
59 
60 # ------------------------------------------------------------------------------
61 # Setting up the simulation
62 L = (opts.npoly / opts.density)**(1.0/3.0)
63 N = opts.npoly * opts.perpoly
64 sigmas = [1.]*N
65 
66 box = sim.OriginBox(L)
67 atoms = sim.AtomVec([1.]*N)
68 # the NeighborList, for keeping track of what atoms are near what other atoms
69 neighbors = sim.NeighborList(box, atoms, 0.4)
70 repulse = sim.Repulsion(atoms, neighbors)
71 bonds = sim.BondPairs()
72 angles = sim.AngleTriples()
73 
74 # ------------------------------------------------------------------------------
75 # Initial Conditions
76 # Now we have created our Atom, but we need to add our atoms to it. We do that
77 # in a way that prevents overlap
78 
79 print('Placing atoms...', end=' ')
80 E0 = 0
81 angle = (opts.angle % 180) * np.pi/180
82 lasta = None
83 preva = None
84 for n, a, s in zip(range(N), atoms, sigmas):
85  E = E0 + 10
86  a.v = sim.rand_vec() # from a gaussian distribution
87  repulse.add(sim.EpsSigExpAtom(a, 1.0, s, 2.0))
88  if n % opts.perpoly != 0:
89  bonds.add(opts.bondk, s, lasta, a)
90  neighbors.ignore(lasta, a)
91  if n % opts.perpoly > 1:
92  angles.add(opts.anglek, angle, preva, lasta, a)
93  neighbors.ignore(preva, a)
94  while E > E0 + 0.1:
95  if n % opts.perpoly > 1:
96  dx = sim.rand_vec()
97  lastdx = lasta.x - preva.x
98  lastdx /= np.linalg.norm(lastdx)
99  perp = np.cross(dx, lastdx)
100  perp /= np.linalg.norm(perp)
101  dx = (lastdx * np.cos(np.pi - angle) +
102  perp * np.sin(np.pi - angle))
103  a.x = lasta.x + dx
104  elif n % opts.perpoly > 0:
105  dx = sim.rand_vec()
106  dx /= np.linalg.norm(dx)
107  a.x = lasta.x + dx
108  else:
109  a.x = box.rand_loc()
110  neighbors.update_list()
111  E = repulse.energy(box) + bonds.energy(box) + angles.energy(box)
112  E0 = E
113  lastx = a.x
114  preva = lasta
115  lasta = a
116  if n % 10 == 0:
117  print(N - n, end=', ')
118  sys.stdout.flush()
119 print('Done.')
120 
121 # the integrator
122 # We use a simple velocity-verlet integrator, which is time-reversible and
123 # NVE ensemble
124 # i.e., it preserves number of atoms, volume of box, and energy
125 collec = sim.CollectionSol(box, atoms, opts.dt, opts.damping, opts.temp,
126  [repulse, bonds, angles], [neighbors])
127 
128 # subtract center-of-mass velocity from all particles
129 collec.reset_com_velocity()
130 # scale all velocities to get an instantaneous temperature T = T0, at least at the beginning
131 collec.scale_velocities_to_temp(opts.temp)
132 
133 ################################################################################
134 # Data Analysis
135 
136 data_functions = {
137  'E': collec.energy,
138  'T': collec.temp,
139  'U': lambda: repulse.energy(box),
140  'P': collec.pressure
141 }
142 
143 data_arrays = {k: [] for k in data_functions}
144 data_arrays['t'] = []
145 
146 
147 def take_data(time):
148  """Take each measurement in data_functions at time 'time', and store it in
149  data_arrays"""
150  data_arrays['t'].append(time)
151  for k, f in data_functions.items():
152  data_arrays[k].append(f())
153 
154 
155 def write_data():
156  np.savez_compressed(opts.stats, **data_arrays)
157 
158 # ------------------------------------------------------------------------------
159 # XYZ file
160 
161 
162 def write_xyz(time):
163  # print the current line to file
164  with open(opts.xyz, 'a') as f:
165  print(N, file=f)
166  # xyz format for VMD requires a line here, and ignores it; I put the time here.
167  print(time, file=f)
168  for a in atoms:
169  x = box.diff(a.x, sim.vec())
170  print('C', *x, file=f)
171 
172 # empty out the file
173 with open(opts.xyz, 'w') as f:
174  f.truncate()
175 
176 # ------------------------------------------------------------------------------
177 # TCL file
178 # this is just to tell VMD to show the box and show the atoms as the right size.
179 # Not necessary if you're not using VMD for visualization.
180 
181 tcl_str = """\
182 mol modstyle 0 0 VDW 1 32
183 mol rep VDW 1 20
184 
185 set cell [pbc set {{{L} {L} {L}}} -all];
186 pbc box -toggle -center origin -color red;
187 set natoms [atomselect 0 "name C";];
188 $natoms set radius {r};""".format(L=L, r=sigmas[0]/2.0)
189 
190 with open(opts.xyz[:-4] + '.tcl', 'w') as f:
191  print(tcl_str, file=f)
192 
193 xyz_m = 0
194 data_m = 0
195 print_m = 1
196 
197 # for tracking and printing our progress
198 progress = util.Progress(opts.time)
199 total_steps = int(opts.time / opts.dt + 0.5)
200 
201 for step in range(total_steps+1):
202  if step > 0:
203  collec.timestep()
204  time = step*opts.dt
205 
206  if time > xyz_m * opts.xyzt:
207  write_xyz(time)
208  xyz_m += 1
209 
210  if time > data_m * opts.statt:
211  take_data(time)
212  write_data()
213  data_m += 1
214 
215  if time > print_m * opts.time / opts.printn:
216  print(progress.eta_str(time), 'T: %6.2f' % collec.temp())
217  print_m += 1
218 
219 print("Done.")