Coverage for gamdpy/configuration/Configuration.py: 80%

360 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1import numpy as np 

2import numba 

3import math 

4from numba import cuda 

5from .colarray import colarray 

6from ..simulation_boxes import Orthorhombic, LeesEdwards 

7from .topology import Topology, duplicate_topology, replicate_topologies 

8from ..simulation.get_default_compute_flags import get_default_compute_flags 

9 

10# IO 

11import h5py 

12import gzip 

13 

14# TODO: add possibility of "with ... as conf:" TypeError: 'Configuration' object does not support the context manager protocol 

15 

16class Configuration: 

17 """ The configuration class 

18 

19 Store particle vectors (positions, velocities, forces) and scalars (energy, virial, mass ...). 

20 Also store particle type, image coordinates, and the simulation box. 

21 

22 Parameters 

23 ---------- 

24 D : int 

25 Spatial dimension for the configuration. 

26  

27 N : int [Optional] 

28 Number of particles.  

29 If not set, this will be determined the first time particle data is written to the configuration.  

30  

31 Examples 

32 ------- 

33 

34 >>> import gamdpy as gp 

35 >>> conf = gp.Configuration(D=3, N=1000) 

36 >>> print(conf.vector_columns) # Print names of vector columns 

37 ['r', 'v', 'f'] 

38 >>> print(conf.scalar_columns) # Print names of scalar columns 

39 ['U', 'W', 'K', 'm'] 

40 >>> print(conf['r'].shape) # Vectors are stored as (N, D) numpy arrays 

41 (1000, 3) 

42 >>> print(conf['m'].shape) # Scalars are stored as (N,) numpy arrays 

43 (1000,) 

44 

45 

46 Data can be accessed via string keys (similar to dataframes in pandas): 

47 

48 >>> conf['r'] = np.ones((1000, 3)) 

49 >>> conf['v'] = 2 # Broadcast by numpy to correct shape 

50 >>> print(conf['r'] + 0.01*conf['v']) 

51 [[1.02 1.02 1.02] 

52 [1.02 1.02 1.02] 

53 [1.02 1.02 1.02] 

54 ... 

55 [1.02 1.02 1.02] 

56 [1.02 1.02 1.02] 

57 [1.02 1.02 1.02]] 

58 

59 

60 A configuration can be specified without setting the number particles, N. 

61 In that case N is determined the first time the particle data is written to the configuration: 

62 

63 >>> import numpy as np 

64 >>> conf = gp.Configuration(D=3) 

65 >>> conf['r'] = np.zeros((400, 3)) 

66 >>> print(conf['r'].shape) 

67 (400, 3) 

68 

69 """ 

70 

71 scalar_parameters = ['m'] 

72 scalar_computables_interactions = ['U', 'W', 'lapU'] 

73 scalar_computables_integrator = ['K', 'Fsq'] 

74 scalar_decriptions = {'m': 'Particle mass.', 

75 'U': 'Potential energy.', 

76 'W': 'Virial.', 

77 'lapU': 'Laplace(U).', 

78 'K': 'Kinetic energy.', 

79 'Fsq': 'Squared length of force vector.', 

80 } 

81 

82 

83 def __init__(self, D: int, N: int = None, type_names=None, compute_flags=None, ftype=np.float32, itype=np.int32) -> None: 

84 self.D = D 

85 self.N = N 

86 

87 self.type_names = type_names 

88 self.index_from_type_name = {} 

89 if type_names: 

90 for index, type_name in enumerate(type_names): 

91 self.index_from_type_name[type_name] = index 

92 

93 self.compute_flags = get_default_compute_flags() 

94 if compute_flags != None: 

95 # only keys present in the default are processed 

96 for k in compute_flags: 

97 if k in self.compute_flags: 

98 self.compute_flags[k] = compute_flags[k] 

99 else: 

100 raise ValueError('Unknown key in compute_flags:%s' %k) 

101 

102 self.vector_columns = ['r', 'v', 'f'] # Should be user modifiable 

103 if self.compute_flags['stresses']: 

104 if self.D > 4: 

105 raise ValueError("compute_flags['stresses'] should not be set for D>4") 

106 self.vector_columns += ['sx', 'sy', 'sz','sw'][:self.D] 

107 

108 

109 self.num_cscalars = 0 

110 self.sid = {} 

111 self.scalar_columns = [] 

112 sid_index = 0 

113 

114 for label in self.scalar_computables_interactions: 

115 if self.compute_flags[label]: 

116 self.sid[label] = sid_index 

117 self.scalar_columns.append(label) 

118 sid_index += 1 

119 self.num_cscalars += 1 

120 

121 for label in self.scalar_computables_integrator: 

122 if self.compute_flags[label]: 

123 self.sid[label] = sid_index 

124 self.scalar_columns.append(label) 

125 sid_index += 1 

126 

127 

128 for label in self.scalar_parameters: 

129 self.sid[label] = sid_index 

130 self.scalar_columns.append(label) 

131 sid_index += 1 

132 

133 self.simbox = None 

134 self.topology = Topology() 

135 self.ptype_function = self.make_ptype_function() 

136 self.ftype = ftype 

137 self.itype = itype 

138 if self.N != None: 

139 self.__allocate_arrays() 

140 

141 def __allocate_arrays(self): 

142 self.vectors = colarray(self.vector_columns, size=(self.N, self.D), dtype=self.ftype) 

143 self.scalars = np.zeros((self.N, len(self.scalar_columns)), dtype=self.ftype) 

144 self.r_im = np.zeros((self.N, self.D), dtype=self.itype) # Move to vectors 

145 self.ptype = np.zeros(self.N, dtype=self.itype) # Move to scalars 

146 return 

147 

148 def __repr__(self): 

149 return f'Configuration(D={self.D}, N={self.N}, compute_flags={self.compute_flags})' 

150 

151 def __code__(self): 

152 import sys 

153 np.set_printoptions(threshold=sys.maxsize) 

154 code_str = "# Define configuration class\n" 

155 code_str += f"from gamdpy import Configuration\n" 

156 code_str += f"configuration = Configuration(D={self.D}, N={self.N}, compute_flags={self.compute_flags})\n" 

157 # Following part needs to be done with a read function from the .h5 

158 for key in self.vector_columns: 

159 code_str += f"configuration['{key}'] = {self[key]}\n" 

160 for key in self.scalar_columns: 

161 code_str += f"configuration['{key}'] = {self[key]}\n" 

162 return code_str 

163 

164 def __str__(self): 

165 if self.N == None: 

166 return f'{self.D} dimensional configuration. Particles not yet assigned.' 

167 str = f'{self.N} particles in {self.D} dimensions. Number density (atomic): {self.N/self.get_volume():.3f}' 

168 num_types = np.max(self.ptype)+1 

169 if num_types==1: 

170 str += '. Single component. ' 

171 else: 

172 str += f'. {num_types} components with fractions ' 

173 for ptype in range(num_types): 

174 str += f'{np.mean(self.ptype==ptype):.3f}, ' 

175 str += '\nCurrent scalar data per particle:' 

176 for key in self.sid: 

177 str += f'\n{key+",":5} {np.mean(self.scalars[:,self.sid[key]]):.3f}' 

178 if key in self.scalar_decriptions: 

179 str += '\t' + self.scalar_decriptions[key] 

180 return str 

181 

182 def __setitem__(self, key, data): 

183 if self.N is None: # First time setting particle data, so allocate arrays 

184 if type(data) != np.ndarray: 

185 raise (TypeError)( 

186 'Number of particles, N, not determined yet, so assignment needs to be with a numpy array') 

187 self.N = data.shape[0] 

188 self.__allocate_arrays() 

189 

190 if key in self.vector_columns: 

191 self.__set_vector(key, data) 

192 return 

193 if key in self.scalar_columns: 

194 self.__set_scalar(key, data) 

195 return 

196 raise ValueError(f'Unknown key {key}. Vectors: {self.vector_columns}, Scalars: {self.scalar_columns}') 

197 

198 def __set_vector(self, key: str, data: np.ndarray) -> None: 

199 """ Set new vector data """ 

200 

201 if type(data) == np.ndarray: # Allow for possibility of using scalar float, which is then broadcast by numpy 

202 N, D = data.shape 

203 if N != self.N: 

204 raise ValueError(f'Inconsistent number of particles, {N} <> {self.N}') 

205 if D != self.D: 

206 raise ValueError(f'Inconsistent number of dimensions, {D} <> {self.D}') 

207 self.vectors[key] = data 

208 return 

209 

210 def __set_scalar(self, key: str, data) -> None: 

211 """ Set new scalar data """ 

212 

213 if type(data) == np.ndarray: # Allow for possibility of using scalar float, which is then broadcast by numpy 

214 N, = data.shape 

215 if N != self.N: 

216 raise ValueError(f'Inconsistent number of particles, {N} <> {self.N}') 

217 self.scalars[:, self.sid[key]] = data 

218 return 

219 

220 def __getitem__(self, key): 

221 if key in self.vector_columns: 

222 return self.vectors[key] 

223 if key in self.scalar_columns: 

224 return self.scalars[:, self.sid[key]] 

225 raise ValueError(f'Unknown key {key}. Vectors: {self.vector_columns}, Scalars: {self.scalar_columns}') 

226 

227 def copy_to_device(self): 

228 """ Copy all data to device memory """ 

229 self.d_vectors = cuda.to_device(self.vectors.array) 

230 self.d_scalars = cuda.to_device(self.scalars) 

231 self.d_r_im = cuda.to_device(self.r_im) 

232 self.d_ptype = cuda.to_device(self.ptype) 

233 self.simbox.copy_to_device() 

234 return 

235 

236 def copy_to_host(self): 

237 """ Copy all data to host memory """ 

238 self.vectors.array = self.d_vectors.copy_to_host() 

239 self.scalars = self.d_scalars.copy_to_host() 

240 self.r_im = self.d_r_im.copy_to_host() 

241 self.ptype = self.d_ptype.copy_to_host() 

242 self.simbox.copy_to_host() 

243 return 

244 

245 def make_ptype_function(self) -> callable: 

246 def ptype_function(pid, ptype_array): 

247 ptype = ptype_array[pid] # Default: read from ptype_array 

248 return ptype 

249 

250 return ptype_function 

251 

252 def get_potential_energy(self) -> float: 

253 """ Get total potential energy of the configuration """ 

254 return float(np.sum(self['U'])) 

255 

256 def get_volume(self): 

257 """ Get volume of simulation box associated with configuration """ 

258 return self.simbox.get_volume() 

259 

260 def set_kinetic_temperature(self, temperature, ndofs=None): 

261 if ndofs is None: 

262 ndofs = self.D * (self.N - 1) 

263 

264 T_ = np.sum(np.dot(self['m'], np.sum(self['v'] ** 2, axis=1))) / ndofs 

265 if T_ == 0: 

266 raise ValueError('Cannot rescale velocities when all equal to zero') 

267 self['v'] *= (temperature / T_) ** 0.5 

268 

269 def randomize_velocities(self, temperature, seed=None, ndofs=None): 

270 """ Randomize velocities according to a given temperature. If T <= 0, set all velocities to zero. """ 

271 if self.D is None: 

272 raise ValueError('Cannot randomize velocities. Start by assigning positions.') 

273 masses = self['m'] 

274 if np.any(masses == 0): 

275 raise ValueError('Cannot randomize velocities when any mass is zero') 

276 if temperature > 0.0: 

277 self['v'] = generate_random_velocities(self.N, self.D, T=temperature, seed=seed, m=self['m']) 

278 # rescale to get the kinetic temperature exactly right 

279 self.set_kinetic_temperature(temperature=temperature, ndofs=ndofs) 

280 else: 

281 self['v'] = np.zeros((self.N, self.D), np.float32) 

282 

283 def make_lattice(self, unit_cell: dict, cells: list, rho: float = None) -> None: 

284 """ Generate a lattice configuration 

285 

286 The lattice is constructed by replicating the unit cell in all directions. 

287 Unit cell is a dictonary with `fractional_coordinates` for particles, and 

288 the `lattice_constants` as a list of unit cell lengths in all directions. 

289 

290 Unit cells are available in gamdpy.unit_cells 

291 

292 Example 

293 ------- 

294 

295 >>> import gamdpy as gp 

296 >>> conf = gp.Configuration(D=3) 

297 >>> conf.make_lattice(gp.unit_cells.FCC, cells=[8, 8, 8], rho=1.0) 

298 >>> print(gp.unit_cells.FCC) # Example of a unit cell dict 

299 {'fractional_coordinates': [[0.0, 0.0, 0.0], [0.5, 0.5, 0.0], [0.5, 0.0, 0.5], [0.0, 0.5, 0.5]], 'lattice_constants': [1.0, 1.0, 1.0]} 

300 

301 """ 

302 from .make_lattice import make_lattice 

303 positions, box_vector = make_lattice(unit_cell=unit_cell, cells=cells, rho=rho) 

304 self['r'] = positions 

305 self.simbox = Orthorhombic(self.D, box_vector) 

306 return 

307 

308 def make_positions(self, N, rho): 

309 """ 

310 Generate particle positions in D dimensions. 

311 

312 Positions are generated in a simple cubic configuration in D dimensions. 

313 Takes the number of particles N and the density rho as inputs 

314 

315 Example 

316 ------- 

317 

318 >>> import gamdpy as gp 

319 >>> conf = gp.Configuration(D=3) 

320 >>> conf.make_positions(N=27, rho=0.2) 

321 """ 

322 

323 D = self.D 

324 part_per_line = np.ceil(pow(N, 1./D)) 

325 

326 box_length = pow(N/rho, 1./D) 

327 box_vector = np.array(D*[box_length]) 

328 

329 index = 0 

330 x = [] # empty list 

331 

332 # This loop places particles in a simple cubic configuration 

333 # The first particle is in D*[0] 

334 while index < N: 

335 dcurrent = D - 1 

336 i_d = D*[float(0)] 

337 i_d[dcurrent] = (index / pow(part_per_line, dcurrent)) 

338 rest = index % (pow(part_per_line, dcurrent)) 

339 while dcurrent != 0: 

340 dcurrent = dcurrent - 1 

341 i_d[dcurrent] = (rest/pow(part_per_line,dcurrent)) 

342 rest = index % (pow(part_per_line, dcurrent)) 

343 x.append(i_d) 

344 index = index + 1 

345 pos = np.array(x) 

346 # Centering the array 

347 dcurrent = 1 

348 remove = 0 

349 while dcurrent < D: 

350 remove += D**(D-dcurrent) 

351 pos[:, dcurrent] -= remove/N 

352 dcurrent = dcurrent + 1 

353 pos -= np.array(D*[int(0.5*part_per_line)]) # center cube at 0 

354 # Scaling for density 

355 pos *= box_length/part_per_line 

356 # Saving to Configuration object 

357 self['r'] = pos 

358 self.simbox = Orthorhombic(self.D, box_vector) 

359 # Check all particles are in the box (-L/2, L/2) 

360 assert np.any(np.abs(pos))<0.5*box_length 

361 

362 return 

363 

364 def atomic_scale(self, density): 

365 actual_rho = self.N / self.get_volume() 

366 scale_factor = (actual_rho / density)**(1/3) 

367 self.vectors['r'] *= scale_factor 

368 self.simbox.scale(scale_factor) 

369 

370 

371 def save(self, output: h5py.File, group_name: str, mode: str="w", include_topology: bool=False) -> None: 

372 """ Write a configuration to a HDF5 file 

373  

374 Parameters 

375 ---------- 

376 

377 configuration : gamdpy.Configuration 

378 a gamdpy configuration object 

379 

380 output : h5py.File 

381 h5 file 

382 

383 group_name : str 

384 name of the group which will be created in the h5 and in which 

385 the configuration will be saved 

386 

387 mode: str 

388 default value is "w" and corresponds to replacing existing dataset 

389 

390 Example 

391 ------- 

392 

393 >>> import os 

394 >>> import h5py 

395 >>> import gamdpy as gp 

396 >>> conf = gp.Configuration(D=3) 

397 >>> conf.make_positions(N=10, rho=1.0) 

398 >>> conf.save(output=h5py.File("final.h5", "w"), group_name="configuration", mode="w") 

399 >>> os.remove("final.h5") # Removes file (for doctests) 

400 >>> with h5py.File("manyconfs.h5", "a") as fout:  

401 ... conf.save(output=fout, group_name="restarts/restart0000", mode="w") 

402 ... conf.save(output=fout, group_name="restarts/restart0001", mode="w") 

403 ... conf.save(output=fout, group_name="restarts/restart0002", mode="w") 

404 >>> os.remove("manyconfs.h5") # Removes file (for doctests) 

405 

406 """ 

407 

408 # Sanity: 

409 #print(f"output {isinstance(output, h5py.File)} {output}") 

410 #print(f"group_name {isinstance(group_name, str)} {group_name}") 

411 # Creating group group_name in h5 root 

412 if group_name in output.keys() and mode=="w": 

413 print(f"{group_name} already present in h5 root, replacing it") 

414 del output[f'{group_name}'] 

415 output.create_group(group_name) 

416 # Checks if group group_name exists in case mode="append" 

417 elif group_name not in output.keys() and mode=="a": 

418 output.create_group(group_name) 

419 elif group_name not in output.keys() and mode=="w": 

420 output.create_group(group_name) 

421 elif group_name in output.keys() and mode=="a": 

422 print(f"append data to {group_name} in h5 root") 

423 else: 

424 raise ValueError("Unexpected combination of input in save method of Configuration") 

425 

426 # Save attributes of group group_name 

427# output[group_name].attrs['simbox'] = self.simbox.get_lengths() 

428 

429 # Saving vectors separately 

430 #output[group_name].create_dataset('r', data=self['r'], dtype=np.float32) 

431 #output[f"{group_name}/r"].attrs['simbox'] = self.simbox.get_lengths() 

432 #output[group_name].create_dataset('v', data=self['v'], dtype=np.float32) 

433 #output[group_name].create_dataset('f', data=self['f'], dtype=np.float32) 

434 

435 # Saving vectors all together 

436 #output[group_name].create_dataset('vectors', data=np.hstack([self['r'], self['v'], self['f']]), dtype=np.float32) 

437 output[group_name].create_dataset('vectors', data=self.vectors.array, dtype=np.float32) 

438 output[f"{group_name}/vectors"].attrs['vector_columns'] = self.vector_columns 

439 

440 # Saving other things 

441 output[group_name].create_dataset('ptype', data=self.ptype, dtype=np.int32) 

442 #output[group_name].create_dataset('m', data=self['m'], dtype=np.float32) # included in scalars 

443 output[group_name].create_dataset('r_im', data=self.r_im, dtype=np.int32) 

444 output[group_name].create_dataset('scalars', data=self.scalars, dtype=np.float32) 

445 output[f"{group_name}/scalars"].attrs['scalar_columns'] = self.scalar_columns 

446 

447 # save simulation box 

448 output[group_name].attrs['simbox_name'] = self.simbox.get_name() 

449 #output[group_name].attrs['simbox_data'] = self.simbox.get_lengths() 

450 output[group_name].attrs['simbox_data'] = self.simbox.data_array 

451 

452 # save topology, depending on flag 

453 if include_topology: 

454 output[group_name].create_group('topology') 

455 self.topology.save(output[f'{group_name}/topology']) 

456 

457 # The following is equivalent to overloading in c++ : https://stackoverflow.com/questions/12179271/meaning-of-classmethod-and-staticmethod-for-beginner 

458 # cls stands for class, in this case the Configuration class 

459 @classmethod 

460 def from_h5(cls, h5file: h5py.File, group_name: str, reset_images: bool=False, compute_flags: bool=None): 

461 """ Read a configuration from an open HDF5 file identified by group-name 

462 

463 Parameters 

464 ---------- 

465 h5file : HDF5 File 

466 open HDF5 open, as returned by h5py.File() 

467 

468 group_name : str 

469 string corresponding to a key in the h5py.File containing a saved gamdpy configuration 

470 

471 reset_images : bool 

472 if True set the images to zero (default False) 

473 

474 compute_flags : bool 

475 NOTE: still to be developed, should be possible to define compute flags from dictionary 

476 compute_flags defining what will be stored in the configuration (default None) 

477 

478 Returns 

479 ------- 

480 

481 configuration : gamdpy.Configuration 

482 a gamdpy configuration object 

483 

484 

485 Example: 

486 -------- 

487 

488 >>> import gamdpy as gp 

489 >>> output_file = h5py.File('examples/Data/LJ_r0.973_T0.70_toread.h5') 

490 >>> conf = Configuration.from_h5(output_file, 'restarts/restart0000') 

491 >>> print(conf.D, conf.N, conf['r'][0]) # Print number of dimensions D, number of particles N and position of first particle 

492 3 2048 [-6.384221 -6.3622074 -6.3125153] 

493 

494 """ 

495 

496 

497 vectors_array = h5file[group_name]['vectors'][:] 

498 _, N, D = vectors_array.shape 

499 configuration = cls(D=D, N=N, compute_flags=compute_flags) 

500 

501 configuration.vector_columns = h5file[group_name]['vectors'].attrs['vector_columns'] 

502 configuration.scalar_columns = h5file[group_name]['scalars'].attrs['scalar_columns'] 

503 

504 configuration.ptype = h5file[group_name]['ptype'][:] 

505 

506 scalars_array = h5file[group_name]['scalars'][:] 

507 configuration.scalars = scalars_array 

508 configuration.vectors.array = vectors_array 

509 

510 simbox_name = h5file[group_name].attrs['simbox_name'] 

511 simbox_data = h5file[group_name].attrs['simbox_data'] 

512 

513 if simbox_name == 'Orthorhombic': 

514 configuration.simbox = Orthorhombic(D, simbox_data) 

515 elif simbox_name == 'LeesEdwards': 

516 box_shift_image = {True:0, False: int(simbox_data[D+1])} [reset_images] 

517 configuration.simbox = LeesEdwards(D, simbox_data[:D], simbox_data[D], box_shift_image) 

518 else: 

519 raise ValueError('simbox_name %s not recognized in group %s' % (simbox_name, group_name)) 

520 

521 if reset_images: 

522 configuration.r_im = np.zeros((N, D), dtype=np.int32) 

523 else: 

524 configuration.r_im = h5file[group_name]['r_im'][:] 

525 return configuration 

526 

527 

528# Helper functions 

529 

530def generate_random_velocities(N, D, T, seed, m=1, dtype=np.float32): 

531 """ Generate random velocities according to a given temperature. """ 

532 v = np.zeros((N, D), dtype=dtype) 

533 # default value of seed is None and random.seed(None) has no effect 

534 np.random.seed(seed) 

535 for k in range(D): 

536 # to cover the case that m is a 1D array of length N, need to 

537 # generate one column at a time, passing the initial zeros as the 

538 # mean to avoid problems with inferring the correct shape 

539 v[:, k] = np.random.normal(v[:, k], (T / m) ** 0.5) 

540 CM_drift = np.mean(m * v[:, k]) / np.mean(m) 

541 v[:, k] -= CM_drift 

542 return dtype(v) 

543 

544@numba.njit 

545def generate_fcc_positions(nx, ny, nz, rho, dtype=np.float32): 

546 # This function is not recommended to use, and should be considered deprecated 

547 # raise DeprecationWarning('Use Configuration.make_lattice() instead') 

548 

549 D = 3 

550 conf = np.zeros((nx * ny * nz * 4, D), dtype=dtype) 

551 count = 0 

552 for ix in range(nx): 

553 for iy in range(ny): 

554 for iz in range(nz): 

555 conf[count + 0, :] = [ix + 0.25, iy + 0.25, iz + 0.25] 

556 conf[count + 1, :] = [ix + 0.75, iy + 0.75, iz + 0.25] 

557 conf[count + 2, :] = [ix + 0.75, iy + 0.25, iz + 0.75] 

558 conf[count + 3, :] = [ix + 0.25, iy + 0.75, iz + 0.75] 

559 count += 4 

560 for k in range(D): 

561 conf[:, k] -= np.mean(conf[:, k]) # put sample in the middle of the box 

562 sim_box = np.array((nx, ny, nz), dtype=dtype) 

563 rho_initial = 4.0 

564 scale_factor = dtype((rho_initial / rho) ** (1 / D)) 

565 

566 return conf * scale_factor, sim_box * scale_factor 

567 

568 

569def make_configuration_fcc(nx, ny, nz, rho, N=None): 

570 """ 

571 Generate Configuration for particle positions and simbox of a FCC lattice with a given density 

572 (nx x ny x nz unit cells),  

573 and default types ('0') and masses ('1.') 

574 If N is given, only N particles will be in the configuration  

575 (needs to be equal to or smaller than number of particle in generated crystal) 

576 """ 

577 

578 # This function is not recommended to use, and should be considered deprecated 

579 # raise DeprecationWarning('Use Configuration.make_lattice() instead') 

580 

581 positions, simbox_data = generate_fcc_positions(nx, ny, nz, rho) 

582 N_, D = positions.shape 

583 if N == None: 

584 N = N_ 

585 else: 

586 if N > N_: 

587 raise ValueError( 

588 f'N ({N}) needs to be equal to or smaller than number of particle in generated crystal ({N_})') 

589 scale_factor = (N / N_) ** (1 / 3) 

590 positions *= scale_factor 

591 simbox_data *= scale_factor 

592 

593 configuration = Configuration(D=3) 

594 configuration['r'] = positions[:N, :] 

595 configuration.simbox = Orthorhombic(D, simbox_data) 

596 configuration['m'] = np.ones(N, dtype=np.float32) # Set masses 

597 configuration.ptype = np.zeros(N, dtype=np.int32) # Set types 

598 

599 return configuration 

600 

601 

602def replicate_molecules(molecule_dicts, num_molecules_each_type_list, safety_distance, random_rotations=True, compute_flags=None): 

603 """ Construct a configuration containing different molecules, with the numbers of each type specified 

604 

605 Parameters: 

606 moleculde_dicts (list): A list of dictionaries, each of which contains keys "positions", "particle_types", "masses" and "topology", whose values are corresponding lists of data for that molecule 

607 num_molecules_each_type_list (list): A list of integers, specifying how many molecules of each type are to be included 

608 safety_distance (float): A length to be added in all directions to the size of the bounding box to be used for each molecule when placing them initially on a lattice 

609 random_rotation (Bool): Whether the x,y,z coordinates in each molecule should be randomly permutated to give a simple randomization of orientations. 

610 Returns: 

611 configuration (Configuration): the resulting configuration with all molecules replicated and including the corresponding replicated topology 

612 """ 

613 D = 3 

614 num_molecule_types = len(molecule_dicts) 

615 total_num_particles = 0 

616 total_num_molecules = 0 

617 mol_types = [] 

618 positions_array_list = [] 

619 cell_length_list = [] 

620 size_molecule_type_list = [] 

621 

622 # unpack the list of molecule dictionaries and make lists of positions, particle_types, masses and topologies 

623 mol_topology_list = [] 

624 mol_positions_list = [] 

625 particle_type_list = [] 

626 masses_list = [] 

627 for idx in range(num_molecule_types): 

628 mol_positions_list.append(molecule_dicts[idx]["positions"]) 

629 particle_type_list.append(molecule_dicts[idx]["particle_types"]) 

630 masses_list.append(molecule_dicts[idx]["masses"]) 

631 mol_topology_list.append(molecule_dicts[idx]["topology"]) 

632 

633 # tally the total numbers of particles and molecules, make shifted arrays of possitions for each molecule type 

634 for idx in range(num_molecule_types): 

635 num_mol_this_type = num_molecules_each_type_list[idx] 

636 total_num_particles += len(mol_positions_list[idx]) * num_mol_this_type 

637 total_num_molecules += num_mol_this_type 

638 mol_types += [idx] * num_mol_this_type 

639 positions_array = np.array(mol_positions_list[idx]) 

640 positions_array -= np.min(positions_array, axis=0) 

641 positions_array_list.append(positions_array) 

642 size_molecule_type_list.append( len(mol_positions_list[idx]) ) 

643 cell_length = np.max(positions_array) + safety_distance 

644 cell_length_list.append(cell_length) 

645 

646 # shuffle molecule types randomly 

647 np.random.shuffle(mol_types) 

648 

649 

650 configuration = Configuration(D=D, N=total_num_particles, compute_flags=compute_flags) 

651 configuration.topology = replicate_topologies(mol_topology_list, num_molecules_each_type_list, mol_types, size_molecule_type_list) 

652 

653 max_cell_length = max(cell_length_list) 

654 # make a cubic box big enough to hold the total number of molecules 

655 num_cells_axis = math.ceil(total_num_molecules**(1/3)) 

656 simbox_data = np.ones(D) * (num_cells_axis * max_cell_length) 

657 configuration.simbox = Orthorhombic(D, simbox_data) 

658 

659 mol_count = 0 

660 particle_count = 0 

661 for ix in range(num_cells_axis): 

662 for iy in range(num_cells_axis): 

663 for iz in range(num_cells_axis): 

664 if mol_count < total_num_molecules: 

665 # add a molecule 

666 this_mol_type = mol_types[mol_count] 

667 particles_this_molecule = size_molecule_type_list[this_mol_type] 

668 arr = np.arange(D) 

669 if random_rotations: 

670 np.random.shuffle(arr) 

671 

672 configuration['r'][particle_count:particle_count+particles_this_molecule,0] = positions_array_list[this_mol_type][:,arr[0]] + ix*max_cell_length 

673 configuration['r'][particle_count:particle_count+particles_this_molecule,1] = positions_array_list[this_mol_type][:,arr[1]] + iy*max_cell_length 

674 configuration['r'][particle_count:particle_count+particles_this_molecule,2] = positions_array_list[this_mol_type][:,arr[2]] + iz*max_cell_length 

675 configuration['m'][particle_count:particle_count+particles_this_molecule] = masses_list[this_mol_type] 

676 configuration.ptype[particle_count:particle_count+particles_this_molecule] = particle_type_list[this_mol_type] 

677 particle_count += particles_this_molecule 

678 

679 mol_count += 1 

680 

681 assert mol_count == total_num_molecules 

682 assert particle_count == total_num_particles 

683 

684 for i in range(configuration.D): 

685 configuration['r'][:,i] -= configuration.simbox.get_lengths()[i]/2 

686 

687 return configuration