diff --git a/README.md b/README.md index 77d0dcbf8..3ec016a47 100644 --- a/README.md +++ b/README.md @@ -152,3 +152,20 @@ Frame selection can be implemented by dpdata.LabeledSystem('OUTCAR').sub_system([0,-1]).to_deepmd_raw('dpmd_raw') ``` by which only the first and last frames are dumped to `dpmd_raw`. + +## replicate +dpdata will create a super cell of the current atom configuration. +```python +dpdata.System('./POSCAR').replicate((1,2,3,) ) +``` +tuple(1,2,3) means don't copy atom configuration in x direction, make 2 copys in y direction, make 3 copys in z direction. + +## perturb +By the following example, each frame of the original system (`dpdata.System('./POSCAR')`) is perturbed to generate three new frames. For each frame, the cell is perturbed by 5% and the atom positions are perturbed by 0.6 Angstrom. `atom_pert_style` indicates that the perturbation to the atom positions is subject to normal distribution. Other available options to `atom_pert_style` are`uniform` (uniform in a ball), and `const` (uniform on a sphere). +```python +perturbed_system = dpdata.System('./POSCAR').perturb(pert_num=3, + cell_pert_fraction=0.05, + atom_pert_distance=0.6, + atom_pert_style='normal') +print(perturbed_system.data) +``` diff --git a/dpdata/system.py b/dpdata/system.py index aaba652b7..ebc38ea1b 100644 --- a/dpdata/system.py +++ b/dpdata/system.py @@ -579,6 +579,142 @@ def add_atom_names(self, atom_names): self.data['atom_names'].extend(atom_names) self.data['atom_numbs'].extend([0 for _ in atom_names]) + def replicate(self, ncopy): + """ + Replicate the each frame in the system in 3 dimensions. + Each frame in the system will become a supercell. + + Parameters + ---------- + ncopy : + list: [4,2,3] + or tuple: (4,2,3,) + make `ncopy[0]` copys in x dimensions, + make `ncopy[1]` copys in y dimensions, + make `ncopy[2]` copys in z dimensions. + + Returns + ------- + tmp : System + The system after replication. + """ + if len(ncopy) !=3: + raise RuntimeError('ncopy must be a list or tuple with 3 int') + for ii in ncopy: + if type(ii) is not int: + raise RuntimeError('ncopy must be a list or tuple must with 3 int') + + tmp = System() + nframes = self.get_nframes() + data = self.data + tmp.data['atom_names'] = list(np.copy(data['atom_names'])) + tmp.data['atom_numbs'] = list(np.array(np.copy(data['atom_numbs'])) * np.prod(ncopy)) + tmp.data['atom_types'] = np.sort(np.tile(np.copy(data['atom_types']),np.prod(ncopy))) + tmp.data['cells'] = np.copy(data['cells']) + for ii in range(3): + tmp.data['cells'][:,ii,:] *= ncopy[ii] + tmp.data['coords'] = np.tile(np.copy(data['coords']),tuple(ncopy)+(1,1,1)) + + for xx in range(ncopy[0]): + for yy in range(ncopy[1]): + for zz in range(ncopy[2]): + tmp.data['coords'][xx,yy,zz,:,:,:] += xx * np.reshape(data['cells'][:,0,:], [-1,1,3])\ + + yy * np.reshape(data['cells'][:,1,:], [-1,1,3])\ + + zz * np.reshape(data['cells'][:,2,:], [-1,1,3]) + tmp.data['coords'] = np.reshape(np.transpose(tmp.data['coords'], [3,4,0,1,2,5]), (nframes, -1 , 3)) + return tmp + + def perturb(self, + pert_num, + cell_pert_fraction, + atom_pert_distance, + atom_pert_style='normal'): + """ + Perturb each frame in the system randomly. + The cell will be deformed randomly, and atoms will be displaced by a random distance in random direction. + + Parameters + ---------- + pert_num : int + Each frame in the system will make `pert_num` copies, + and all the copies will be perturbed. + That means the system to be returned will contain `pert_num` * frame_num of the input system. + cell_pert_fraction : float + A fraction determines how much (relatively) will cell deform. + The cell of each frame is deformed by a symmetric matrix perturbed from identity. + The perturbation to the diagonal part is subject to a uniform distribution in [-cell_pert_fraction, cell_pert_fraction), + and the perturbation to the off-diagonal part is subject to a uniform distribution in [-0.5*cell_pert_fraction, 0.5*cell_pert_fraction). + atom_pert_distance: float + unit: Angstrom. A distance determines how far atoms will move. + Atoms will move about `atom_pert_distance` in random direction. + The distribution of the distance atoms move is determined by atom_pert_style + atom_pert_style : str + Determines the distribution of the distance atoms move is subject to. + Avaliable options are + - `'normal'`: the `distance` will be object to `chi-square distribution with 3 degrees of freedom` after normalization. + The mean value of the distance is `atom_pert_fraction*side_length` + - `'uniform'`: will generate uniformly random points in a 3D-balls with radius as `atom_pert_distance`. + These points are treated as vector used by atoms to move. + Obviously, the max length of the distance atoms move is `atom_pert_distance`. + - `'const'`: The distance atoms move will be a constant `atom_pert_distance`. + + Returns + ------- + perturbed_system : System + The perturbed_system. It contains `pert_num` * frame_num of the input system frames. + """ + perturbed_system = System() + nframes = self.get_nframes() + for ii in range(nframes): + for jj in range(pert_num): + tmp_system = self[ii].copy() + cell_perturb_matrix = get_cell_perturb_matrix(cell_pert_fraction) + tmp_system.data['cells'][0] = np.matmul(tmp_system.data['cells'][0],cell_perturb_matrix) + tmp_system.data['coords'][0] = np.matmul(tmp_system.data['coords'][0],cell_perturb_matrix) + for kk in range(len(tmp_system.data['coords'][0])): + atom_perturb_vector = get_atom_perturb_vector(atom_pert_distance, atom_pert_style) + tmp_system.data['coords'][0][kk] += atom_perturb_vector + tmp_system.rot_lower_triangular() + perturbed_system.append(tmp_system) + return perturbed_system + +def get_cell_perturb_matrix(cell_pert_fraction): + if cell_pert_fraction<0: + raise RuntimeError('cell_pert_fraction can not be negative') + e0 = np.random.rand(6) + e = e0 * 2 *cell_pert_fraction - cell_pert_fraction + cell_pert_matrix = np.array( + [[1+e[0], 0.5 * e[5], 0.5 * e[4]], + [0.5 * e[5], 1+e[1], 0.5 * e[3]], + [0.5 * e[4], 0.5 * e[3], 1+e[2]]] + ) + return cell_pert_matrix + +def get_atom_perturb_vector(atom_pert_distance, atom_pert_style='normal'): + random_vector = None + if atom_pert_distance < 0: + raise RuntimeError('atom_pert_distance can not be negative') + + if atom_pert_style == 'normal': + e = np.random.randn(3) + random_vector=(atom_pert_distance/np.sqrt(3))*e + elif atom_pert_style == 'uniform': + e = np.random.randn(3) + while np.linalg.norm(e) < 0.1: + e = np.random.randn(3) + random_unit_vector = e/np.linalg.norm(e) + v0 = np.random.rand(1) + v = np.power(v0,1/3) + random_vector = atom_pert_distance*v*random_unit_vector + elif atom_pert_style == 'const' : + e = np.random.randn(3) + while np.linalg.norm(e) < 0.1: + e = np.random.randn(3) + random_unit_vector = e/np.linalg.norm(e) + random_vector = atom_pert_distance*random_unit_vector + else: + raise RuntimeError('unsupported options atom_pert_style={}'.format(atom_pert_style)) + return random_vector class LabeledSystem (System): ''' diff --git a/tests/poscars/POSCAR.SiC b/tests/poscars/POSCAR.SiC new file mode 100644 index 000000000..0722aaec2 --- /dev/null +++ b/tests/poscars/POSCAR.SiC @@ -0,0 +1,16 @@ +C4 Si4 +1.0 +4.1454462863815970e+00 0.0000000000000000e+00 0.0000000000000000e+00 +1.3000000000000000e-01 3.9594462863815970e+00 0.0000000000000000e+00 +2.5000000000000000e-01 3.7000000000000000e-01 4.0400062863815970e+00 +C Si +4 4 +cartesian + -0.0666948568 0.0604660432 2.1539831432 + 1.9907431432 -0.0864349568 0.0663397432 + 0.0885598432 2.0783031432 0.0622547432 + 2.0324731432 2.0217031432 2.0253831432 + -0.0448649568 0.1153571432 0.0540263432 + 1.9664731432 1.9313831432 -0.1405848568 + 2.0807731432 0.0065058032 1.9956031432 + 0.0632185432 1.9999431432 2.0438931432 diff --git a/tests/poscars/POSCAR.SiC.const b/tests/poscars/POSCAR.SiC.const new file mode 100644 index 000000000..986356eb4 --- /dev/null +++ b/tests/poscars/POSCAR.SiC.const @@ -0,0 +1,16 @@ +C4 Si4 +1.0 +3.9448633066729739e+00 -3.6344794672422329e-18 1.1581293029516294e-17 +1.6817409381958889e-01 4.0320777239133783e+00 -8.7954588948930866e-19 +1.4551929935812052e-01 3.9989012843738053e-01 3.9977352939318109e+00 +C Si +4 4 +Cartesian + 0.0946327782 -0.2781320996 1.6927633385 + 2.1395921916 -0.5505295607 0.3561009428 + -0.2623111485 2.0403483142 0.5289485573 + 2.0291102621 2.0166734366 1.4189102698 + -0.4274609464 0.4275845699 -0.2870878166 + 1.8398859643 1.9175800522 -0.7344827910 + 1.5794721062 0.2698516856 1.5600233361 + 0.2272461445 2.4170250626 2.4545090907 diff --git a/tests/poscars/POSCAR.SiC.normal b/tests/poscars/POSCAR.SiC.normal new file mode 100644 index 000000000..e5e2be845 --- /dev/null +++ b/tests/poscars/POSCAR.SiC.normal @@ -0,0 +1,16 @@ +C4 Si4 +1.0 +4.0354487481064565e+00 1.1027270790560616e-17 2.5642993008475204e-17 +2.0693526054669642e-01 4.1066892997402196e+00 -8.6715682899078028e-18 +4.2891472979598610e-01 5.5796885749827474e-01 4.1100061517204542e+00 +C Si +4 4 +Cartesian + 0.2840021179 -0.6003817253 2.7192043932 + 1.7870636884 0.4043686026 -0.3468222936 + -0.4647946470 2.0243215781 -0.0624256251 + 2.1747504767 2.0704389063 2.4228997681 + -0.3817468326 0.0611851697 0.1116535817 + 2.1483270977 2.3528212071 -0.3719335435 + 2.4540854549 0.7297685673 1.8434611305 + 0.1075358778 2.0300985762 2.0687710181 diff --git a/tests/poscars/POSCAR.SiC.replicate123 b/tests/poscars/POSCAR.SiC.replicate123 new file mode 100644 index 000000000..ac911d463 --- /dev/null +++ b/tests/poscars/POSCAR.SiC.replicate123 @@ -0,0 +1,56 @@ +C24 Si24 +1.000000 + 4.14544629 0.00000000 0.00000000 + 0.26000000 7.91889257 0.00000000 + 0.75000000 1.11000000 12.12001886 +C Si +24 24 +Cartesian + -0.06669486 0.06046604 2.15398314 + 0.18330514 0.43046604 6.19398943 + 0.43330514 0.80046604 10.23399572 + 0.06330514 4.01991233 2.15398314 + 0.31330514 4.38991233 6.19398943 + 0.56330514 4.75991233 10.23399572 + 1.99074314 -0.08643496 0.06633974 + 2.24074314 0.28356504 4.10634603 + 2.49074314 0.65356504 8.14635232 + 2.12074314 3.87301133 0.06633974 + 2.37074314 4.24301133 4.10634603 + 2.62074314 4.61301133 8.14635232 + 0.08855984 2.07830314 0.06225474 + 0.33855984 2.44830314 4.10226103 + 0.58855984 2.81830314 8.14226732 + 0.21855984 6.03774943 0.06225474 + 0.46855984 6.40774943 4.10226103 + 0.71855984 6.77774943 8.14226732 + 2.03247314 2.02170314 2.02538314 + 2.28247314 2.39170314 6.06538943 + 2.53247314 2.76170314 10.10539572 + 2.16247314 5.98114943 2.02538314 + 2.41247314 6.35114943 6.06538943 + 2.66247314 6.72114943 10.10539572 + -0.04486496 0.11535714 0.05402634 + 0.20513504 0.48535714 4.09403263 + 0.45513504 0.85535714 8.13403892 + 0.08513504 4.07480343 0.05402634 + 0.33513504 4.44480343 4.09403263 + 0.58513504 4.81480343 8.13403892 + 1.96647314 1.93138314 -0.14058486 + 2.21647314 2.30138314 3.89942143 + 2.46647314 2.67138314 7.93942772 + 2.09647314 5.89082943 -0.14058486 + 2.34647314 6.26082943 3.89942143 + 2.59647314 6.63082943 7.93942772 + 2.08077314 0.00650580 1.99560314 + 2.33077314 0.37650580 6.03560943 + 2.58077314 0.74650580 10.07561572 + 2.21077314 3.96595209 1.99560314 + 2.46077314 4.33595209 6.03560943 + 2.71077314 4.70595209 10.07561572 + 0.06321854 1.99994314 2.04389314 + 0.31321854 2.36994314 6.08389943 + 0.56321854 2.73994314 10.12390572 + 0.19321854 5.95938943 2.04389314 + 0.44321854 6.32938943 6.08389943 + 0.69321854 6.69938943 10.12390572 diff --git a/tests/poscars/POSCAR.SiC.uniform b/tests/poscars/POSCAR.SiC.uniform new file mode 100644 index 000000000..9a6214267 --- /dev/null +++ b/tests/poscars/POSCAR.SiC.uniform @@ -0,0 +1,16 @@ +C4 Si4 +1.0 +4.0817852422279959e+00 -8.0334347485565818e-20 1.4195449766884313e-17 +1.2315711101650186e-01 3.7860044730134366e+00 -1.7522489439993958e-18 +4.0639226354941010e-01 3.2444019947671882e-01 4.2124083393682454e+00 +C Si +4 4 +Cartesian + -0.1022885809 0.5553879154 2.3401729917 + 1.4574918226 -0.0391762715 0.0190932943 + -0.0904993945 1.7684440711 -0.1710987880 + 2.0638159391 2.1681985074 2.4580083474 + -0.3261354126 0.1759255681 0.2236316249 + 2.0738703301 2.3024191125 -0.5031567813 + 2.3488610334 0.3825072268 1.7330866249 + -0.0534780340 1.5938442575 1.8486892947 diff --git a/tests/test_perturb.py b/tests/test_perturb.py new file mode 100644 index 000000000..e34626aab --- /dev/null +++ b/tests/test_perturb.py @@ -0,0 +1,137 @@ +import os +import numpy as np +import unittest +from context import dpdata +from comp_sys import CompSys + +from unittest.mock import Mock +from unittest.mock import patch, MagicMock + +class NormalGenerator(object): + def __init__(self): + self.randn_generator = self.get_randn_generator() + self.rand_generator = self.get_rand_generator() + def randn(self,number): + return next(self.randn_generator) + def rand(self,number): + return next(self.rand_generator) + @staticmethod + def get_randn_generator(): + data = np.asarray([ + [ 0.71878148, -2.20667426, 1.49373955], + [-0.42728113, 1.43836059, -1.17553854], + [-1.70793073, -0.39588759, -0.40880927], + [ 0.17078291, -0.34856352, 1.04307936], + [-0.99103413, -0.1886479, 0.13813131], + [ 0.5839343, 1.04612646, -0.62631026], + [ 0.9752889, 1.85932517, -0.47875828], + [-0.23977172, -0.38373444, -0.04375488]]) + count = 0 + while True: + yield data[count] + count +=1 + + @staticmethod + def get_rand_generator(): + yield np.asarray([0.23182233, 0.87106847, 0.68728511, 0.94180274, 0.92860453, 0.69191187]) + +class UniformGenerator(object): + def __init__(self): + self.randn_generator = self.get_randn_generator() + self.rand_generator = self.get_rand_generator() + def randn(self,number): + return next(self.randn_generator) + def rand(self,number): + return next(self.rand_generator) + + @staticmethod + def get_randn_generator(): + data = [[-0.19313281, 0.80194715, 0.14050915], + [-1.47859926, 0.12921667, -0.17632456], + [-0.60836805, -0.7700423, -0.8386948 ], + [-0.03236753, 0.36690245, 0.5041072 ], + [-1.59366933, 0.37069227, 0.89608291], + [ 0.18165617, 0.53875315, -0.42233955], + [ 0.74052496, 1.26627555, -1.12094823], + [-0.89610092, -1.44247021, -1.3502529 ]] + yield np.asarray([0.0001,0.0001,0.0001]) # test for not using small vector + count = 0 + while True: + yield data[count] + count +=1 + + @staticmethod + def get_rand_generator(): + data = np.asarray([[0.71263084], [0.61339295], + [0.22948181], [0.36087632], + [0.17582222], [0.97926742], + [0.84706761], [0.44495513]]) + + yield np.asarray([0.34453551, 0.0618966, 0.9327273, 0.43013654, 0.88624993, 0.48827425]) + count =0 + while True: + yield np.asarray(data[count]) + count+=1 + +class ConstGenerator(object): + def __init__(self): + self.randn_generator = self.get_randn_generator() + self.rand_generator = self.get_rand_generator() + def randn(self,number): + return next(self.randn_generator) + def rand(self,number): + return next(self.rand_generator) + + @staticmethod + def get_randn_generator(): + data = np.asarray([[ 0.95410606, -1.62338002, -2.05359934], + [ 0.69213769, -1.26008667, 0.77970721], + [-1.77926476, -0.39227219, 2.31677298], + [ 0.08785233, -0.03966649, -0.45325656], + [-0.53860887, 0.42536802, -0.46167309], + [-0.26865791, -0.19901684, -2.51444768], + [-0.31627314, 0.22076982, -0.36032225], + [0.66731887, 1.2505806, 1.46112938]]) + yield np.asarray([0.0001,0.0001,0.0001]) # test for not using small vector + count = 0 + while True: + yield data[count] + count +=1 + + @staticmethod + def get_rand_generator(): + yield np.asarray([0.01525907, 0.68387374, 0.39768541, 0.55596047, 0.26557088, 0.60883073]) + +# %% +class TestPerturbNormal(unittest.TestCase, CompSys): + @patch('numpy.random') + def setUp (self, random_mock): + random_mock.rand = NormalGenerator().rand + random_mock.randn = NormalGenerator().randn + system_1_origin = dpdata.System('poscars/POSCAR.SiC',fmt='vasp/poscar') + self.system_1 = system_1_origin.perturb(1,0.05,0.6,'normal') + self.system_2 = dpdata.System('poscars/POSCAR.SiC.normal',fmt='vasp/poscar') + self.places = 6 + +class TestPerturbUniform(unittest.TestCase, CompSys): + @patch('numpy.random') + def setUp (self, random_mock) : + random_mock.rand = UniformGenerator().rand + random_mock.randn = UniformGenerator().randn + system_1_origin = dpdata.System('poscars/POSCAR.SiC',fmt='vasp/poscar') + self.system_1 = system_1_origin.perturb(1,0.05,0.6,'uniform') + self.system_2 = dpdata.System('poscars/POSCAR.SiC.uniform',fmt='vasp/poscar') + self.places = 6 + +class TestPerturbConst(unittest.TestCase, CompSys): + @patch('numpy.random') + def setUp (self, random_mock) : + random_mock.rand = ConstGenerator().rand + random_mock.randn = ConstGenerator().randn + system_1_origin = dpdata.System('poscars/POSCAR.SiC',fmt='vasp/poscar') + self.system_1 = system_1_origin.perturb(1,0.05,0.6,'const') + self.system_2 = dpdata.System('poscars/POSCAR.SiC.const',fmt='vasp/poscar') + self.places = 6 + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_replicate.py b/tests/test_replicate.py new file mode 100644 index 000000000..18eba3d96 --- /dev/null +++ b/tests/test_replicate.py @@ -0,0 +1,22 @@ +import os +import numpy as np +import unittest +from context import dpdata +from comp_sys import CompSys + +class TestReplicate123(unittest.TestCase, CompSys): + def setUp (self) : + system_1_origin = dpdata.System('poscars/POSCAR.SiC',fmt='vasp/poscar') + self.system_1 = system_1_origin.replicate((1,2,3,)) + self.system_2 = dpdata.System('poscars/POSCAR.SiC.replicate123',fmt='vasp/poscar') + self.places = 6 + +class TestReplicate123_not_change_origin(unittest.TestCase, CompSys): + def setUp (self) : + self.system_1 = dpdata.System('poscars/POSCAR.SiC',fmt='vasp/poscar') + self.system_1.replicate((1,2,3,)) + self.system_2 = dpdata.System('poscars/POSCAR.SiC',fmt='vasp/poscar') + self.places = 6 + +if __name__ == '__main__': + unittest.main()