Skip to content

Commit 717f020

Browse files
committed
improved units managments
1 parent ee3c275 commit 717f020

File tree

8 files changed

+360
-237
lines changed

8 files changed

+360
-237
lines changed

docs/source/chapters/chapter1.rst

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
.. _chapter1-label:
22

3-
Start coding
3+
Start Coding
44
============
55

6+
Let's start with the Python script. During this first chapter, some Python
7+
files will be created and filled in a minimal fashion. At the end of this
8+
chapter, a small test will be set up to ensure that the files were correctly
9+
created.
10+
611
Presentation
712
------------
813

@@ -38,7 +43,8 @@ containing either Python functions or classes:
3843
- *Prepare* class: Methods for preparing the non-dimensionalization of the
3944
units
4045
* - *Utilities.py*
41-
- *Utilities* class: General-purpose methods, inherited by all other classes
46+
- *Utilities* class: General-purpose methods, inherited by all other
47+
classes
4248
* - *InitializeSimulation.py*
4349
- *InitializeSimulation* class: Methods necessary to set up the system and
4450
prepare the simulation, inherited by all the classes below
@@ -54,21 +60,27 @@ containing either Python functions or classes:
5460
- Functions for performing specific measurements on the system
5561
* - *potentials.py*
5662
- Functions for calculating the potentials and forces between atoms
57-
* - *tools.py*
63+
* - *logger.py*
5864
- Functions for outputting data into text files
65+
* - *dumper.py*
66+
- Functions for outputting data into trajectory files for visualization
67+
* - *reader.py*
68+
- Functions for importing data from text files
5969

70+
Some of these files are created in this chapter; others will be created later
71+
on. All of these files must be created within the same folder.
6072

61-
Potential for inter-atomic interaction
73+
Potential for Inter-Atomic Interaction
6274
--------------------------------------
6375

6476
In molecular simulations, potential functions are used to mimic the interaction
6577
between atoms. Although more complicated options exist, potentials are usually
6678
defined as functions of the distance :math:`r` between two atoms.
6779

68-
Within a dedicated folder, create the first file named *potentials.py*. This
69-
file will contain a function called *potentials*. Two types of potential can
70-
be returned by this function: the Lennard-Jones potential (LJ), and the
71-
hard-sphere potential.
80+
Create the first file named *potentials.py*. This file will contain a function
81+
called *potentials*. For the moment, the only potential that can be returned by
82+
this function is the Lennard-Jones potential (LJ). This may change in the
83+
future.
7284

7385
Copy the following lines into *potentials.py*:
7486

@@ -84,44 +96,33 @@ Copy the following lines into *potentials.py*:
8496
return 48 * epsilon * ((sigma / r) ** 12 - 0.5 * (sigma / r) ** 6) / r
8597
else:
8698
return 4 * epsilon * ((sigma / r) ** 12 - (sigma / r) ** 6)
87-
elif potential_type == "Hard-Sphere":
88-
if derivative:
89-
# Derivative is not defined for Hard-Sphere potential.
90-
# --> return 0
91-
return np.zeros(len(r))
92-
else:
93-
return np.where(r > sigma, 0, 1000)
9499
else:
95100
raise ValueError(f"Unknown potential type: {potential_type}")
96101
97102
.. label:: end_potentials_class
98103

99-
The hard-sphere potential either returns a value of 0 when the distance between
100-
the two particles is larger than the parameter, :math:`r > \sigma`, or 1000 when
101-
:math:`r < \sigma`. The value of *1000* was chosen to be large enough to ensure
102-
that any Monte Carlo move that would lead to the two particles to overlap will
103-
be rejected.
104-
105-
In the case of the LJ potential, depending on the value of the optional
106-
argument *derivative*, which can be either *False* or *True*, the *LJ_potential*
107-
function will return the force:
104+
Depending on the value of the optional argument *derivative*, which can be
105+
either *False* or *True*, this function returns the derivative of the potential,
106+
i.e., the force, :math:`F_\text{LJ} = - \mathrm{d} U_\text{LJ} / \mathrm{d} r`:
108107

109108
.. math::
110109
111-
F_\text{LJ} = 48 \dfrac{\epsilon}{r} \left[ \left( \frac{\sigma}{r} \right)^{12} - \frac{1}{2} \left( \frac{\sigma}{r} \right)^6 \right],
110+
F_\text{LJ} = 48 \dfrac{\epsilon}{r} \left[ \left( \frac{\sigma}{r} \right)^{12}
111+
- \frac{1}{2} \left( \frac{\sigma}{r} \right)^6 \right],
112112
113113
or the potential energy:
114114

115115
.. math::
116116
117-
U_\text{LJ} = 4 \epsilon \left[ \left( \frac{\sigma}{r} \right)^{12} - \left( \frac{\sigma}{r} \right)^6 \right].
117+
U_\text{LJ} = 4 \epsilon \left[ \left( \frac{\sigma}{r} \right)^{12}
118+
- \left( \frac{\sigma}{r} \right)^6 \right].
118119
119120
Create the Classes
120121
------------------
121122

122-
Let's create the files with the minimal information about the classes and
123-
their inheritance. The classes will be developed progressively in the
124-
following chapters.
123+
Let's create the files with some minimal details about the classes and their
124+
inheritance. The classes will be developed progressively in the following
125+
chapters.
125126

126127
The first class is the *Prepare* class, which will be used for the
127128
nondimensionalization of the parameters. In the same folder as *potentials.py*,
@@ -157,8 +158,8 @@ copy the following lines:
157158
158159
.. label:: end_Utilities_class
159160

160-
The line *from potentials import LJ_potential* is used to import the
161-
*LJ_potential* function.
161+
The line *from potentials import potentials* is used to import the
162+
previously created *potentials* function.
162163

163164
Within the *InitializeSimulation.py* file, copy the following lines:
164165

@@ -168,9 +169,10 @@ Within the *InitializeSimulation.py* file, copy the following lines:
168169
169170
import numpy as np
170171
from Prepare import Prepare
172+
from Utilities import Utilities
171173
172174
173-
class InitializeSimulation(Prepare):
175+
class InitializeSimulation(Prepare, Utilities):
174176
def __init__(self,
175177
*args,
176178
**kwargs,
@@ -180,7 +182,8 @@ Within the *InitializeSimulation.py* file, copy the following lines:
180182
.. label:: end_InitializeSimulation_class
181183

182184
The *InitializeSimulation* class inherits from the previously created
183-
*Prepare* class. Additionally, we anticipate that *NumPy* will be required.
185+
*Prepare* and Utilities classes. Additionally, we anticipate that *NumPy* will
186+
be required.
184187

185188
Within the *Measurements.py* file, copy the following lines:
186189

@@ -189,10 +192,9 @@ Within the *Measurements.py* file, copy the following lines:
189192
.. code-block:: python
190193
191194
from InitializeSimulation import InitializeSimulation
192-
from Utilities import Utilities
193195
194196
195-
class Measurements(InitializeSimulation, Utilities):
197+
class Measurements(InitializeSimulation):
196198
def __init__(self,
197199
*args,
198200
**kwargs):
@@ -206,7 +208,9 @@ The *Measurements* class inherits both the *InitializeSimulation* and
206208
Finally, let us create the three remaining classes, named *MinimizeEnergy*,
207209
*MonteCarlo*, and *MolecularDynamics*. Each of these three classes inherits
208210
from the *Measurements* class, and thus from the classes inherited by
209-
*Measurements*. Within the *MinimizeEnergy.py* file, copy the following lines:
211+
*Measurements*.
212+
213+
Within the *MinimizeEnergy.py* file, copy the following lines:
210214

211215
.. label:: start_MinimizeEnergy_class
212216

@@ -317,7 +321,7 @@ any *AssertionError*:
317321
Utilities does not inherit from MonteCarlo, as expected
318322
MonteCarlo correctly inherits from Utilities
319323
320-
Alternatively, this test can also be launched using Pytest by typing in a terminal:
324+
Alternatively, this test can also be launched using *Pytest* by typing in a terminal:
321325

322326
.. code-block:: bash
323327

docs/source/chapters/chapter2.rst

Lines changed: 68 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,15 @@ Modify the *Prepare* class as follows:
9595
9696
class Prepare:
9797
def __init__(self,
98-
number_atoms=[10], # List - no unit
99-
epsilon=[0.1], # List - Kcal/mol
100-
sigma=[3], # List - Angstrom
101-
atom_mass=[10], # List - g/mol
98+
ureg, # Pint unit registry
99+
number_atoms, # List - no unit
100+
epsilon, # List - Kcal/mol
101+
sigma, # List - Angstrom
102+
atom_mass, # List - g/mol
102103
potential_type="Lennard-Jones",
103104
*args,
104105
**kwargs):
106+
self.ureg = ureg
105107
self.number_atoms = number_atoms
106108
self.epsilon = epsilon
107109
self.sigma = sigma
@@ -116,7 +118,7 @@ Here, the four lists *number_atoms* :math:`N`, *epsilon* :math:`\epsilon`,
116118
:math:`10`, :math:`0.1~\text{[Kcal/mol]}`, :math:`3~\text{[Å]}`, and
117119
:math:`10~\text{[g/mol]}`, respectively.
118120

119-
The type of potential is also specified, with Lennard-Jones being closen as
121+
The type of potential is also specified, with Lennard-Jones being chosen as
120122
the default option.
121123

122124
All the parameters are assigned to *self*, allowing other methods to access
@@ -136,25 +138,33 @@ unit system to the *LJ* unit system:
136138
137139
def calculate_LJunits_prefactors(self):
138140
"""Calculate the Lennard-Jones units prefactors."""
139-
# Define the reference distance, energy, and mass
140-
self.reference_distance = self.sigma[0] # Angstrom
141-
self.reference_energy = self.epsilon[0] # kcal/mol
142-
self.reference_mass = self.atom_mass[0] # g/mol
143-
144-
# Calculate the prefactor for the time
145-
mass_kg = self.atom_mass[0]/cst.kilo/cst.Avogadro # kg
146-
epsilon_J = self.epsilon[0]*cst.calorie*cst.kilo/cst.Avogadro # J
147-
sigma_m = self.sigma[0]*cst.angstrom # m
148-
time_s = np.sqrt(mass_kg*sigma_m**2/epsilon_J) # s
149-
self.reference_time = time_s / cst.femto # fs
150-
151-
# Calculate the prefactor for the temperature
141+
# First define constants
152142
kB = cst.Boltzmann*cst.Avogadro/cst.calorie/cst.kilo # kcal/mol/K
153-
self.reference_temperature = self.epsilon[0]/kB # K
154-
155-
# Calculate the prefactor for the pressure
156-
pressure_pa = epsilon_J/sigma_m**3 # Pa
157-
self.reference_pressure = pressure_pa/cst.atm # atm
143+
kB *= self.ureg.kcal/self.ureg.mol/self.ureg.kelvin
144+
Na = cst.Avogadro/self.ureg.mol
145+
# Define the reference distance, energy, and mass
146+
self.ref_length = self.sigma[0] # Angstrom
147+
self.ref_energy = self.epsilon[0] # kcal/mol
148+
self.ref_mass = self.atom_mass[0] # g/mol
149+
# Optional: assert that units were correctly provided by users
150+
assert self.ref_length.units == self.ureg.angstrom, \
151+
f"Error: Provided sigma has wrong units, should be angstrom"
152+
assert self.ref_energy.units == self.ureg.kcal/self.ureg.mol, \
153+
f"Error: Provided epsilon has wrong units, should be kcal/mol"
154+
assert self.ref_mass.units == self.ureg.g/self.ureg.mol, \
155+
f"Error: Provided mass has wrong units, should be g/mol"
156+
# Calculate the prefactor for the time (in femtosecond)
157+
self.ref_time = np.sqrt(self.ref_mass \
158+
*self.ref_length**2/self.ref_energy).to(self.ureg.femtosecond)
159+
# Calculate the prefactor for the temperature (in Kelvin)
160+
self.ref_temperature = self.ref_energy/kB # Kelvin
161+
# Calculate the prefactor for the pressure (in Atmosphere)
162+
self.ref_pressure = (self.ref_energy \
163+
/self.ref_length**3/Na).to(self.ureg.atmosphere)
164+
# Regroup all the reference quantities in list, for practicality
165+
self.ref_quantities = [self.ref_length, self.ref_energy,
166+
self.ref_mass, self.ref_time, self.ref_pressure]
167+
self.ref_units = [ref.units for ref in self.ref_quantities]
158168
159169
.. label:: end_Prepare_class
160170

@@ -192,23 +202,26 @@ Let us take advantage of the calculated reference values and normalize the
192202
three inputs of the *Prepare* class that have physical dimensions, i.e.,
193203
*epsilon*, *sigma*, and *atom_mass*.
194204

195-
Create a new method called *nondimensionalize_units_0* within the *Prepare*
205+
Create a new method called *nondimensionalize_units* within the *Prepare*
196206
class:
197207

198208
.. label:: start_Prepare_class
199209

200210
.. code-block:: python
201211
202-
def nondimensionalize_units_0(self):
203-
# Normalize LJ properties
204-
epsilon, sigma, atom_mass = [], [], []
205-
for e0, s0, m0 in zip(self.epsilon, self.sigma, self.atom_mass):
206-
epsilon.append(e0/self.reference_energy)
207-
sigma.append(s0/self.reference_distance)
208-
atom_mass.append(m0/self.reference_mass)
209-
self.epsilon = epsilon
210-
self.sigma = sigma
211-
self.atom_mass = atom_mass
212+
def nondimensionalize_units(self, quantities_to_normalise):
213+
for quantity in quantities_to_normalise:
214+
if isinstance(quantity, list):
215+
for i, element in enumerate(quantity):
216+
assert element.units in self.ref_units, \
217+
f"Error: Units not part of the reference units"
218+
ref_value = self.ref_quantities[self.ref_units.index(element.units)]
219+
quantity[i] = element/ref_value
220+
assert quantity[i].units == self.ureg.dimensionless, \
221+
f"Error: Quantities are not properly nondimensionalized"
222+
quantity[i] = quantity[i].magnitude # get rid of ureg
223+
else:
224+
print("NON ANTICIPATED!")
212225
213226
.. label:: end_Prepare_class
214227

@@ -218,7 +231,7 @@ will be used to nondimensionalize units in future classes. We anticipate that
218231
each element is normalized with the corresponding reference value. The
219232
*zip()* function allows us to loop over all three lists at once.
220233

221-
Let us also call the *nondimensionalize_units_0* from the *__init__()* method
234+
Let us also call the *nondimensionalize_units* from the *__init__()* method
222235
of the *Prepare* class:
223236

224237
.. label:: start_Prepare_class
@@ -228,7 +241,7 @@ of the *Prepare* class:
228241
def __init__(self,
229242
(...)
230243
self.calculate_LJunits_prefactors()
231-
self.nondimensionalize_units_0()
244+
self.nondimensionalize_units([self.epsilon, self.sigma, self.atom_mass])
232245
233246
.. label:: end_Prepare_class
234247

@@ -288,7 +301,7 @@ Let us call the *identify_atom_properties* from the *__init__()* method:
288301
289302
def __init__(self,
290303
(...)
291-
self.nondimensionalize_units_0()
304+
self.nondimensionalize_units([self.epsilon, self.sigma, self.atom_mass])
292305
self.identify_atom_properties()
293306
294307
.. label:: end_Prepare_class
@@ -306,13 +319,25 @@ type 1, and 3 atoms of type 2:
306319
307320
import numpy as np
308321
from Prepare import Prepare
309-
310-
# Initialize the Prepare object
322+
from pint import UnitRegistry
323+
ureg = UnitRegistry()
324+
325+
# Define atom number of each group
326+
nmb_1, nmb_2= [2, 3]
327+
# Define LJ parameters (sigma)
328+
sig_1, sig_2 = [3, 4]*ureg.angstrom
329+
# Define LJ parameters (epsilon)
330+
eps_1, eps_2 = [0.2, 0.4]*ureg.kcal/ureg.mol
331+
# Define atom mass
332+
mss_1, mss_2 = [10, 20]*ureg.gram/ureg.mol
333+
334+
# Initialize the prepare object
311335
prep = Prepare(
312-
number_atoms=[2, 3],
313-
epsilon=[0.2, 0.4], # kcal/mol
314-
sigma=[3, 4], # A
315-
atom_mass=[10, 20], # g/mol
336+
ureg = ureg,
337+
number_atoms=[nmb_1, nmb_2],
338+
epsilon=[eps_1, eps_2], # kcal/mol
339+
sigma=[sig_1, sig_2], # A
340+
atom_mass=[mss_1, mss_2], # g/mol
316341
)
317342
318343
# Test function using pytest

0 commit comments

Comments
 (0)