From 9d824f02af8914e708213b5cbb081ee0f9a0426e Mon Sep 17 00:00:00 2001 From: maxdolan Date: Wed, 10 Jul 2024 22:29:04 +0100 Subject: [PATCH 01/18] added different particle types exclusive interactions to md --- pylj/md.py | 9 +++-- pylj/pairwise.py | 70 +++++++++++++++++++++++++++++-------- pylj/sample.py | 18 +++++----- pylj/tests/test_md.py | 2 +- pylj/tests/test_pairwise.py | 6 ++-- pylj/util.py | 16 ++++++++- 6 files changed, 92 insertions(+), 29 deletions(-) diff --git a/pylj/md.py b/pylj/md.py index 86e9268..1ecb851 100644 --- a/pylj/md.py +++ b/pylj/md.py @@ -10,8 +10,9 @@ def initialise( init_conf, timestep_length=1e-14, mass=39.948, - constants=[1.363e-134, 9.273e-78], + constants=[[1.363e-134, 9.273e-78]], forcefield=ff.lennard_jones, + mixing = False ): """Initialise the particle positions (this can be either as a square or random arrangement), velocities (based on the temperature defined, and @@ -54,6 +55,7 @@ def initialise( mass, init_conf=init_conf, timestep_length=timestep_length, + mixing = mixing ) v = np.random.rand(system.particles.size, 2, 12) v = np.sum(v, axis=2) - 6.0 @@ -78,7 +80,7 @@ def initialize( #Jit tag here had to be removed def velocity_verlet( - particles, timestep_length, box_length, cut_off, constants, forcefield, mass + particles, timestep_length, box_length, cut_off, constants, forcefield, mass, type_identifiers ): """Uses the Velocity-Verlet integrator to move forward in time. The Updates the particles positions and velocities in terms of the Velocity @@ -112,7 +114,7 @@ def velocity_verlet( xacceleration_store = list(particles["xacceleration"]) yacceleration_store = list(particles["yacceleration"]) particles, distances, forces, energies = heavy.compute_force( - particles, box_length, cut_off, constants, forcefield, mass + particles, box_length, cut_off, constants, forcefield, mass, type_identifiers ) [particles["xvelocity"], particles["yvelocity"]] = update_velocities( [particles["xvelocity"], particles["yvelocity"]], @@ -153,6 +155,7 @@ def sample(particles, box_length, initial_particles, system): system.cut_off, system.constants, system.forcefield, + system.type_identifiers ) msd_new = calculate_msd(particles, initial_particles, box_length) system.pressure_sample = np.append(system.pressure_sample, pressure_new) diff --git a/pylj/pairwise.py b/pylj/pairwise.py index 72f2f60..17aac7f 100644 --- a/pylj/pairwise.py +++ b/pylj/pairwise.py @@ -3,7 +3,7 @@ from pylj import pairwise as heavy #Jit tag here had to be removed -def compute_force(particles, box_length, cut_off, constants, forcefield, mass): +def compute_force(particles, box_length, cut_off, constants, forcefield, mass, type_identifiers): r"""Calculates the forces and therefore the accelerations on each of the particles in the simulation. Parameters @@ -39,7 +39,6 @@ def compute_force(particles, box_length, cut_off, constants, forcefield, mass): (particles["xacceleration"].size - 1) * particles["xacceleration"].size / 2 ) forces = np.zeros(pairs) - distances = np.zeros(pairs) energies = np.zeros(pairs) atomic_mass_unit = 1.660539e-27 # kilograms mass_amu = mass # amu @@ -47,9 +46,16 @@ def compute_force(particles, box_length, cut_off, constants, forcefield, mass): distances, dx, dy = heavy.dist( particles["xposition"], particles["yposition"], box_length ) - ff = forcefield(constants) - forces = ff.force(distances) - energies = ff.energy(distances) + for type, constants in enumerate(constants): + identifier = type_identifiers[type] + ff = forcefield(constants) + type_distances = distances * create_dist_identifiers(identifier) + type_forces = ff.force(type_distances) + type_energies = ff.energy(distances) + type_forces = np.nan_to_num(type_forces) + type_energies = np.nan_to_num(type_energies) + forces+=type_forces + energies+=type_energies forces[np.where(distances > cut_off)] = 0.0 energies[np.where(distances > cut_off)] = 0.0 particles = update_accelerations(particles, forces, mass_kg, dx, dy, distances) @@ -95,11 +101,12 @@ def update_accelerations(particles, f, m, dx, dy, dr): k = 0 for i in range(0, particles.size - 1): for j in range(i + 1, particles.size): - particles["xacceleration"][i] += second_law(f[k], m, dx[k], dr[k]) - particles["yacceleration"][i] += second_law(f[k], m, dy[k], dr[k]) - particles["xacceleration"][j] -= second_law(f[k], m, dx[k], dr[k]) - particles["yacceleration"][j] -= second_law(f[k], m, dy[k], dr[k]) + particles["xacceleration"][i] += second_law(f[k], m, dx[k], dr[k]) if f[k]!=0 else 0 + particles["yacceleration"][i] += second_law(f[k], m, dy[k], dr[k]) if f[k]!=0 else 0 + particles["xacceleration"][j] -= second_law(f[k], m, dx[k], dr[k]) if f[k]!=0 else 0 + particles["yacceleration"][j] -= second_law(f[k], m, dy[k], dr[k]) if f[k]!=0 else 0 k += 1 + return particles @@ -171,7 +178,7 @@ def lennard_jones_force(A, B, dr): float: The force between the two particles. """ - print( + xacceleration( "pairwise.lennard_jones_energy has been deprecated, please use " "forcefields.lennard_jones with force=True instead" ) @@ -218,7 +225,7 @@ def compute_energy(particles, box_length, cut_off, constants, forcefield): def calculate_pressure( - particles, box_length, temperature, cut_off, constants, forcefield + particles, box_length, temperature, cut_off, constants, forcefield, type_identifiers ): r"""Calculates the instantaneous pressure of the simulation cell, found with the following relationship: @@ -248,9 +255,19 @@ def calculate_pressure( """ distances, dx, dy = heavy.dist( particles["xposition"], particles["yposition"], box_length - ) - ff = forcefield(constants) - forces = ff.force(distances) + ) + forces = np.zeros(len(distances)) + energies = np.zeros(len(distances)) + for type, constants in enumerate(constants): + identifier = type_identifiers[type] + ff = forcefield(constants) + type_distances = distances * create_dist_identifiers(identifier) + type_forces = ff.force(type_distances) + type_energies = ff.energy(distances) + type_forces = np.nan_to_num(type_forces) + type_energies = np.nan_to_num(type_energies) + forces+=type_forces + energies+=type_energies forces[np.where(distances > cut_off)] = 0.0 pres = np.sum(forces * distances) boltzmann_constant = 1.3806e-23 # joules / kelvin @@ -345,3 +362,28 @@ def pbc_correction(position, cell): if np.abs(position) > 0.5 * cell: position *= 1 - cell / np.abs(position) return position + +def create_dist_identifiers(type_identifier): + ''' + Creates correct distance identifier matrix for particular type + of particle + Parameters + ------- + type identifiers: + the identifier array listing 1 for particles of that type + or 0 for particles of a different type + Returns + ------- + distances: float, array_like + the distance identifier for interactions between each particle + of that type, or 0 for interactions involving particles of a + different type + + ''' + distance_type_identifier = np.array([]) + for index in range(len(type_identifier)): + if type_identifier[index]: + distance_type_identifier = np.append(distance_type_identifier,type_identifier[index+1:]) + else: + distance_type_identifier = np.append(distance_type_identifier,np.zeros(len(type_identifier[index+1:]))) + return distance_type_identifier \ No newline at end of file diff --git a/pylj/sample.py b/pylj/sample.py index 3ad023e..ebf9f91 100644 --- a/pylj/sample.py +++ b/pylj/sample.py @@ -494,12 +494,13 @@ def setup_cellview(ax, system, scale=1): # pragma: no cover scale: float (optional) The amount by which the particle size should be scaled down. """ - xpos = system.particles["xposition"] - ypos = system.particles["yposition"] # These numbers are chosen to scale the size of the particles nicely to # allow the particles to appear to interact appropriately mk = 6.00555e-8 / (system.box_length - 2.2727e-10) - 1e-10 / scale - ax.plot(xpos, ypos, "o", markersize=mk, markeredgecolor="black", color="#34a5daff") + for type in system.type_identifiers: + xpos = system.particles["xposition"] * type + ypos = system.particles["yposition"] * type + ax.plot(xpos, ypos, "o", markersize=mk, markeredgecolor="black") ax.set_xlim([0, system.box_length]) ax.set_ylim([0, system.box_length]) ax.set_xticks([]) @@ -612,11 +613,12 @@ def update_cellview(ax, system): # pragma: no cover system: System The whole system information. """ - x3 = system.particles["xposition"] - y3 = system.particles["yposition"] - line = ax.lines[0] - line.set_ydata(y3) - line.set_xdata(x3) + for index, type in enumerate(system.type_identifiers): + x3 = system.particles["xposition"] * type + y3 = system.particles["yposition"] * type + line = ax.lines[index] + line.set_ydata(y3) + line.set_xdata(x3) def update_rdfview(ax, system, average_rdf, r): # pragma: no cover diff --git a/pylj/tests/test_md.py b/pylj/tests/test_md.py index 663609b..454007a 100644 --- a/pylj/tests/test_md.py +++ b/pylj/tests/test_md.py @@ -15,7 +15,7 @@ def test_initialise_square(self): def test_velocity_verlet(self): a = md.initialise(2, 300, 8, "square") a.particles, a.distances, a.forces, a.energies = md.velocity_verlet( - a.particles, 1, a.box_length, a.cut_off, a.constants, a.forcefield, a.mass + a.particles, 1, a.box_length, a.cut_off, a.constants, a.forcefield, a.mass, a.type_identifiers ) assert_almost_equal(a.particles["xprevious_position"] * 1e10, [2, 2]) assert_almost_equal(a.particles["yprevious_position"] * 1e10, [2, 6]) diff --git a/pylj/tests/test_pairwise.py b/pylj/tests/test_pairwise.py index 8efbe07..b58eda5 100644 --- a/pylj/tests/test_pairwise.py +++ b/pylj/tests/test_pairwise.py @@ -34,9 +34,10 @@ def test_compute_forces(self): particles, 30, 15, - constants=[1.363e-134, 9.273e-78], + constants=[[1.363e-134, 9.273e-78]], forcefield=ff.lennard_jones, mass=39.948, + type_identifiers = [[1,1]] ) assert_almost_equal(distances, [4e-10]) assert_almost_equal(energies, [-1.4515047e-21]) @@ -70,8 +71,9 @@ def test_calculate_pressure(self): 30, 300, 15, - constants=[1.363e-134, 9.273e-78], + constants=[[1.363e-134, 9.273e-78]], forcefield=ff.lennard_jones, + type_identifiers = [[1,1]] ) assert_almost_equal(p * 1e24, 7.07368869) diff --git a/pylj/util.py b/pylj/util.py index eda0122..b6c730d 100644 --- a/pylj/util.py +++ b/pylj/util.py @@ -46,10 +46,23 @@ def __init__( init_conf="square", timestep_length=1e-14, cut_off=15, + mixing = False ): self.number_of_particles = number_of_particles self.init_temp = temperature - self.constants = constants + self.constants = constants #if mixing else constants[0] + + # Creates arrays to identify which particle is in which type + number_of_types = len(constants) + type_identifiers = np.zeros((number_of_types,number_of_particles)) + i = 0 + while i < number_of_particles: + for k in range(number_of_types): + if i < number_of_particles: + type_identifiers[k][i] = 1 + i+=1 + self.type_identifiers = type_identifiers + self.forcefield = forcefield self.mass = mass if box_length <= 600: @@ -180,6 +193,7 @@ def integrate(self, method): self.constants, self.forcefield, self.mass, + self.type_identifiers ) def md_sample(self): From 06e567d7c53f32cc7bc1d4f7de6dd3b6590d3996 Mon Sep 17 00:00:00 2001 From: maxdolan Date: Thu, 11 Jul 2024 14:11:11 +0100 Subject: [PATCH 02/18] restructuring to allow for particles of different types to interact --- pylj/mc.py | 3 ++- pylj/pairwise.py | 53 +++++++++++++++++++++++++++++++++++------------- pylj/util.py | 34 +++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 15 deletions(-) diff --git a/pylj/mc.py b/pylj/mc.py index 8dc41cc..1f86b67 100644 --- a/pylj/mc.py +++ b/pylj/mc.py @@ -8,7 +8,7 @@ def initialise( box_length, init_conf, mass=39.948, - constants=[1.363e-134, 9.273e-78], + constants=[[1.363e-134, 9.273e-78]], forcefield=ff.lennard_jones, ): """Initialise the particle positions (this can be either as a square or @@ -49,6 +49,7 @@ def initialise( forcefield, mass, init_conf=init_conf, + mixing = False ) system.particles["xvelocity"] = 0 system.particles["yvelocity"] = 0 diff --git a/pylj/pairwise.py b/pylj/pairwise.py index 17aac7f..99b6670 100644 --- a/pylj/pairwise.py +++ b/pylj/pairwise.py @@ -43,13 +43,22 @@ def compute_force(particles, box_length, cut_off, constants, forcefield, mass, t atomic_mass_unit = 1.660539e-27 # kilograms mass_amu = mass # amu mass_kg = mass_amu * atomic_mass_unit # kilograms - distances, dx, dy = heavy.dist( - particles["xposition"], particles["yposition"], box_length + distances, dx, dy, pair_types = heavy.dist( + particles["xposition"], particles["yposition"], box_length, particles['types'] ) - for type, constants in enumerate(constants): - identifier = type_identifiers[type] - ff = forcefield(constants) - type_distances = distances * create_dist_identifiers(identifier) + unique_pairs = list(set(pair_types)) + for pair in unique_pairs: + type_distances = distances.copy() + for i in range(len(distances)): + if pair != pair_types[i]: + type_distances[i] = 0 + if pair.split(',')[0] == pair.split(',')[1]: + constants_type = constants[int(pair.split(',')[0])] + else: + constants_1 = np.array(constants[int(pair.split(',')[0])]) + constants_2 = np.array(constants[int(pair.split(',')[1])]) + constants_type = np.sqrt(constants_1*constants_2) + ff = forcefield(constants_type) type_forces = ff.force(type_distances) type_energies = ff.energy(distances) type_forces = np.nan_to_num(type_forces) @@ -215,11 +224,25 @@ def compute_energy(particles, box_length, cut_off, constants, forcefield): ) distances = np.zeros(pairs) energies = np.zeros(pairs) - distances, dx, dy = heavy.dist( - particles["xposition"], particles["yposition"], box_length + distances, dx, dy, pair_types = heavy.dist( + particles["xposition"], particles["yposition"], box_length, particles['types'] ) - ff = forcefield(constants) - energies = ff.energy(distances) + unique_pairs = list(set(pair_types)) + for pair in unique_pairs: + type_distances = distances.copy() + for i in range(len(distances)): + if pair != pair_types[i]: + type_distances[i] = 0 + if pair.split(',')[0] == pair.split(',')[1]: + constants_type = constants[int(pair.split(',')[0])] + else: + constants_1 = np.array(constants[int(pair.split(',')[0])]) + constants_2 = np.array(constants[int(pair.split(',')[1])]) + constants_type = np.sqrt(constants_1*constants_2) + ff = forcefield(constants_type) + type_energies = ff.energy(distances) + type_energies = np.nan_to_num(type_energies) + energies+=type_energies energies[np.where(distances > cut_off)] = 0.0 return distances, energies @@ -253,8 +276,8 @@ def calculate_pressure( float: Instantaneous pressure of the simulation. """ - distances, dx, dy = heavy.dist( - particles["xposition"], particles["yposition"], box_length + distances, dx, dy, pair_types = heavy.dist( + particles["xposition"], particles["yposition"], box_length, particles['types'] ) forces = np.zeros(len(distances)) energies = np.zeros(len(distances)) @@ -307,7 +330,7 @@ def heat_bath(particles, temperature_sample, bath_temp): #Jit tag here had to be removed -def dist(xposition, yposition, box_length): +def dist(xposition, yposition, box_length, types): """Returns the distance array for the set of particles. Parameters ---------- @@ -331,6 +354,7 @@ def dist(xposition, yposition, box_length): drr = np.zeros(int((xposition.size - 1) * xposition.size / 2)) dxr = np.zeros(int((xposition.size - 1) * xposition.size / 2)) dyr = np.zeros(int((xposition.size - 1) * xposition.size / 2)) + pair_types = [] k = 0 for i in range(0, xposition.size - 1): for j in range(i + 1, xposition.size): @@ -342,8 +366,9 @@ def dist(xposition, yposition, box_length): drr[k] = dr dxr[k] = dx dyr[k] = dy + pair_types.append(types[i] + ',' + types[j]) k += 1 - return drr, dxr, dyr + return drr, dxr, dyr, pair_types #Jit tag here had to be removed diff --git a/pylj/util.py b/pylj/util.py index b6c730d..5a17c34 100644 --- a/pylj/util.py +++ b/pylj/util.py @@ -63,6 +63,22 @@ def __init__( i+=1 self.type_identifiers = type_identifiers + long_const = [] + types = [] + particle_list = [] + for i in range(number_of_particles): + constants_type = constants[i%len(constants)] + particle_type = f'{i%len(constants)}' + + long_const.append(constants_type) + types.append(particle_type) + particle = Particle(constants_type, i, mass, particle_type) + particle.add(particle_list) + + self.particle_list = particle_list + self.long_const = long_const + self.types = types + self.forcefield = forcefield self.mass = mass if box_length <= 600: @@ -128,6 +144,7 @@ def square(self): """ part_dt = particle_dt() self.particles = np.zeros(self.number_of_particles, dtype=part_dt) + self.particles['types'] = self.types m = int(np.ceil(np.sqrt(self.number_of_particles))) d = self.box_length / m n = 0 @@ -143,6 +160,7 @@ def random(self): """ part_dt = particle_dt() self.particles = np.zeros(self.number_of_particles, dtype=part_dt) + self.particles['types'] = self.types num_part = self.number_of_particles self.particles["xposition"] = np.random.uniform(0, self.box_length, num_part) self.particles["yposition"] = np.random.uniform(0, self.box_length, num_part) @@ -248,6 +266,21 @@ def reject(self): self.position_store, self.particles, self.random_particle ) +class Particle: + def __init__(self, + constants, + index, + mass, + particle_type + ): + self.constants = constants + self.index = index + self.mass = mass + self.type = particle_type + + def add(self, particles): + particles.append(self) + return particles def __cite__(): # pragma: no cover """This function will launch the website for the JOSE publication on @@ -285,5 +318,6 @@ def particle_dt(): ("energy", np.float64), ("xpbccount", int), ("ypbccount", int), + ("types", list) ] ) From e2f2db785a108e574c80d01347278367bd3a1050 Mon Sep 17 00:00:00 2001 From: maxdolan Date: Thu, 18 Jul 2024 14:18:07 +0100 Subject: [PATCH 03/18] updating tests --- pylj/tests/test_pairwise.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pylj/tests/test_pairwise.py b/pylj/tests/test_pairwise.py index b58eda5..bd2f8e2 100644 --- a/pylj/tests/test_pairwise.py +++ b/pylj/tests/test_pairwise.py @@ -30,6 +30,7 @@ def test_compute_forces(self): particles = np.zeros(2, dtype=part_dt) particles["xposition"][0] = 1e-10 particles["xposition"][1] = 5e-10 + particles['types'] = ['0','0'] particles, distances, forces, energies = pairwise.compute_force( particles, 30, @@ -51,11 +52,12 @@ def test_compute_energy(self): particles = np.zeros(2, dtype=part_dt) particles["xposition"][0] = 1e-10 particles["xposition"][1] = 5e-10 + particles['types'] = ['0','0'] d, e = pairwise.compute_energy( particles, 30, 15, - constants=[1.363e-134, 9.273e-78], + constants=[[1.363e-134, 9.273e-78]], forcefield=ff.lennard_jones, ) assert_almost_equal(d, [4e-10]) @@ -66,16 +68,16 @@ def test_calculate_pressure(self): particles = np.zeros(2, dtype=part_dt) particles["xposition"][0] = 1e-10 particles["xposition"][1] = 5e-10 + particles['types'] = ['0','0'] p = pairwise.calculate_pressure( particles, 30, 300, 15, constants=[[1.363e-134, 9.273e-78]], - forcefield=ff.lennard_jones, - type_identifiers = [[1,1]] + forcefield=ff.lennard_jones ) - assert_almost_equal(p * 1e24, 7.07368869) + assert_almost_equal(p * 1e24, 9.20399999) def test_pbc_correction(self): a = pairwise.pbc_correction(1, 10) From 3ed9d1596487bdb1cb65cf4f7dc44de13469d6cb Mon Sep 17 00:00:00 2001 From: maxdolan Date: Thu, 18 Jul 2024 14:47:12 +0100 Subject: [PATCH 04/18] cleaning up, updating docstrings, tests and examples --- examples/ideal_gas_law/ideal_gas_law.ipynb | 2 +- .../simple_examples/molecular_dynamics.ipynb | 16 +++- examples/simple_examples/monte_carlo.ipynb | 7 +- pylj/mc.py | 4 +- pylj/md.py | 13 ++-- pylj/pairwise.py | 36 ++++++--- pylj/tests/test_md.py | 2 +- pylj/tests/test_pairwise.py | 3 +- pylj/util.py | 78 +++++++++++-------- 9 files changed, 94 insertions(+), 67 deletions(-) diff --git a/examples/ideal_gas_law/ideal_gas_law.ipynb b/examples/ideal_gas_law/ideal_gas_law.ipynb index f55e986..4304eb6 100644 --- a/examples/ideal_gas_law/ideal_gas_law.ipynb +++ b/examples/ideal_gas_law/ideal_gas_law.ipynb @@ -450,7 +450,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.10.12" }, "latex_envs": { "LaTeX_envs_menu_present": true, diff --git a/examples/simple_examples/molecular_dynamics.ipynb b/examples/simple_examples/molecular_dynamics.ipynb index e111b50..946c156 100644 --- a/examples/simple_examples/molecular_dynamics.ipynb +++ b/examples/simple_examples/molecular_dynamics.ipynb @@ -7,7 +7,7 @@ "outputs": [], "source": [ "import warnings\n", - "from pylj import md, sample\n", + "from pylj import md, sample, pairwise\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import time\n", @@ -23,7 +23,8 @@ "source": [ "def md_simulation(number_of_particles, temperature, box_length, number_of_steps, sample_frequency):\n", " # Initialise the system\n", - " system = md.initialise(number_of_particles, temperature, box_length, 'square')\n", + " constants=[[1.363e-134, 9.273e-78],[1.365e-130, 9.278e-77],[1.368e-130, 9.278e-77]]\n", + " system = md.initialise(number_of_particles, temperature, box_length, 'square', constants=constants)\n", " # This sets the sampling class\n", " sample_system = sample.MaxBolt(system)# Start at time 0\n", " system.time = 0\n", @@ -63,9 +64,16 @@ "metadata": {}, "outputs": [], "source": [ - "md_simulation(20, 1000, 100, 5000, 100)" + "system = md_simulation(20, 1000, 100, 2500, 50)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "code", "execution_count": null, @@ -90,7 +98,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.12.4" }, "latex_envs": { "LaTeX_envs_menu_present": true, diff --git a/examples/simple_examples/monte_carlo.ipynb b/examples/simple_examples/monte_carlo.ipynb index f728d8b..d92a6b1 100644 --- a/examples/simple_examples/monte_carlo.ipynb +++ b/examples/simple_examples/monte_carlo.ipynb @@ -22,7 +22,8 @@ "source": [ "def mc_simulation(number_of_particles, temperature, box_length, number_of_steps, sample_frequency):\n", " # Initialise the system placing the particles on a square lattice\n", - " system = mc.initialise(number_of_particles, temperature, box_length, 'square')\n", + " constants=[[1.363e-134, 9.273e-78],[1.363e-134, 9.273e-78]]\n", + " system = mc.initialise(number_of_particles, temperature, box_length, 'square', constants=constants, mixing = False)\n", " # This sets the sampling class as Energy, which shows the energy of the system\n", " sample_system = sample.Energy(system)\n", " # Compute the energy of the system\n", @@ -71,7 +72,7 @@ "metadata": {}, "outputs": [], "source": [ - "system = mc_simulation(100, 273.15, 45, 5000, 25)" + "system = mc_simulation(50, 273.15, 45, 1000, 25)" ] }, { @@ -105,7 +106,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.12.4" }, "latex_envs": { "LaTeX_envs_menu_present": true, diff --git a/pylj/mc.py b/pylj/mc.py index 1f86b67..5e1ff5b 100644 --- a/pylj/mc.py +++ b/pylj/mc.py @@ -10,6 +10,7 @@ def initialise( mass=39.948, constants=[[1.363e-134, 9.273e-78]], forcefield=ff.lennard_jones, + mixing = False ): """Initialise the particle positions (this can be either as a square or random arrangement), velocities (based on the temperature defined, and # @@ -48,8 +49,7 @@ def initialise( constants, forcefield, mass, - init_conf=init_conf, - mixing = False + init_conf=init_conf ) system.particles["xvelocity"] = 0 system.particles["yvelocity"] = 0 diff --git a/pylj/md.py b/pylj/md.py index 1ecb851..5daf416 100644 --- a/pylj/md.py +++ b/pylj/md.py @@ -11,8 +11,7 @@ def initialise( timestep_length=1e-14, mass=39.948, constants=[[1.363e-134, 9.273e-78]], - forcefield=ff.lennard_jones, - mixing = False + forcefield=ff.lennard_jones ): """Initialise the particle positions (this can be either as a square or random arrangement), velocities (based on the temperature defined, and @@ -54,8 +53,7 @@ def initialise( forcefield, mass, init_conf=init_conf, - timestep_length=timestep_length, - mixing = mixing + timestep_length=timestep_length ) v = np.random.rand(system.particles.size, 2, 12) v = np.sum(v, axis=2) - 6.0 @@ -80,7 +78,7 @@ def initialize( #Jit tag here had to be removed def velocity_verlet( - particles, timestep_length, box_length, cut_off, constants, forcefield, mass, type_identifiers + particles, timestep_length, box_length, cut_off, constants, forcefield, mass ): """Uses the Velocity-Verlet integrator to move forward in time. The Updates the particles positions and velocities in terms of the Velocity @@ -114,7 +112,7 @@ def velocity_verlet( xacceleration_store = list(particles["xacceleration"]) yacceleration_store = list(particles["yacceleration"]) particles, distances, forces, energies = heavy.compute_force( - particles, box_length, cut_off, constants, forcefield, mass, type_identifiers + particles, box_length, cut_off, constants, forcefield, mass ) [particles["xvelocity"], particles["yvelocity"]] = update_velocities( [particles["xvelocity"], particles["yvelocity"]], @@ -154,8 +152,7 @@ def sample(particles, box_length, initial_particles, system): temperature_new, system.cut_off, system.constants, - system.forcefield, - system.type_identifiers + system.forcefield ) msd_new = calculate_msd(particles, initial_particles, box_length) system.pressure_sample = np.append(system.pressure_sample, pressure_new) diff --git a/pylj/pairwise.py b/pylj/pairwise.py index 99b6670..cf7477f 100644 --- a/pylj/pairwise.py +++ b/pylj/pairwise.py @@ -3,7 +3,7 @@ from pylj import pairwise as heavy #Jit tag here had to be removed -def compute_force(particles, box_length, cut_off, constants, forcefield, mass, type_identifiers): +def compute_force(particles, box_length, cut_off, constants, forcefield, mass): r"""Calculates the forces and therefore the accelerations on each of the particles in the simulation. Parameters @@ -248,7 +248,7 @@ def compute_energy(particles, box_length, cut_off, constants, forcefield): def calculate_pressure( - particles, box_length, temperature, cut_off, constants, forcefield, type_identifiers + particles, box_length, temperature, cut_off, constants, forcefield ): r"""Calculates the instantaneous pressure of the simulation cell, found with the following relationship: @@ -281,15 +281,21 @@ def calculate_pressure( ) forces = np.zeros(len(distances)) energies = np.zeros(len(distances)) - for type, constants in enumerate(constants): - identifier = type_identifiers[type] - ff = forcefield(constants) - type_distances = distances * create_dist_identifiers(identifier) - type_forces = ff.force(type_distances) + unique_pairs = list(set(pair_types)) + for pair in unique_pairs: + type_distances = distances.copy() + for i in range(len(distances)): + if pair != pair_types[i]: + type_distances[i] = 0 + if pair.split(',')[0] == pair.split(',')[1]: + constants_type = constants[int(pair.split(',')[0])] + else: + constants_1 = np.array(constants[int(pair.split(',')[0])]) + constants_2 = np.array(constants[int(pair.split(',')[1])]) + constants_type = np.sqrt(constants_1*constants_2) + ff = forcefield(constants_type) type_energies = ff.energy(distances) - type_forces = np.nan_to_num(type_forces) type_energies = np.nan_to_num(type_energies) - forces+=type_forces energies+=type_energies forces[np.where(distances > cut_off)] = 0.0 pres = np.sum(forces * distances) @@ -342,14 +348,20 @@ def dist(xposition, yposition, box_length, types): y-dimension positions of the particles. box_length: float The box length of the simulation cell. + types: str, array_like (N) + Array of length N, where N is the number of particles, providing the + type of each particle. Returns ------- - drr float, array_like ((N - 1) * N / 2)) + drr: float, array_like ((N - 1) * N / 2)) The pairs of distances between the particles. - dxr float, array_like ((N - 1) * N / 2)) + dxr: float, array_like ((N - 1) * N / 2)) The pairs of distances between the particles, in only the x-dimension. - dyr float, array_like ((N - 1) * N / 2)) + dyr: float, array_like ((N - 1) * N / 2)) The pairs of distances between the particles, in only the y-dimension. + pair_types: str, array_like ((N - 1) * N / 2)) + The types of the two particles in each interaction, saved as + 'type1,type2' for each """ drr = np.zeros(int((xposition.size - 1) * xposition.size / 2)) dxr = np.zeros(int((xposition.size - 1) * xposition.size / 2)) diff --git a/pylj/tests/test_md.py b/pylj/tests/test_md.py index 454007a..663609b 100644 --- a/pylj/tests/test_md.py +++ b/pylj/tests/test_md.py @@ -15,7 +15,7 @@ def test_initialise_square(self): def test_velocity_verlet(self): a = md.initialise(2, 300, 8, "square") a.particles, a.distances, a.forces, a.energies = md.velocity_verlet( - a.particles, 1, a.box_length, a.cut_off, a.constants, a.forcefield, a.mass, a.type_identifiers + a.particles, 1, a.box_length, a.cut_off, a.constants, a.forcefield, a.mass ) assert_almost_equal(a.particles["xprevious_position"] * 1e10, [2, 2]) assert_almost_equal(a.particles["yprevious_position"] * 1e10, [2, 6]) diff --git a/pylj/tests/test_pairwise.py b/pylj/tests/test_pairwise.py index bd2f8e2..bcbfa21 100644 --- a/pylj/tests/test_pairwise.py +++ b/pylj/tests/test_pairwise.py @@ -37,8 +37,7 @@ def test_compute_forces(self): 15, constants=[[1.363e-134, 9.273e-78]], forcefield=ff.lennard_jones, - mass=39.948, - type_identifiers = [[1,1]] + mass=39.948 ) assert_almost_equal(distances, [4e-10]) assert_almost_equal(energies, [-1.4515047e-21]) diff --git a/pylj/util.py b/pylj/util.py index 5a17c34..9d61ef8 100644 --- a/pylj/util.py +++ b/pylj/util.py @@ -45,42 +45,19 @@ def __init__( mass, init_conf="square", timestep_length=1e-14, - cut_off=15, - mixing = False + cut_off=15 ): self.number_of_particles = number_of_particles self.init_temp = temperature - self.constants = constants #if mixing else constants[0] - - # Creates arrays to identify which particle is in which type - number_of_types = len(constants) - type_identifiers = np.zeros((number_of_types,number_of_particles)) - i = 0 - while i < number_of_particles: - for k in range(number_of_types): - if i < number_of_particles: - type_identifiers[k][i] = 1 - i+=1 - self.type_identifiers = type_identifiers - - long_const = [] - types = [] - particle_list = [] - for i in range(number_of_particles): - constants_type = constants[i%len(constants)] - particle_type = f'{i%len(constants)}' - - long_const.append(constants_type) - types.append(particle_type) - particle = Particle(constants_type, i, mass, particle_type) - particle.add(particle_list) - - self.particle_list = particle_list - self.long_const = long_const - self.types = types - - self.forcefield = forcefield + self.constants = constants self.mass = mass + self.type_identifiers = None + self.particle_list = None + self.long_const = None + self.types = None + self.setup_type_identifiers() + self.setup_types() + self.forcefield = forcefield if box_length <= 600: self.box_length = box_length * 1e-10 else: @@ -165,6 +142,39 @@ def random(self): self.particles["xposition"] = np.random.uniform(0, self.box_length, num_part) self.particles["yposition"] = np.random.uniform(0, self.box_length, num_part) + def setup_types(self): + """Sets the long constants and types arrays of the particles + """ + long_const = [] + types = [] + particle_list = [] + for i in range(self.number_of_particles): + # Get set of constants and index + constants_type = self.constants[i%len(self.constants)] + particle_type = f'{i%len(self.constants)}' + # Append to lists + long_const.append(constants_type) + types.append(particle_type) + particle = Particle(constants_type, i, self.mass, particle_type) + particle.add(particle_list) + self.particle_list = particle_list + self.long_const = long_const + self.types = types + + def setup_type_identifiers(self): + """Sets type-identifers arrays - legacy method now only used for plotting + """ + # Creates arrays to identify which particle is in which type + number_of_types = len(self.constants) + type_identifiers = np.zeros((number_of_types,self.number_of_particles)) + i = 0 + while i < self.number_of_particles: + for k in range(number_of_types): + if i < self.number_of_particles: + type_identifiers[k][i] = 1 + i+=1 + self.type_identifiers = type_identifiers + def compute_force(self): """Maps to the compute_force function in either the comp (if Cython is installed) or the pairwise module and allows for a cleaner interface. @@ -210,8 +220,7 @@ def integrate(self, method): self.cut_off, self.constants, self.forcefield, - self.mass, - self.type_identifiers + self.mass ) def md_sample(self): @@ -304,6 +313,7 @@ def particle_dt(): - xprevious_position and yprevious_position - xforce and yforce - energy + - types """ return np.dtype( [ From 9c3df090d27f9e478f4860280d11c92a5f04bd5f Mon Sep 17 00:00:00 2001 From: maxdolan Date: Tue, 23 Jul 2024 14:17:27 +0100 Subject: [PATCH 05/18] updating forcefields method for improved particle mixing --- pylj/forcefields.py | 109 ++++++++++++++++++------------------ pylj/pairwise.py | 41 ++++++++------ pylj/tests/test_pairwise.py | 2 +- 3 files changed, 78 insertions(+), 74 deletions(-) diff --git a/pylj/forcefields.py b/pylj/forcefields.py index ddc59e2..ede6d0b 100644 --- a/pylj/forcefields.py +++ b/pylj/forcefields.py @@ -1,20 +1,35 @@ import numpy as np - -class lennard_jones(object): +class lennard_jones_base(object): r"""Calculate the energy or force for a pair of particles using the - Lennard-Jones (A/B variant) forcefield. + Lennard-Jones forcefield. Either the a_b or sigma_epsilon constants + must be specified Parameters ---------- - constants: float, array_like + a_b: float, array_like An array of length two consisting of the A and B parameters for the 12-6 Lennard-Jones function - + sigma_epsilon: float, array_like + An array of length two consisting of the sigma and epsilon parameters for the + 12-6 Lennard-Jones function """ - def __init__(self, constants): - self.a = constants[0] - self.b = constants[1] + def __init__(self, constants, a_b = False, sigma_epsilon = False): + + if sigma_epsilon: + self.sigma = constants[0] + self.epsilon = constants[1] + self.a = 4 * self.epsilon * (self.sigma**12) + self.b = 4 * self.epsilon * (self.sigma**6) + self.type = 'sigma_epsilon' + + elif a_b: + self.a = constants[0] + self.b = constants[1] + self.sigma = (self.a / self.b)**(1/6) + self.epsilon = (self.b**2)/(4*self.a) + self.type = 'a_b' + def energy(self, dr ): r"""Calculate the energy for a pair of particles using the @@ -53,65 +68,49 @@ def force(self, dr): """ self.force = 12 * self.a * np.power(dr, -13) - (6 * self.b * np.power(dr, -7)) return self.force + + def mixing(self, constants_2): + + if self.type == 'a_b': + a2 = constants_2[0] + b2 = constants_2[1] + sigma2 = (a2 / b2)**(1/6) + epsilon2 = (b2**2)/(4*a2) + + elif self.type == 'sigma_epsilon': + sigma2 = constants_2[0] + epsilon2 = constants_2[1] + + self.sigma = (self.sigma+sigma2)/2 + self.epsilon = np.sqrt(self.epsilon**2 + epsilon2**2) + self.a = 4 * self.epsilon * (self.sigma**12) + self.b = 4 * self.epsilon * (self.sigma**6) +class lennard_jones(lennard_jones_base): + r"""Calculate the energy or force for a pair of particles using the + Lennard-Jones (A/B variant) forcefield. Maps to lennard_jones_base class + Parameters + ---------- + constants: float, array_like + An array of length two consisting of the A and B + parameters for the 12-6 Lennard-Jones function + """ + def __init__(self, constants): + super().__init__(constants, a_b = True) -class lennard_jones_sigma_epsilon(object): +class lennard_jones_sigma_epsilon(lennard_jones_base): r"""Calculate the energy or force for a pair of particles using the - Lennard-Jones (sigma/epsilon variant) forcefield. + Lennard-Jones (sigma/epsilon variant) forcefield. Maps to lennard_jones_base class Parameters ---------- constants: float, array_like An array of length two consisting of the sigma (a) and epsilon (e) parameters for the 12-6 Lennard-Jones function - """ def __init__(self, constants): - self.sigma = constants[0] - self.epsilon = constants[1] - - def energy(self, dr): - r"""Calculate the energy for a pair of particles using the - Lennard-Jones (sigma/epsilon variant) forcefield. - - .. math:: - E = \frac{4e*a^{12}}{dr^{12}} - \frac{4e*a^{6}}{dr^6} - - Attributes: - ---------- - dr (float): The distance between particles. - - Returns - ------- - float: array_like - The potential energy between the particles. - """ - self.energy = 4 * self.epsilon * np.power(self.sigma, 12) * np.power(dr, -12) - ( - 4 * self.epsilon * np.power(self.sigma, 6) * np.power(dr, -6)) - return self.energy - - def force(self, dr): - r"""Calculate the force for a pair of particles using the - Lennard-Jones (sigma/epsilon variant) forcefield. - - .. math:: - f = \frac{48e*a^{12}}{dr^{13}} - \frac{24e*a^{6}}{dr^7} - - Attributes: - ---------- - dr (float): The distance between particles. - - Returns - ------- - float: array_like - The force between the particles. - """ - self.force = 48 * self.epsilon * np.power(self.sigma, 12) * np.power( - dr, -13) - (24 * self.epsilon * np.power(self.sigma, 6) * np.power(dr, -7)) - return self.force - - + super().__init__(constants, sigma_epsilon = True) class buckingham(object): r""" Calculate the energy or force for a pair of particles using the diff --git a/pylj/pairwise.py b/pylj/pairwise.py index cf7477f..c4c9148 100644 --- a/pylj/pairwise.py +++ b/pylj/pairwise.py @@ -52,13 +52,13 @@ def compute_force(particles, box_length, cut_off, constants, forcefield, mass): for i in range(len(distances)): if pair != pair_types[i]: type_distances[i] = 0 - if pair.split(',')[0] == pair.split(',')[1]: - constants_type = constants[int(pair.split(',')[0])] - else: - constants_1 = np.array(constants[int(pair.split(',')[0])]) + type_1 = pair.split(',')[0] + type_2 = pair.split(',')[1] + constants_1 = np.array(constants[int(pair.split(',')[0])]) + ff = forcefield(constants_1) + if type_1 != type_2: constants_2 = np.array(constants[int(pair.split(',')[1])]) - constants_type = np.sqrt(constants_1*constants_2) - ff = forcefield(constants_type) + ff.mixing(constants_2) type_forces = ff.force(type_distances) type_energies = ff.energy(distances) type_forces = np.nan_to_num(type_forces) @@ -233,14 +233,16 @@ def compute_energy(particles, box_length, cut_off, constants, forcefield): for i in range(len(distances)): if pair != pair_types[i]: type_distances[i] = 0 - if pair.split(',')[0] == pair.split(',')[1]: - constants_type = constants[int(pair.split(',')[0])] - else: - constants_1 = np.array(constants[int(pair.split(',')[0])]) + type_1 = pair.split(',')[0] + type_2 = pair.split(',')[1] + constants_1 = np.array(constants[int(pair.split(',')[0])]) + ff = forcefield(constants_1) + if type_1 != type_2: constants_2 = np.array(constants[int(pair.split(',')[1])]) - constants_type = np.sqrt(constants_1*constants_2) - ff = forcefield(constants_type) + ff.mixing(constants_2) + type_forces = ff.force(type_distances) type_energies = ff.energy(distances) + type_forces = np.nan_to_num(type_forces) type_energies = np.nan_to_num(type_energies) energies+=type_energies energies[np.where(distances > cut_off)] = 0.0 @@ -287,15 +289,18 @@ def calculate_pressure( for i in range(len(distances)): if pair != pair_types[i]: type_distances[i] = 0 - if pair.split(',')[0] == pair.split(',')[1]: - constants_type = constants[int(pair.split(',')[0])] - else: - constants_1 = np.array(constants[int(pair.split(',')[0])]) + type_1 = pair.split(',')[0] + type_2 = pair.split(',')[1] + constants_1 = np.array(constants[int(pair.split(',')[0])]) + ff = forcefield(constants_1) + if type_1 != type_2: constants_2 = np.array(constants[int(pair.split(',')[1])]) - constants_type = np.sqrt(constants_1*constants_2) - ff = forcefield(constants_type) + ff.mixing(constants_2) + type_forces = ff.force(type_distances) type_energies = ff.energy(distances) + type_forces = np.nan_to_num(type_forces) type_energies = np.nan_to_num(type_energies) + forces+=type_forces energies+=type_energies forces[np.where(distances > cut_off)] = 0.0 pres = np.sum(forces * distances) diff --git a/pylj/tests/test_pairwise.py b/pylj/tests/test_pairwise.py index bcbfa21..71421dd 100644 --- a/pylj/tests/test_pairwise.py +++ b/pylj/tests/test_pairwise.py @@ -76,7 +76,7 @@ def test_calculate_pressure(self): constants=[[1.363e-134, 9.273e-78]], forcefield=ff.lennard_jones ) - assert_almost_equal(p * 1e24, 9.20399999) + assert_almost_equal(p * 1e24, 7.07368867) def test_pbc_correction(self): a = pairwise.pbc_correction(1, 10) From 60c748b2c785b04efc5262142fcbcd8f3c1c5afd Mon Sep 17 00:00:00 2001 From: maxdolan Date: Tue, 23 Jul 2024 14:43:41 +0100 Subject: [PATCH 06/18] getting rid of unnessacery energy function and calculations within pressure function --- pylj/md.py | 34 +-------------- pylj/pairwise.py | 86 +++---------------------------------- pylj/tests/test_pairwise.py | 19 +------- pylj/util.py | 12 +----- 4 files changed, 11 insertions(+), 140 deletions(-) diff --git a/pylj/md.py b/pylj/md.py index 5daf416..f640f2d 100644 --- a/pylj/md.py +++ b/pylj/md.py @@ -152,7 +152,8 @@ def sample(particles, box_length, initial_particles, system): temperature_new, system.cut_off, system.constants, - system.forcefield + system.forcefield, + system.mass ) msd_new = calculate_msd(particles, initial_particles, box_length) system.pressure_sample = np.append(system.pressure_sample, pressure_new) @@ -329,37 +330,6 @@ def compute_force(particles, box_length, cut_off, constants, forcefield, mass): return part, dist, forces, energies -def compute_energy(particles, box_length, cut_off, constants, forcefield): - r"""Calculates the total energy of the simulation. - Parameters - ---------- - particles: util.particle_dt, array_like - Information about the particles. - box_length: float - Length of a single dimension of the simulation square, in Angstrom. - cut_off: float - The distance greater than which the energies between particles is - taken as zero. - constants: float, array_like (optional) - The constants associated with the particular forcefield used, e.g. for - the function forcefields.lennard_jones, theses are [A, B] - forcefield: function (optional) - The particular forcefield to be used to find the energy and forces. - Returns - ------- - util.particle_dt, array_like - Information about particles, with updated accelerations and forces. - float, array_like - Current distances between pairs of particles in the simulation. - float, array_like - Current energies between pairs of particles in the simulation. - """ - dist, energies = heavy.compute_energy( - particles, box_length, cut_off, constants, forcefield - ) - return dist, energies - - def heat_bath(particles, temperature_sample, bath_temperature): r"""Rescales the velocities of the particles in the system to control the temperature of the simulation. Thereby allowing for an NVT ensemble. The diff --git a/pylj/pairwise.py b/pylj/pairwise.py index c4c9148..d49f054 100644 --- a/pylj/pairwise.py +++ b/pylj/pairwise.py @@ -194,63 +194,8 @@ def lennard_jones_force(A, B, dr): return 12 * A * np.power(dr, -13) - 6 * B * np.power(dr, -7) -def compute_energy(particles, box_length, cut_off, constants, forcefield): - r"""Calculates the total energy of the simulation. - Parameters - ---------- - particles: util.particle_dt, array_like - Information about the particles. - box_length: float - Length of a single dimension of the simulation square, in Angstrom. - cut_off: float - The distance greater than which the energies between particles is - taken as zero. - constants: float, array_like (optional) - The constants associated with the particular forcefield used, e.g. for - the function forcefields.lennard_jones, theses are [A, B] - forcefield: function (optional) - The particular forcefield to be used to find the energy and forces. - Returns - ------- - util.particle_dt, array_like - Information about particles, with updated accelerations and forces. - float, array_like - Current distances between pairs of particles in the simulation. - float, array_like - Current energies between pairs of particles in the simulation. - """ - pairs = int( - (particles["xacceleration"].size - 1) * particles["xacceleration"].size / 2 - ) - distances = np.zeros(pairs) - energies = np.zeros(pairs) - distances, dx, dy, pair_types = heavy.dist( - particles["xposition"], particles["yposition"], box_length, particles['types'] - ) - unique_pairs = list(set(pair_types)) - for pair in unique_pairs: - type_distances = distances.copy() - for i in range(len(distances)): - if pair != pair_types[i]: - type_distances[i] = 0 - type_1 = pair.split(',')[0] - type_2 = pair.split(',')[1] - constants_1 = np.array(constants[int(pair.split(',')[0])]) - ff = forcefield(constants_1) - if type_1 != type_2: - constants_2 = np.array(constants[int(pair.split(',')[1])]) - ff.mixing(constants_2) - type_forces = ff.force(type_distances) - type_energies = ff.energy(distances) - type_forces = np.nan_to_num(type_forces) - type_energies = np.nan_to_num(type_energies) - energies+=type_energies - energies[np.where(distances > cut_off)] = 0.0 - return distances, energies - - def calculate_pressure( - particles, box_length, temperature, cut_off, constants, forcefield + particles, box_length, temperature, cut_off, constants, forcefield, mass ): r"""Calculates the instantaneous pressure of the simulation cell, found with the following relationship: @@ -278,31 +223,10 @@ def calculate_pressure( float: Instantaneous pressure of the simulation. """ - distances, dx, dy, pair_types = heavy.dist( - particles["xposition"], particles["yposition"], box_length, particles['types'] - ) - forces = np.zeros(len(distances)) - energies = np.zeros(len(distances)) - unique_pairs = list(set(pair_types)) - for pair in unique_pairs: - type_distances = distances.copy() - for i in range(len(distances)): - if pair != pair_types[i]: - type_distances[i] = 0 - type_1 = pair.split(',')[0] - type_2 = pair.split(',')[1] - constants_1 = np.array(constants[int(pair.split(',')[0])]) - ff = forcefield(constants_1) - if type_1 != type_2: - constants_2 = np.array(constants[int(pair.split(',')[1])]) - ff.mixing(constants_2) - type_forces = ff.force(type_distances) - type_energies = ff.energy(distances) - type_forces = np.nan_to_num(type_forces) - type_energies = np.nan_to_num(type_energies) - forces+=type_forces - energies+=type_energies - forces[np.where(distances > cut_off)] = 0.0 + particles, distances, forces, energies = heavy.compute_force( + particles, box_length, cut_off, constants, forcefield, mass + ) + pres = np.sum(forces * distances) boltzmann_constant = 1.3806e-23 # joules / kelvin pres = 1.0 / (2 * box_length * box_length) * pres + ( diff --git a/pylj/tests/test_pairwise.py b/pylj/tests/test_pairwise.py index 71421dd..c3bfa0c 100644 --- a/pylj/tests/test_pairwise.py +++ b/pylj/tests/test_pairwise.py @@ -46,22 +46,6 @@ def test_compute_forces(self): assert_almost_equal(particles["xacceleration"][0] / 1e14, 1.4451452) assert_almost_equal(particles["xacceleration"][1] / 1e14, -1.4451452) - def test_compute_energy(self): - part_dt = util.particle_dt() - particles = np.zeros(2, dtype=part_dt) - particles["xposition"][0] = 1e-10 - particles["xposition"][1] = 5e-10 - particles['types'] = ['0','0'] - d, e = pairwise.compute_energy( - particles, - 30, - 15, - constants=[[1.363e-134, 9.273e-78]], - forcefield=ff.lennard_jones, - ) - assert_almost_equal(d, [4e-10]) - assert_almost_equal(e, [-1.4515047e-21]) - def test_calculate_pressure(self): part_dt = util.particle_dt() particles = np.zeros(2, dtype=part_dt) @@ -74,7 +58,8 @@ def test_calculate_pressure(self): 300, 15, constants=[[1.363e-134, 9.273e-78]], - forcefield=ff.lennard_jones + forcefield=ff.lennard_jones, + mass = 39.948 ) assert_almost_equal(p * 1e24, 7.07368867) diff --git a/pylj/util.py b/pylj/util.py index 9d61ef8..861e287 100644 --- a/pylj/util.py +++ b/pylj/util.py @@ -193,17 +193,9 @@ def compute_force(self): self.energies = energies def compute_energy(self): - """Maps to the compute_energy function in either the comp (if Cython - is installed) or the pairwise module and allows for a cleaner - interface. + """Maps to the compute_force function, as this also calculates energy """ - self.distances, self.energies = md.compute_energy( - self.particles, - self.box_length, - self.cut_off, - self.constants, - self.forcefield, - ) + self.compute_force() #Jit tag here had to be removed def integrate(self, method): From 6b1ab910e04af32c0e69c6b8f7eab72405f3a2cc Mon Sep 17 00:00:00 2001 From: maxdolan Date: Tue, 23 Jul 2024 15:42:18 +0100 Subject: [PATCH 07/18] updating tests to reflect multiple particle types --- pylj/pairwise.py | 3 ++- pylj/tests/test_forcefields.py | 11 ++++++++++- pylj/tests/test_pairwise.py | 22 ++++++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/pylj/pairwise.py b/pylj/pairwise.py index d49f054..2426e89 100644 --- a/pylj/pairwise.py +++ b/pylj/pairwise.py @@ -218,6 +218,8 @@ def calculate_pressure( the function forcefields.lennard_jones, theses are [A, B] forcefield: function (optional) The particular forcefield to be used to find the energy and forces. + mass: float (optional) + The mass of the particle being simulated (units of atomic mass units). Returns ------- float: @@ -226,7 +228,6 @@ def calculate_pressure( particles, distances, forces, energies = heavy.compute_force( particles, box_length, cut_off, constants, forcefield, mass ) - pres = np.sum(forces * distances) boltzmann_constant = 1.3806e-23 # joules / kelvin pres = 1.0 / (2 * box_length * box_length) * pres + ( diff --git a/pylj/tests/test_forcefields.py b/pylj/tests/test_forcefields.py index a1376d8..1b5924c 100644 --- a/pylj/tests/test_forcefields.py +++ b/pylj/tests/test_forcefields.py @@ -4,6 +4,16 @@ class TestForcefields(unittest.TestCase): + def test_lennard_jones_base(self): + a = forcefields.lennard_jones_base([1,2], a_b = True) + assert_almost_equal([a.sigma, a.epsilon],[0.8908987, 1]) + a.mixing([3,4]) + assert_almost_equal([a.a, a.b],[2.5171807, 4.0964869]) + b = forcefields.lennard_jones_base([1,2], sigma_epsilon = True) + assert_almost_equal([b.a, b.b], [8, 8]) + b.mixing([3,4]) + assert_almost_equal([b.sigma, b.epsilon],[2, 4.472135955]) + def test_lennard_jones_energy(self): a = forcefields.lennard_jones([1.0, 1.0]) assert_almost_equal(a.energy(2.0), -0.015380859) @@ -84,6 +94,5 @@ def test_square_well_force(self): with self.assertRaises(ValueError): b.force() - if __name__ == '__main__': unittest.main(exit=False) diff --git a/pylj/tests/test_pairwise.py b/pylj/tests/test_pairwise.py index c3bfa0c..615d330 100644 --- a/pylj/tests/test_pairwise.py +++ b/pylj/tests/test_pairwise.py @@ -68,3 +68,25 @@ def test_pbc_correction(self): assert_almost_equal(a, 1) b = pairwise.pbc_correction(11, 10) assert_almost_equal(b, 1) + + def test_multiple_particles(self): + part_dt = util.particle_dt() + particles = np.zeros(3, dtype=part_dt) + particles["xposition"][0] = 1e-10 + particles["xposition"][1] = 5e-10 + particles["yposition"][2] = 5e-10 + particles['types'] = ['0','1','0'] + particles, distances, forces, energies = pairwise.compute_force( + particles, + 30, + 15, + constants=[[1.363e-134, 9.273e-78],[1.363e-133, 9.273e-77]], + forcefield=ff.lennard_jones, + mass=39.948 + ) + assert_almost_equal(distances, [4.0000000e-10, 5.0990195e-10, 7.0710678e-10]) + assert_almost_equal(energies, [-3.0626388e-20, -1.0201147e-20, -1.5468582e-21]) + assert_almost_equal(forces, [-9.6342138e-11, -5.1698213e-12, -6.1773405e-12]) + assert_almost_equal(particles["yacceleration"], [7.6421356e+13, 6.5847975e+13, -1.4226933e+14], decimal=-7) + assert_almost_equal(particles["xacceleration"][0] / 1e14, 14.3706863) + assert_almost_equal(particles["xacceleration"][1] / 1e14, -15.1820088) \ No newline at end of file From e5fd5d7560583a4c510741e227a9cd121cb8ba12 Mon Sep 17 00:00:00 2001 From: maxdolan Date: Tue, 23 Jul 2024 16:13:35 +0100 Subject: [PATCH 08/18] adding mixing to buckingham --- pylj/forcefields.py | 6 +++++- pylj/tests/test_forcefields.py | 9 +++++++-- pylj/tests/test_pairwise.py | 6 +++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pylj/forcefields.py b/pylj/forcefields.py index ede6d0b..64d7f7d 100644 --- a/pylj/forcefields.py +++ b/pylj/forcefields.py @@ -82,7 +82,7 @@ def mixing(self, constants_2): epsilon2 = constants_2[1] self.sigma = (self.sigma+sigma2)/2 - self.epsilon = np.sqrt(self.epsilon**2 + epsilon2**2) + self.epsilon = np.sqrt(self.epsilon * epsilon2) self.a = 4 * self.epsilon * (self.sigma**12) self.b = 4 * self.epsilon * (self.sigma**6) @@ -166,6 +166,10 @@ def force(self, dr): self.force = self.a * self.b * np.exp(- np.multiply(self.b, dr)) - 6 * self.c / np.power(dr, 7) return self.force + def mixing(self, constants2): + self.a = np.sqrt(self.a*constants2[0]) + self.b = np.sqrt(self.b*constants2[1]) + self.c = np.sqrt(self.c*constants2[2]) class square_well(object): diff --git a/pylj/tests/test_forcefields.py b/pylj/tests/test_forcefields.py index 1b5924c..5e10981 100644 --- a/pylj/tests/test_forcefields.py +++ b/pylj/tests/test_forcefields.py @@ -8,11 +8,11 @@ def test_lennard_jones_base(self): a = forcefields.lennard_jones_base([1,2], a_b = True) assert_almost_equal([a.sigma, a.epsilon],[0.8908987, 1]) a.mixing([3,4]) - assert_almost_equal([a.a, a.b],[2.5171807, 4.0964869]) + assert_almost_equal([a.a, a.b],[1.743954 , 2.8381294]) b = forcefields.lennard_jones_base([1,2], sigma_epsilon = True) assert_almost_equal([b.a, b.b], [8, 8]) b.mixing([3,4]) - assert_almost_equal([b.sigma, b.epsilon],[2, 4.472135955]) + assert_almost_equal([b.sigma, b.epsilon],[2, 2.8284271]) def test_lennard_jones_energy(self): a = forcefields.lennard_jones([1.0, 1.0]) @@ -72,6 +72,11 @@ def test_buckingham_force(self): c = forcefields.buckingham([1.5, 0.1, 2.0]) assert_almost_equal(c.force([2.0, 1.0, 4.0]), [0.0290596, -11.8642744, 0.0998156]) + def test_buckingham_mixing(self): + a = forcefields.buckingham([1.0, 1.0, 1.0]) + a.mixing([4.0, 4.0, 4.0]) + assert_almost_equal([a.a, a.b, a.c], [2.0, 2.0, 2.0]) + def test_square_well_energy(self): a = forcefields.square_well([1.0, 1.5, 2.0]) assert_equal(a.energy(2.0), -1.0) diff --git a/pylj/tests/test_pairwise.py b/pylj/tests/test_pairwise.py index 615d330..d1dc3e2 100644 --- a/pylj/tests/test_pairwise.py +++ b/pylj/tests/test_pairwise.py @@ -87,6 +87,6 @@ def test_multiple_particles(self): assert_almost_equal(distances, [4.0000000e-10, 5.0990195e-10, 7.0710678e-10]) assert_almost_equal(energies, [-3.0626388e-20, -1.0201147e-20, -1.5468582e-21]) assert_almost_equal(forces, [-9.6342138e-11, -5.1698213e-12, -6.1773405e-12]) - assert_almost_equal(particles["yacceleration"], [7.6421356e+13, 6.5847975e+13, -1.4226933e+14], decimal=-7) - assert_almost_equal(particles["xacceleration"][0] / 1e14, 14.3706863) - assert_almost_equal(particles["xacceleration"][1] / 1e14, -15.1820088) \ No newline at end of file + assert_almost_equal(particles["yacceleration"], [7.6421357e+13, 2.07196175e+13,-9.71409740e+13], decimal=-7) + assert_almost_equal(particles["xacceleration"][0] / 1e14, 4.4171075) + assert_almost_equal(particles["xacceleration"][1] / 1e14, -4.7771464) \ No newline at end of file From e07c15cd524457c5bcd188e05ba1dd87129b66bc Mon Sep 17 00:00:00 2001 From: maxdolan Date: Tue, 23 Jul 2024 16:19:55 +0100 Subject: [PATCH 09/18] updating docstrings --- pylj/forcefields.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/pylj/forcefields.py b/pylj/forcefields.py index 64d7f7d..4993099 100644 --- a/pylj/forcefields.py +++ b/pylj/forcefields.py @@ -7,12 +7,13 @@ class lennard_jones_base(object): Parameters ---------- - a_b: float, array_like - An array of length two consisting of the A and B parameters for the - 12-6 Lennard-Jones function - sigma_epsilon: float, array_like - An array of length two consisting of the sigma and epsilon parameters for the + constants: float, array_like + An array of length two consisting of the two parameters for the 12-6 Lennard-Jones function + a_b: bool + Controls whether parameters are a/b, default is false + sigma_epsilon: bool + Controls whether parameters are sigma/epsilon, default is false """ def __init__(self, constants, a_b = False, sigma_epsilon = False): @@ -70,7 +71,17 @@ def force(self, dr): return self.force def mixing(self, constants_2): + r""" Calculates mixing for two sets of constants + + ..math:: + \sigma_{12} = \frac{\sigma_1 + \sigma_2}{2} + \epsilon{12} = \sqrt{\epsilon_1 * \epsilon_2} + Attributes: + ---------- + constants_2: float, array_like + The second set of constantss + """ if self.type == 'a_b': a2 = constants_2[0] b2 = constants_2[1] @@ -167,6 +178,18 @@ def force(self, dr): return self.force def mixing(self, constants2): + r""" Calculates mixing for two sets of constants + + ..math:: + a_{12} = \sqrt{a_1 * a_2} + b_{12} = \sqrt{b_1 * b_2} + c_{12} = \sqrt{c_1 * c_2} + + Attributes: + ---------- + constants_2: float, array_like + The second set of constantss + """ self.a = np.sqrt(self.a*constants2[0]) self.b = np.sqrt(self.b*constants2[1]) self.c = np.sqrt(self.c*constants2[2]) From 98f228e3213eec9bb0cbb429bb9d3b8ddba9e706 Mon Sep 17 00:00:00 2001 From: maxdolan Date: Wed, 24 Jul 2024 09:38:30 +0100 Subject: [PATCH 10/18] reducing three LJ classes to two --- pylj/forcefields.py | 123 ++++++++++++++++----------------- pylj/tests/test_forcefields.py | 79 +++++++-------------- 2 files changed, 85 insertions(+), 117 deletions(-) diff --git a/pylj/forcefields.py b/pylj/forcefields.py index 4993099..a958bd3 100644 --- a/pylj/forcefields.py +++ b/pylj/forcefields.py @@ -1,73 +1,59 @@ import numpy as np -class lennard_jones_base(object): + +class lennard_jones_sigma_epsilon(object): r"""Calculate the energy or force for a pair of particles using the - Lennard-Jones forcefield. Either the a_b or sigma_epsilon constants - must be specified + Lennard-Jones (sigma/epsilon variant) forcefield. Parameters ---------- constants: float, array_like - An array of length two consisting of the two parameters for the - 12-6 Lennard-Jones function - a_b: bool - Controls whether parameters are a/b, default is false - sigma_epsilon: bool - Controls whether parameters are sigma/epsilon, default is false - """ - def __init__(self, constants, a_b = False, sigma_epsilon = False): - - if sigma_epsilon: - self.sigma = constants[0] - self.epsilon = constants[1] - self.a = 4 * self.epsilon * (self.sigma**12) - self.b = 4 * self.epsilon * (self.sigma**6) - self.type = 'sigma_epsilon' - - elif a_b: - self.a = constants[0] - self.b = constants[1] - self.sigma = (self.a / self.b)**(1/6) - self.epsilon = (self.b**2)/(4*self.a) - self.type = 'a_b' - + An array of length two consisting of the sigma (a) and epsilon (e) + parameters for the 12-6 Lennard-Jones function - def energy(self, dr ): + """ + def __init__(self, constants): + self.sigma = constants[0] + self.epsilon = constants[1] + + def energy(self, dr): r"""Calculate the energy for a pair of particles using the - Lennard-Jones (A/B variant) forcefield. + Lennard-Jones (sigma/epsilon variant) forcefield. .. math:: - E = \frac{A}{dr^{12}} - \frac{B}{dr^6} + E = \frac{4e*a^{12}}{dr^{12}} - \frac{4e*a^{6}}{dr^6} Attributes: ---------- dr (float): The distance between particles. - + Returns ------- float: array_like The potential energy between the particles. """ - self.energy = self.a * np.power(dr, -12) - (self.b * np.power(dr, -6)) - return self.energy + self.energy = 4 * self.epsilon * np.power(self.sigma, 12) * np.power(dr, -12) - ( + 4 * self.epsilon * np.power(self.sigma, 6) * np.power(dr, -6)) + return self.energy def force(self, dr): r"""Calculate the force for a pair of particles using the - Lennard-Jones (A/B variant) forcefield. + Lennard-Jones (sigma/epsilon variant) forcefield. .. math:: - f = \frac{12A}{dr^{13}} - \frac{6B}{dr^7} + f = \frac{48e*a^{12}}{dr^{13}} - \frac{24e*a^{6}}{dr^7} Attributes: ---------- dr (float): The distance between particles. - + Returns ------- float: array_like The force between the particles. """ - self.force = 12 * self.a * np.power(dr, -13) - (6 * self.b * np.power(dr, -7)) + self.force = 48 * self.epsilon * np.power(self.sigma, 12) * np.power( + dr, -13) - (24 * self.epsilon * np.power(self.sigma, 6) * np.power(dr, -7)) return self.force def mixing(self, constants_2): @@ -77,29 +63,24 @@ def mixing(self, constants_2): \sigma_{12} = \frac{\sigma_1 + \sigma_2}{2} \epsilon{12} = \sqrt{\epsilon_1 * \epsilon_2} - Attributes: + Parameters: ---------- constants_2: float, array_like The second set of constantss """ - if self.type == 'a_b': - a2 = constants_2[0] - b2 = constants_2[1] - sigma2 = (a2 / b2)**(1/6) - epsilon2 = (b2**2)/(4*a2) - - elif self.type == 'sigma_epsilon': - sigma2 = constants_2[0] - epsilon2 = constants_2[1] - + sigma2 = constants_2[0] + epsilon2 = constants_2[1] self.sigma = (self.sigma+sigma2)/2 self.epsilon = np.sqrt(self.epsilon * epsilon2) - self.a = 4 * self.epsilon * (self.sigma**12) - self.b = 4 * self.epsilon * (self.sigma**6) -class lennard_jones(lennard_jones_base): - r"""Calculate the energy or force for a pair of particles using the - Lennard-Jones (A/B variant) forcefield. Maps to lennard_jones_base class + +class lennard_jones(lennard_jones_sigma_epsilon): + r"""Converts a/b variant values to sigma/epsilon variant + then maps to lennard_jones_sigma_epsilon class + + ..math:: + \sigma = \frac{a}{b}^(\frac{1}{6}) + \sigma = \frace{b^2}{4*a} Parameters ---------- @@ -108,20 +89,34 @@ class lennard_jones(lennard_jones_base): parameters for the 12-6 Lennard-Jones function """ def __init__(self, constants): - super().__init__(constants, a_b = True) + self.a = constants[0] + self.b = constants[1] + sigma = (self.a / self.b)**(1/6) + epsilon = (self.b**2)/(4*self.a) + super().__init__([sigma, epsilon]) -class lennard_jones_sigma_epsilon(lennard_jones_base): - r"""Calculate the energy or force for a pair of particles using the - Lennard-Jones (sigma/epsilon variant) forcefield. Maps to lennard_jones_base class + def mixing(self, constants_2): + r"""Converts second set of a/b constants into sigma/epsilon + for use in mixing method. Then converts changed self sigma/epsilon + values back to a/b + + ..math:: + a = 4*\epsilon*(\sigma^12) + b = 4*\epsilon*(\sigma^6) + + Parameters: + ---------- + constants_2: float, array_like + The second set of constantss + """ + a2 = constants_2[0] + b2 = constants_2[1] + sigma2 = (a2 / b2)**(1/6) + epsilon2 = (b2**2)/(4*a2) + super().mixing([sigma2,epsilon2]) + self.a = 4 * self.epsilon * (self.sigma**12) + self.b = 4 * self.epsilon * (self.sigma**6) - Parameters - ---------- - constants: float, array_like - An array of length two consisting of the sigma (a) and epsilon (e) - parameters for the 12-6 Lennard-Jones function - """ - def __init__(self, constants): - super().__init__(constants, sigma_epsilon = True) class buckingham(object): r""" Calculate the energy or force for a pair of particles using the diff --git a/pylj/tests/test_forcefields.py b/pylj/tests/test_forcefields.py index 5e10981..32cce90 100644 --- a/pylj/tests/test_forcefields.py +++ b/pylj/tests/test_forcefields.py @@ -4,82 +4,63 @@ class TestForcefields(unittest.TestCase): - def test_lennard_jones_base(self): - a = forcefields.lennard_jones_base([1,2], a_b = True) - assert_almost_equal([a.sigma, a.epsilon],[0.8908987, 1]) - a.mixing([3,4]) - assert_almost_equal([a.a, a.b],[1.743954 , 2.8381294]) - b = forcefields.lennard_jones_base([1,2], sigma_epsilon = True) - assert_almost_equal([b.a, b.b], [8, 8]) - b.mixing([3,4]) - assert_almost_equal([b.sigma, b.epsilon],[2, 2.8284271]) - - def test_lennard_jones_energy(self): + def test_lennard_jones(self): a = forcefields.lennard_jones([1.0, 1.0]) assert_almost_equal(a.energy(2.0), -0.015380859) + assert_almost_equal(a.force(2.0), -0.045410156) + a.mixing([2.0, 3.0]) + assert_almost_equal([a.a, a.b], [1.4239324, 1.7379922]) b = forcefields.lennard_jones([1.0, 1.0]) assert_almost_equal(b.energy([2.0, 4.5],), [-0.015380859, -0.00012041]) + assert_almost_equal(b.force([2.0, 4.0]), [-0.045410156, -3.66032124e-04]) c = forcefields.lennard_jones([0.5, 3.5]) assert_almost_equal(c.energy([2.0, 4.5],), [-0.05456543, -0.00042149]) + assert_almost_equal(c.force([2.0, 4.5],), [-0.1633301, -0.000562]) d = forcefields.lennard_jones( [5.0, 3.5]) assert_almost_equal(d.energy([1.0, 1.5, 20.0]), [1.50000000, -0.268733500, -5.46874988e-08]) + assert_almost_equal(d.force([1.0, 1.5, 20.0]), [ 3.9000000e+01, -9.2078707e-01, -1.6406249e-08]) e = forcefields.lennard_jones([100.0, 300.0]) assert_almost_equal(e.energy([100.0, 200.0, 500.0]), [0, 0, 0]) + assert_almost_equal(e.force([100.0, 200.0, 500.0]), [0, 0, 0]) - def test_lennard_jones_force(self): - a = forcefields.lennard_jones([1.0, 1.0]) - assert_almost_equal(a.force(2.0), -0.045410156) - b = forcefields.lennard_jones([1.0, 1.0]) - assert_almost_equal(b.force([2.0, 4.0, 6.0]), [-0.045410156, -3.66032124e-04, -2.14325517e-05]) - c = forcefields.lennard_jones([1.5, 4.0]) - assert_almost_equal(c.force([2.0, 4.0, 6.0]), [-0.185302734, -1.46457553e-03, -8.57325038e-05]) - d = forcefields.lennard_jones([200.0, 500.0]) - assert_almost_equal(d.force([150.0, 300.0, 500.0]), [-1.7558299e-12, -1.3717421e-14, -3.8400000e-16]) - - def test_lennard_jones_sigma_epsilon_energy(self): + def test_lennard_jones_sigma_epsilon(self): a = forcefields.lennard_jones_sigma_epsilon([1.0, 0.25]) assert_almost_equal(a.energy(2.0), -0.015380859) + assert_almost_equal(a.force(2.0), -0.045410156) b = forcefields.lennard_jones_sigma_epsilon([1.0, 0.25]) assert_almost_equal(b.energy([2.0, 1.0]), [-0.015380859, 0]) + assert_almost_equal(b.force([2.0, 4.0]), [-0.0454102, -0.000366]) c = forcefields.lennard_jones_sigma_epsilon([0.5, 0.75]) assert_almost_equal(c.energy([2.0, 1.0, 1.5]), [-0.0007322, -0.0461425, -0.0041095]) + assert_almost_equal(c.force([2.0, 1.0, 1.5]), [-0.0021962, -0.2724609, -0.0164157]) d = forcefields.lennard_jones_sigma_epsilon([5e-10, 9e-9]) assert_almost_equal(d.energy([400.0, 500.0, 600.0]), [0, 0, 0]) + assert_almost_equal(d.force([400.0, 500.0, 600.0]), [0, 0, 0]) + e = forcefields.lennard_jones_sigma_epsilon([1.0, 1.0]) + e.mixing([4.0, 4.0]) + assert_almost_equal([e.sigma, e.epsilon], [2.5, 2.0]) - def test_lennard_jones_sigma_epsilon_force(self): - a = forcefields.lennard_jones_sigma_epsilon([1.0, 0.25]) - assert_almost_equal(a.force(2.0), -0.045410156) - b = forcefields.lennard_jones_sigma_epsilon([1.0, 0.25]) - assert_almost_equal(b.force([2.0, 4.0]), [-0.0454102, -0.000366]) - c = forcefields.lennard_jones_sigma_epsilon([3.0, 1.0]) - assert_almost_equal(c.force([3.0, 4.0]), [8.0, -0.6877549]) - - def test_buckingham_energy(self): + def test_buckingham(self): a = forcefields.buckingham([1.0, 1.0, 1.0]) assert_almost_equal(a.energy(2.0), 0.1197103832) + assert_almost_equal(a.force(2.0), 0.08846028324) + a.mixing([4.0, 4.0, 4.0]) + assert_almost_equal([a.a, a.b, a.c], [2.0, 2.0, 2.0]) b = forcefields.buckingham([1.0, 1.0, 1.0]) assert_almost_equal(b.energy([2.0]), 0.1197103832) + assert_almost_equal(b.force([2.0]), 0.08846028324) c = forcefields.buckingham([1.0, 1.0, 1.0]) assert_almost_equal(c.energy([2.0, 4.0]), [0.1197103832, 0.0180715]) + assert_almost_equal(c.force([2.0, 4.0]), [0.0884603, 0.0179494]) d = forcefields.buckingham([0.01, 0.01, 0.01]) assert_almost_equal(d.energy([2.0, 4.0, 5.0]), [0.0096457, 0.0096055, 0.0095117]) + assert_almost_equal(d.force([2.0, 4.0, 5.0]), [-3.7073013e-04, 9.2416835e-05, 9.4354942e-05]) - def test_buckingham_force(self): - a = forcefields.buckingham([1.0, 1.0, 1.0]) - assert_almost_equal(a.force(2.0), 0.08846028324) - b = forcefields.buckingham([1.0, 1.0, 1.0]) - assert_almost_equal(b.force([2.0]), 0.08846028324) - c = forcefields.buckingham([1.5, 0.1, 2.0]) - assert_almost_equal(c.force([2.0, 1.0, 4.0]), [0.0290596, -11.8642744, 0.0998156]) - - def test_buckingham_mixing(self): - a = forcefields.buckingham([1.0, 1.0, 1.0]) - a.mixing([4.0, 4.0, 4.0]) - assert_almost_equal([a.a, a.b, a.c], [2.0, 2.0, 2.0]) - - def test_square_well_energy(self): + def test_square_well(self): a = forcefields.square_well([1.0, 1.5, 2.0]) assert_equal(a.energy(2.0), -1.0) + with self.assertRaises(ValueError): + a.force() b = forcefields.square_well([1.0, 2.0, 1.25]) assert_equal(b.energy(0.5), float('inf')) c = forcefields.square_well([0.5, 1.5, 1.25]) @@ -91,13 +72,5 @@ def test_square_well_energy(self): f = forcefields.square_well([1.0, 1.5, 1.25], max_val=5000) assert_equal(f.energy([3.0, 3.0, 0.25]), [0, 0, 5000]) - def test_square_well_force(self): - a = forcefields.square_well([1.0, 1.5, 2.0]) - with self.assertRaises(ValueError): - a.force() - b = forcefields.square_well([1.0, 1.5, 2.0]) - with self.assertRaises(ValueError): - b.force() - if __name__ == '__main__': unittest.main(exit=False) From fcd5be2d8996d7faca2f3c6e10b0eb39cde4607b Mon Sep 17 00:00:00 2001 From: maxdolan Date: Wed, 24 Jul 2024 11:17:31 +0100 Subject: [PATCH 11/18] adding particle sizing for LJ --- pylj/forcefields.py | 1 + pylj/sample.py | 4 ++-- pylj/util.py | 13 ++++++++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/pylj/forcefields.py b/pylj/forcefields.py index a958bd3..3b66388 100644 --- a/pylj/forcefields.py +++ b/pylj/forcefields.py @@ -15,6 +15,7 @@ class lennard_jones_sigma_epsilon(object): def __init__(self, constants): self.sigma = constants[0] self.epsilon = constants[1] + self.point_size = 1.3e10 * self.sigma*(2**(1/6)) def energy(self, dr): r"""Calculate the energy for a pair of particles using the diff --git a/pylj/sample.py b/pylj/sample.py index ebf9f91..b1de4b2 100644 --- a/pylj/sample.py +++ b/pylj/sample.py @@ -497,10 +497,10 @@ def setup_cellview(ax, system, scale=1): # pragma: no cover # These numbers are chosen to scale the size of the particles nicely to # allow the particles to appear to interact appropriately mk = 6.00555e-8 / (system.box_length - 2.2727e-10) - 1e-10 / scale - for type in system.type_identifiers: + for index, type in enumerate(system.type_identifiers): xpos = system.particles["xposition"] * type ypos = system.particles["yposition"] * type - ax.plot(xpos, ypos, "o", markersize=mk, markeredgecolor="black") + ax.plot(xpos, ypos, "o", markersize=system.point_sizes[index], markeredgecolor="black") ax.set_xlim([0, system.box_length]) ax.set_ylim([0, system.box_length]) ax.set_xticks([]) diff --git a/pylj/util.py b/pylj/util.py index 861e287..bcf2cbc 100644 --- a/pylj/util.py +++ b/pylj/util.py @@ -51,13 +51,15 @@ def __init__( self.init_temp = temperature self.constants = constants self.mass = mass + self.forcefield = forcefield self.type_identifiers = None self.particle_list = None self.long_const = None self.types = None + self.point_sizes = None + self.setup_point_sizes() self.setup_type_identifiers() self.setup_types() - self.forcefield = forcefield if box_length <= 600: self.box_length = box_length * 1e-10 else: @@ -175,6 +177,15 @@ def setup_type_identifiers(self): i+=1 self.type_identifiers = type_identifiers + def setup_point_sizes(self): + """Sets point sizes for use in plotting + """ + point_sizes = [] + for pair in self.constants: + size = self.forcefield(pair).point_size + point_sizes.append(size) + self.point_sizes = point_sizes + def compute_force(self): """Maps to the compute_force function in either the comp (if Cython is installed) or the pairwise module and allows for a cleaner interface. From 265ce47ef8cf94af415bc3eb25b7ef57e7c46757 Mon Sep 17 00:00:00 2001 From: maxdolan Date: Wed, 24 Jul 2024 11:56:35 +0100 Subject: [PATCH 12/18] updated version uses multiple particle types --- .../simple_examples/molecular_dynamics.ipynb | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/examples/simple_examples/molecular_dynamics.ipynb b/examples/simple_examples/molecular_dynamics.ipynb index 946c156..73bbdd0 100644 --- a/examples/simple_examples/molecular_dynamics.ipynb +++ b/examples/simple_examples/molecular_dynamics.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -17,7 +17,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -60,20 +60,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "system = md_simulation(20, 1000, 100, 2500, 50)" + "system = md_simulation(20, 1000, 100, 5000, 25)" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, { "cell_type": "code", "execution_count": null, From 974f23f32e9e42852664803a143680fbdfbf4ef8 Mon Sep 17 00:00:00 2001 From: maxdolan Date: Thu, 25 Jul 2024 15:45:46 +0100 Subject: [PATCH 13/18] fixing buckingham to work for mixing --- pylj/forcefields.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pylj/forcefields.py b/pylj/forcefields.py index 3b66388..bc6743e 100644 --- a/pylj/forcefields.py +++ b/pylj/forcefields.py @@ -15,7 +15,7 @@ class lennard_jones_sigma_epsilon(object): def __init__(self, constants): self.sigma = constants[0] self.epsilon = constants[1] - self.point_size = 1.3e10 * self.sigma*(2**(1/6)) + self.point_size = 1.3e10 * (self.sigma*(2**(1/6))) def energy(self, dr): r"""Calculate the energy for a pair of particles using the @@ -134,6 +134,7 @@ def __init__(self, constants): self.a = constants[0] self.b = constants[1] self.c = constants[2] + self.point_size = 8 # Needs better solution relevant to constants def energy(self, dr): r"""Calculate the energy for a pair of particles using the @@ -151,7 +152,11 @@ def energy(self, dr): float: array_like The potential energy between the particles. """ - self.energy = self.a * np.exp(- np.multiply(self.b, dr)) - self.c / np.power(dr, 6) + energy = self.a * np.exp(- np.multiply(self.b, dr)) - self.c / np.power(dr, 6) + # Cut out infinite values where r = 0 + energy[energy > 10e300] = 0 + energy[energy < -10e300] = 0 + self.energy = energy return self.energy def force(self, dr): @@ -170,7 +175,11 @@ def force(self, dr): float: array_like The force between the particles. """ - self.force = self.a * self.b * np.exp(- np.multiply(self.b, dr)) - 6 * self.c / np.power(dr, 7) + force = self.a * self.b * np.exp(- np.multiply(self.b, dr)) - 6 * self.c / np.power(dr, 7) + # Cut out infinite values where r = 0 + force[force > 10e300] = 0 + force[force < -10e300] = 0 + self.force = force return self.force def mixing(self, constants2): @@ -210,6 +219,7 @@ def __init__(self, constants, max_val=np.inf): self.sigma = constants[1] self.lamda = constants[2] #Spelling as lamda not lambda to avoid calling python lambda function self.max_val = max_val + self.point_size = 10 def energy(self, dr): r'''Calculate the energy for a pair of particles using a From fe87f9f9d19bc3ee16cc86c7e8cfa0d49f6419c6 Mon Sep 17 00:00:00 2001 From: maxdolan Date: Sun, 28 Jul 2024 11:24:59 +0100 Subject: [PATCH 14/18] fixing tests --- pylj/forcefields.py | 12 ++++++++---- pylj/tests/test_util.py | 10 +++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/pylj/forcefields.py b/pylj/forcefields.py index bc6743e..4c5db74 100644 --- a/pylj/forcefields.py +++ b/pylj/forcefields.py @@ -154,8 +154,10 @@ def energy(self, dr): """ energy = self.a * np.exp(- np.multiply(self.b, dr)) - self.c / np.power(dr, 6) # Cut out infinite values where r = 0 - energy[energy > 10e300] = 0 - energy[energy < -10e300] = 0 + if type(dr) != float: + energy = np.array(energy) + energy[np.where(energy > 10e300)] = 0 + energy[np.where(energy < -10e300)] = 0 self.energy = energy return self.energy @@ -177,8 +179,10 @@ def force(self, dr): """ force = self.a * self.b * np.exp(- np.multiply(self.b, dr)) - 6 * self.c / np.power(dr, 7) # Cut out infinite values where r = 0 - force[force > 10e300] = 0 - force[force < -10e300] = 0 + if type(dr) != float: + force = np.array(force) + force[np.where(force > 10e300)] = 0 + force[np.where(force < -10e300)] = 0 self.force = force return self.force diff --git a/pylj/tests/test_util.py b/pylj/tests/test_util.py index df8c474..4c7c849 100644 --- a/pylj/tests/test_util.py +++ b/pylj/tests/test_util.py @@ -11,7 +11,7 @@ def test_system_square(self): 300, 8, mass=39.948, - constants=[1.363e-134, 9.273e-78], + constants=[[1.363e-134, 9.273e-78]], forcefield=ff.lennard_jones, ) assert_equal(a.number_of_particles, 2) @@ -34,7 +34,7 @@ def test_system_random(self): 8, init_conf="random", mass=39.948, - constants=[1.363e-134, 9.273e-78], + constants=[[1.363e-134, 9.273e-78]], forcefield=ff.lennard_jones, ) assert_equal(a.number_of_particles, 2) @@ -57,7 +57,7 @@ def test_system_too_big(self): 300, 1000, mass=39.948, - constants=[1.363e-134, 9.273e-78], + constants=[[1.363e-134, 9.273e-78]], forcefield=ff.lennard_jones, ) self.assertTrue( @@ -73,7 +73,7 @@ def test_system_too_small(self): 300, 2, mass=39.948, - constants=[1.363e-134, 9.273e-78], + constants=[[1.363e-134, 9.273e-78]], forcefield=ff.lennard_jones, ) self.assertTrue( @@ -89,7 +89,7 @@ def test_system_init_conf(self): 100, init_conf="horseradish", mass=39.948, - constants=[1.363e-134, 9.273e-78], + constants=[[1.363e-134, 9.273e-78]], forcefield=ff.lennard_jones, ) self.assertTrue( From a5243168f84e7d2a03322c38f5a5081e7776086b Mon Sep 17 00:00:00 2001 From: maxdolan Date: Sun, 28 Jul 2024 11:45:48 +0100 Subject: [PATCH 15/18] adding constants length checking to forcefields --- pylj/forcefields.py | 10 +++++++++- pylj/tests/test_forcefields.py | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pylj/forcefields.py b/pylj/forcefields.py index 4c5db74..e54fa41 100644 --- a/pylj/forcefields.py +++ b/pylj/forcefields.py @@ -13,6 +13,9 @@ class lennard_jones_sigma_epsilon(object): """ def __init__(self, constants): + if len(constants) != 2: + raise IndexError(f'There should be two constants per set, not {len(constants)}') + self.sigma = constants[0] self.epsilon = constants[1] self.point_size = 1.3e10 * (self.sigma*(2**(1/6))) @@ -90,6 +93,8 @@ class lennard_jones(lennard_jones_sigma_epsilon): parameters for the 12-6 Lennard-Jones function """ def __init__(self, constants): + if len(constants) != 2: + raise IndexError(f'There should be two constants per set, not {len(constants)}') self.a = constants[0] self.b = constants[1] sigma = (self.a / self.b)**(1/6) @@ -131,6 +136,8 @@ class buckingham(object): """ def __init__(self, constants): + if len(constants) != 3: + raise IndexError(f'There should be three constants per set, not {len(constants)}') self.a = constants[0] self.b = constants[1] self.c = constants[2] @@ -218,7 +225,8 @@ class square_well(object): ''' def __init__(self, constants, max_val=np.inf): - + if len(constants) != 3: + raise IndexError(f'There should be three constants per set, not {len(constants)}') self.epsilon = constants[0] self.sigma = constants[1] self.lamda = constants[2] #Spelling as lamda not lambda to avoid calling python lambda function diff --git a/pylj/tests/test_forcefields.py b/pylj/tests/test_forcefields.py index 32cce90..5d57326 100644 --- a/pylj/tests/test_forcefields.py +++ b/pylj/tests/test_forcefields.py @@ -22,6 +22,8 @@ def test_lennard_jones(self): e = forcefields.lennard_jones([100.0, 300.0]) assert_almost_equal(e.energy([100.0, 200.0, 500.0]), [0, 0, 0]) assert_almost_equal(e.force([100.0, 200.0, 500.0]), [0, 0, 0]) + with self.assertRaises(IndexError): + f = forcefields.lennard_jones([1.0, 1.0, 1.0]) def test_lennard_jones_sigma_epsilon(self): a = forcefields.lennard_jones_sigma_epsilon([1.0, 0.25]) @@ -39,6 +41,8 @@ def test_lennard_jones_sigma_epsilon(self): e = forcefields.lennard_jones_sigma_epsilon([1.0, 1.0]) e.mixing([4.0, 4.0]) assert_almost_equal([e.sigma, e.epsilon], [2.5, 2.0]) + with self.assertRaises(IndexError): + f = forcefields.lennard_jones_sigma_epsilon([1.0, 1.0, 1.0]) def test_buckingham(self): a = forcefields.buckingham([1.0, 1.0, 1.0]) @@ -55,6 +59,8 @@ def test_buckingham(self): d = forcefields.buckingham([0.01, 0.01, 0.01]) assert_almost_equal(d.energy([2.0, 4.0, 5.0]), [0.0096457, 0.0096055, 0.0095117]) assert_almost_equal(d.force([2.0, 4.0, 5.0]), [-3.7073013e-04, 9.2416835e-05, 9.4354942e-05]) + with self.assertRaises(IndexError): + e = forcefields.buckingham([1.0, 1.0]) def test_square_well(self): a = forcefields.square_well([1.0, 1.5, 2.0]) @@ -71,6 +77,8 @@ def test_square_well(self): assert_equal(e.energy([3.0, 3.0, 0.25]), [0, 0, float('inf')]) f = forcefields.square_well([1.0, 1.5, 1.25], max_val=5000) assert_equal(f.energy([3.0, 3.0, 0.25]), [0, 0, 5000]) + with self.assertRaises(IndexError): + g = forcefields.square_well([1.0, 1.0]) if __name__ == '__main__': unittest.main(exit=False) From 4e24d257b2893cec3a9a38176f8b5af25e378251 Mon Sep 17 00:00:00 2001 From: maxdolan Date: Mon, 29 Jul 2024 10:51:11 +0100 Subject: [PATCH 16/18] clearing cell outputs --- .../simple_examples/molecular_dynamics.ipynb | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/examples/simple_examples/molecular_dynamics.ipynb b/examples/simple_examples/molecular_dynamics.ipynb index 73bbdd0..f1878b5 100644 --- a/examples/simple_examples/molecular_dynamics.ipynb +++ b/examples/simple_examples/molecular_dynamics.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -17,7 +17,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -60,20 +60,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "system = md_simulation(20, 1000, 100, 5000, 25)" ] From a83197f676fb9646f01c9d504b3e7c450d40474e Mon Sep 17 00:00:00 2001 From: maxdolan Date: Tue, 30 Jul 2024 15:04:25 +0100 Subject: [PATCH 17/18] fixing minor print message mistkae --- examples/simple_examples/molecular_dynamics.ipynb | 4 ++-- pylj/pairwise.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/simple_examples/molecular_dynamics.ipynb b/examples/simple_examples/molecular_dynamics.ipynb index f1878b5..c748151 100644 --- a/examples/simple_examples/molecular_dynamics.ipynb +++ b/examples/simple_examples/molecular_dynamics.ipynb @@ -2,12 +2,12 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import warnings\n", - "from pylj import md, sample, pairwise\n", + "from pylj import md, sample\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import time\n", diff --git a/pylj/pairwise.py b/pylj/pairwise.py index 2426e89..06b885c 100644 --- a/pylj/pairwise.py +++ b/pylj/pairwise.py @@ -110,12 +110,14 @@ def update_accelerations(particles, f, m, dx, dy, dr): k = 0 for i in range(0, particles.size - 1): for j in range(i + 1, particles.size): + particles["xacceleration"][i] += second_law(f[k], m, dx[k], dr[k]) if f[k]!=0 else 0 particles["yacceleration"][i] += second_law(f[k], m, dy[k], dr[k]) if f[k]!=0 else 0 + particles["xacceleration"][j] -= second_law(f[k], m, dx[k], dr[k]) if f[k]!=0 else 0 particles["yacceleration"][j] -= second_law(f[k], m, dy[k], dr[k]) if f[k]!=0 else 0 k += 1 - + return particles @@ -187,7 +189,7 @@ def lennard_jones_force(A, B, dr): float: The force between the two particles. """ - xacceleration( + print( "pairwise.lennard_jones_energy has been deprecated, please use " "forcefields.lennard_jones with force=True instead" ) From 486123cf9d7f7ab5b73413cdcc95860f192498bd Mon Sep 17 00:00:00 2001 From: maxdolan Date: Tue, 30 Jul 2024 15:31:44 +0100 Subject: [PATCH 18/18] remove legacy mixing flag --- pylj/mc.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pylj/mc.py b/pylj/mc.py index 5e1ff5b..2532148 100644 --- a/pylj/mc.py +++ b/pylj/mc.py @@ -9,8 +9,7 @@ def initialise( init_conf, mass=39.948, constants=[[1.363e-134, 9.273e-78]], - forcefield=ff.lennard_jones, - mixing = False + forcefield=ff.lennard_jones ): """Initialise the particle positions (this can be either as a square or random arrangement), velocities (based on the temperature defined, and #