Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import os 

2import sys 

3import base64 

4import yaml 

5import pkg_resources 

6import warnings 

7from glob import glob 

8from datetime import datetime 

9from typing import Union, Tuple, Any, List 

10 

11import torch 

12from torch import cdist 

13from torch.nn.functional import pdist 

14 

15import numpy as np 

16import matplotlib.pyplot as plt 

17 

18from ase import Atoms 

19from ase.io import read, write 

20from ase.build import make_supercell 

21from ase.build.tools import sort as ase_sort 

22 

23from DebyeCalculator.utility.profiling import Profiler 

24 

25import ipywidgets as widgets 

26from IPython.display import display, HTML, clear_output 

27from ipywidgets import HBox, VBox, Layout 

28from tqdm.auto import tqdm 

29 

30import collections 

31import timeit 

32 

33class DebyeCalculator: 

34 """ 

35 Calculate the scattering intensity I(q) through the Debye scattering equation, the Total Scattering Structure Function S(q),  

36 the Reduced Total Scattering Function F(q), and the Reduced Atomic Pair Distribution Function G(r) for a given atomic structure. 

37 

38 

39 Parameters: 

40 qmin (float): Minimum q-value for the scattering calculation. Default is 1.0. 

41 qmax (float): Maximum q-value for the scattering calculation. Default is 30.0. 

42 qstep (float): Step size for the q-values in the scattering calculation. Default is 0.1. 

43 qdamp (float): Damping parameter caused by the truncated Q-range of the Fourier transformation. Default is 0.04. 

44 rmin (float): Minimum r-value for the pair distribution function (PDF) calculation. Default is 0.0. 

45 rmax (float): Maximum r-value for the PDF calculation. Default is 20.0. 

46 rstep (float): Step size for the r-values in the PDF calculation. Default is 0.01. 

47 rthres (float): Threshold value for exclusion of distances below this value in the scattering calculation. Default is 0.0. 

48 biso (float): Debye-Waller isotropic atomic displacement parameter. Default is 0.3. 

49 device (str): Device to use for computation (e.g., 'cuda' for GPU or 'cpu' for CPU). Default is 'cuda' if the computer has a GPU. 

50 batch_size (int or None): Batch size for computation. If None, the batch size will be automatically set. Default is None. 

51 lorch_mod (bool): Flag to enable Lorch modification. Default is False. 

52 radiation_type (str): Type of radiation for form factor calculations ('xray' or 'neutron'). Default is 'xray'. 

53 profile (bool): Activate profiler. Default is False. 

54 """ 

55 

56 def __init__( 

57 self, 

58 qmin: float = 1.0, 

59 qmax: float = 30.0, 

60 qstep: float = 0.1, 

61 qdamp: float = 0.04, 

62 rmin: float = 0.0, 

63 rmax: float = 20.0, 

64 rstep: float = 0.01, 

65 rthres: float = 0.0, 

66 biso: float = 0.3, 

67 device: str = 'cuda' if torch.cuda.is_available() else 'cpu', 

68 batch_size: Union[int, None] = 10000, 

69 lorch_mod: bool = False, 

70 radiation_type: str = 'xray', 

71 profile: bool = False, 

72 _max_batch_size: int = 4000, 

73 ) -> None: 

74 

75 self.profile = profile 

76 if self.profile: 

77 self.profiler = Profiler() 

78 

79 # Initial parameters 

80 self.device = device 

81 self.batch_size = batch_size 

82 self.lorch_mod = lorch_mod 

83 self.radiation_type = radiation_type 

84 

85 # Standard Debye parameters 

86 self.qmin = qmin 

87 self.qmax = qmax 

88 self.qstep = qstep 

89 self.qdamp = qdamp 

90 self.rmin = rmin 

91 self.rmax = rmax 

92 self.rstep = rstep 

93 self.rthres = rthres 

94 self.biso = biso 

95 

96 # Initialise ranges 

97 self.q = torch.arange(self.qmin, self.qmax, self.qstep).unsqueeze(-1).to(device=self.device) 

98 self.r = torch.arange(self.rmin, self.rmax, self.rstep).unsqueeze(-1).to(device=self.device) 

99 

100 # Form factor coefficients 

101 with open(pkg_resources.resource_filename(__name__, 'form_factor_coef.yaml'), 'r') as yaml_file: 

102 self.FORM_FACTOR_COEF = yaml.safe_load(yaml_file) 

103 

104 # Formfactor retrieval lambda 

105 for k,v in self.FORM_FACTOR_COEF.items(): 

106 if None in v: 

107 v = [value if value is not None else np.nan for value in v] 

108 self.FORM_FACTOR_COEF[k] = torch.tensor(v).to(device=self.device, dtype=torch.float32) 

109 if radiation_type.lower() in ['xray', 'x']: 

110 self.form_factor_func = lambda p: torch.sum(p[:5] * torch.exp(-1*p[6:11] * (self.q / (4*torch.pi)).pow(2)), dim=1) + p[5] 

111 elif radiation_type.lower() in ['neutron', 'n']: 

112 self.form_factor_func = lambda p: p[11].unsqueeze(-1) 

113 

114 # Batch size 

115 self._max_batch_size = _max_batch_size 

116 

117 def __repr__( 

118 self, 

119 ): 

120 parameters = {'qmin': self.qmin, 'qmax': self.qmax, 'qdamp': self.qdamp, 'qstep': self.qstep, 

121 'rmin': self.rmin, 'rmax': self.rmax, 'rstep': self.rstep, 'rthres': self.rthres, 

122 'biso': self.biso} 

123 

124 return f"DebyeCalculator{parameters}" 

125 

126 def update_parameters( 

127 self, 

128 **kwargs: Any, 

129 ) -> None: 

130 """ 

131 Set or update the parameters of the DebyeCalculator. 

132 

133 Parameters: 

134 **kwargs: Arbitrary keyword arguments to update the parameters. 

135 """ 

136 for k,v in kwargs.items(): 

137 try: 

138 setattr(self, k, v) 

139 except: 

140 print("Failed to update parameters because of unexpected parameter names") 

141 return 

142 

143 # Re-initialise ranges 

144 if np.any([k in ['qmin','qmax','qstep','rmin', 'rmax', 'rstep'] for k in kwargs.keys()]): 

145 self.q = torch.arange(self.qmin, self.qmax, self.qstep).unsqueeze(-1).to(device=self.device) 

146 self.r = torch.arange(self.rmin, self.rmax, self.rstep).unsqueeze(-1).to(device=self.device) 

147 

148 def _initialise_structures( 

149 self, 

150 structure_path: Union[str, Atoms, List[Atoms]], 

151 radii: Union[List[float], float, None] = None, 

152 disable_pbar: bool = False, 

153 ) -> None: 

154 

155 """ 

156 Initialise atomic structures and unique elements form factors from an input file. 

157 

158 Parameters: 

159 structure_path (Union[str, Atoms, List[Atoms]]): Path to the atomic structure file in XYZ/CIF format or stored ASE Atoms objects. 

160 radii (Union[List[float], float, None]): List/float of radii/radius of particle(s) to generate with parsed CIF 

161 """ 

162 # Check if input is a file or ASE Atoms object 

163 if isinstance(structure_path, str): 

164 # Check file and extention 

165 structure_ext = structure_path.split('.')[-1] 

166 if structure_ext not in ['xyz', 'cif']: 

167 raise TypeError('FAILED: Invalid file/file-extention, accepts only .xyz or .cif data files') 

168 elif isinstance(structure_path, Atoms) or all(isinstance(lst_elm, Atoms) for lst_elm in structure_path): 

169 structure_ext = 'ase' 

170 else: 

171 raise TypeError('FAILED: Invalid structure format, accepts only .xyz, .cif data files or ASE Atoms objects') 

172 

173 # If cif, check for radii and generate particles 

174 if structure_ext == 'cif': 

175 if radii is not None: 

176 ase_structures, _ = self.generate_nanoparticles(structure_path, radii, disable_pbar=disable_pbar) 

177 self.num_structures = len(ase_structures) 

178 else: 

179 raise ValueError('FAILED: When providing .cif data file, please provide radii (Union[List[float], float]) to generate from.') 

180 

181 self.struc_elements = [] 

182 self.struc_size = [] 

183 self.struc_occupancy = [] 

184 self.struc_xyz = [] 

185 

186 for structure in ase_structures: 

187 elements = structure.get_chemical_symbols() 

188 size = len(elements) 

189 occupancy = torch.ones((size), dtype=torch.float32).to(device=self.device) 

190 xyz = torch.tensor(np.array(structure.get_positions())).to(device=self.device, dtype=torch.float32) 

191 

192 self.struc_elements.append(elements) 

193 self.struc_size.append(size) 

194 self.struc_occupancy.append(occupancy) 

195 self.struc_xyz.append(xyz) 

196 

197 elif structure_ext == 'xyz': 

198 

199 self.num_structures = 1 

200 struc = np.genfromtxt(structure_path, dtype='str', skip_header=2) # Gen 

201 self.struc_elements = [struc[:,0]] # Identities 

202 self.struc_size = [len(self.struc_elements[0])] # Size 

203 

204 # Append occupancy if nothing is provided 

205 if struc.shape[1] == 5: 

206 self.struc_occupancy = [torch.from_numpy(struc[:,-1]).to(device=self.device, dtype=torch.float32)] 

207 self.struc_xyz = [torch.tensor(struc[:,1:-1].astype('float')).to(device=self.device, dtype=torch.float32)] 

208 else: 

209 self.struc_occupancy = [torch.ones((self.struc_size[0]), dtype=torch.float32).to(device=self.device)] 

210 self.struc_xyz = [torch.tensor(struc[:,1:].astype('float')).to(device=self.device, dtype=torch.float32)] 

211 elif structure_ext == 'ase': 

212 if isinstance(structure_path, Atoms): 

213 ase_structures = [structure_path] 

214 else: 

215 ase_structures = structure_path 

216 

217 self.num_structures = len(ase_structures) 

218 

219 self.struc_elements = [] 

220 self.struc_size = [] 

221 self.struc_occupancy = [] 

222 self.struc_xyz = [] 

223 

224 for structure in ase_structures: 

225 elements = structure.get_chemical_symbols() 

226 size = len(elements) 

227 occupancy = torch.ones((size), dtype=torch.float32).to(device=self.device) 

228 xyz = torch.tensor(np.array(structure.get_positions())).to(device=self.device, dtype=torch.float32) 

229 

230 self.struc_elements.append(elements) 

231 self.struc_size.append(size) 

232 self.struc_occupancy.append(occupancy) 

233 self.struc_xyz.append(xyz) 

234 else: 

235 raise TypeError('FAILED: Invalid structure format, accepts only .xyz, .cif data files or ASE Atoms objects') 

236 

237 # Unique elements and their counts 

238 self.triu_indices = [] 

239 self.unique_inverse = [] 

240 self.struc_unique_form_factors = [] 

241 self.struc_form_avg_sq = [] 

242 self.struc_inverse = [] 

243 

244 for i in range(self.num_structures): 

245 

246 # Get unique elements and construc form factor stacks 

247 unique_elements, inverse, counts = np.unique(self.struc_elements[i], return_counts=True, return_inverse=True) 

248 

249 triu_indices = torch.triu_indices(self.struc_size[i], self.struc_size[i], 1) 

250 unique_inverse = torch.from_numpy(inverse[triu_indices]).to(device=self.device) 

251 struc_unique_form_factors = torch.stack([self.form_factor_func(self.FORM_FACTOR_COEF[el]) for el in unique_elements]) 

252 

253 self.triu_indices.append(triu_indices) 

254 self.unique_inverse.append(unique_inverse) 

255 self.struc_unique_form_factors.append(struc_unique_form_factors) 

256 

257 # Calculate average squared form factor and self scattering inverse indices 

258 counts = torch.from_numpy(counts).to(device=self.device) 

259 compositional_fractions = counts / torch.sum(counts) 

260 struc_form_avg_sq = torch.sum(compositional_fractions.reshape(-1,1) * struc_unique_form_factors, dim=0)**2 

261 struc_inverse = torch.from_numpy(np.array([inverse[i] for i in range(self.struc_size[i])])).to(device=self.device) 

262 

263 self.struc_form_avg_sq.append(struc_form_avg_sq) 

264 self.struc_inverse.append(struc_inverse) 

265 

266 def iq( 

267 self, 

268 structure_path: Union[str, Atoms, List[Atoms]], 

269 radii: Union[List[float], float, None] = None, 

270 keep_on_device: bool = False, 

271 _total_scattering: bool = False, 

272 ) -> Union[Tuple[np.float32, Union[List[np.float32], np.float32]], Tuple[torch.FloatTensor, Union[List[torch.FloatTensor], torch.FloatTensor]]]: 

273 

274 """ 

275 Calculate the scattering intensity I(Q) for the given atomic structure. 

276 

277 Parameters: 

278 structure_path (Union[str, Atoms, List[Atoms]]): Path to the atomic structure file in XYZ/CIF format or stored ASE Atoms objects. 

279 radii (Union[List[float], float, None]): List/float of radii/radius of particle(s) to generate with parsed CIF 

280 keep_on_device (bool): Flag to keep the results on the class device. Default is False. 

281 _total_scattering (bool): Flag to return the scattering intensity I(Q) without the self-scattering contribution. Default is False. 

282 

283 Returns: 

284 Tuple of torch tensors containing Q-values and scattering intensity I(Q) if keep_on_device is True, otherwise, numpy arrays on CPU. 

285 """ 

286 

287 # Raises errors if wrong path or parameters 

288 if not os.path.exists(structure_path): 

289 raise FileNotFoundError(f"{structure_path} not found.") 

290 if self.qmin < 0: 

291 raise ValueError("qmin must be non-negative.") 

292 if self.qmax < 0: 

293 raise ValueError("qmax must be non-negative.") 

294 if self.qstep < 0: 

295 raise ValueError("qstep must be non-negative.") 

296 if self.qdamp < 0: 

297 raise ValueError("qdamp must be non-negative.") 

298 if self.rmin < 0: 

299 raise ValueError("rmin must be non-negative.") 

300 if self.rmax < 0: 

301 raise ValueError("rmax must be non-negative.") 

302 if self.rstep < 0: 

303 raise ValueError("rstep must be non-negative.") 

304 if self.rthres < 0: 

305 raise ValueError("rthres must be non-negative.") 

306 if self.biso < 0: 

307 raise ValueError("biso must be non-negative.") 

308 if self.batch_size is not None and self.batch_size < 0: 

309 raise ValueError("batch_size must be non-negative.") 

310 

311 # Initialise structure 

312 self._initialise_structures(structure_path, radii, disable_pbar = True) 

313 

314 if self.profile: 

315 self.profiler.time('Setup structures and form factors') 

316 

317 # Calculate I(Q) for all initialised structures 

318 iq_output = [] 

319 for i in range(self.num_structures): 

320 

321 # Calculate distances and batch 

322 if self.batch_size is None: 

323 self.batch_size = self._max_batch_size 

324 dists = pdist(self.struc_xyz[i]).split(self.batch_size) 

325 indices = self.triu_indices[i].split(self.batch_size, dim=1) 

326 inverse_indices = self.unique_inverse[i].split(self.batch_size, dim=1) 

327 

328 if self.profile: 

329 self.profiler.time('Batching and Distances') 

330 

331 # Calculate scattering using Debye Equation 

332 iq = torch.zeros((len(self.q))).to(device=self.device, dtype=torch.float32) 

333 for d, inv_idx, idx in zip(dists, inverse_indices, indices): 

334 mask = d >= self.rthres 

335 occ_product = self.struc_occupancy[i][idx[0]] * self.struc_occupancy[i][idx[1]] 

336 sinc = torch.sinc(d[mask] * self.q / torch.pi) 

337 ffp = self.struc_unique_form_factors[i][inv_idx[0]] * self.struc_unique_form_factors[i][inv_idx[1]] 

338 iq += torch.sum(occ_product.unsqueeze(-1) * ffp * sinc.permute(1,0), dim=0) 

339 

340 # Apply Debye-Weller Isotropic Atomic Displacement 

341 if self.biso != 0.0: 

342 iq *= torch.exp(-self.q.squeeze(-1).pow(2) * self.biso/(8*torch.pi**2)) 

343 

344 # For total scattering 

345 if _total_scattering: 

346 if self.profile: 

347 self.profiler.time('I(Q)') 

348 iq_output.append(iq) # TODO Times 2 

349 continue 

350 

351 # Self-scattering contribution 

352 sinc = torch.ones((self.struc_size[i], len(self.q))).to(device=self.device) 

353 iq += torch.sum((self.struc_occupancy[i].unsqueeze(-1) * self.struc_unique_form_factors[i][self.struc_inverse[i]])**2 * sinc, dim=0) / 2 

354 iq *= 2 

355 

356 if self.profile: 

357 self.profiler.time('I(Q)') 

358 

359 iq_output.append(iq) 

360 

361 if _total_scattering: 

362 return self.q.squeeze(-1), iq_output 

363 

364 if keep_on_device: 

365 if self.num_structures == 1: 

366 return self.q.squeeze(-1), iq_output[0] 

367 else: 

368 return self.q.squeeze(-1), iq_output 

369 else: 

370 if self.num_structures == 1: 

371 return self.q.squeeze(-1).cpu().numpy(), iq_output[0].cpu().numpy() 

372 else: 

373 return self.q.squeeze(-1).cpu().numpy(), [iq.cpu().numpy() for iq in iq_output] 

374 

375 def sq( 

376 self, 

377 structure_path: Union[str, Atoms, List[Atoms]], 

378 radii: Union[List[float], float, None] = None, 

379 keep_on_device: bool = False, 

380 ) -> Union[Tuple[np.float32, Union[List[np.float32], np.float32]], Tuple[torch.FloatTensor, Union[List[torch.FloatTensor], torch.FloatTensor]]]: 

381 

382 """ 

383 Calculate the structure function S(Q) for the given atomic structure. 

384 

385 Parameters: 

386 structure_path (Union[str, Atoms, List[Atoms]]): Path to the atomic structure file in XYZ/CIF format or stored ASE Atoms objects. 

387 keep_on_device (bool): Flag to keep the results on the class device. Default is False. 

388 

389 Returns: 

390 Tuple of torch tensors containing Q-values and structure function S(Q) if keep_on_device is True, otherwise, numpy arrays on CPU. 

391 """ 

392 

393 # Calculate Scattering S(Q) 

394 _, iq = self.iq(structure_path, radii, keep_on_device=True, _total_scattering=True) 

395 

396 sq_output = [] 

397 for i in range(self.num_structures): 

398 sq = iq[i]/self.struc_form_avg_sq[i]/self.struc_size[i] 

399 sq_output.append(sq) 

400 

401 if keep_on_device: 

402 if self.num_structures == 1: 

403 return self.q.squeeze(-1), sq_output[0] 

404 else: 

405 return self.q.squeeze(-1), sq_output 

406 else: 

407 if self.num_structures == 1: 

408 return self.q.squeeze(-1).cpu().numpy(), sq_output[0].cpu().numpy() 

409 else: 

410 return self.q.squeeze(-1).cpu().numpy(), [sq.cpu().numpy() for sq in sq_output] 

411 

412 def fq( 

413 self, 

414 structure_path: Union[str, Atoms, List[Atoms]], 

415 radii: Union[List[float], float, None] = None, 

416 keep_on_device: bool = False, 

417 ) -> Union[Tuple[np.float32, Union[List[np.float32], np.float32]], Tuple[torch.FloatTensor, Union[List[torch.FloatTensor], torch.FloatTensor]]]: 

418 """ 

419 Calculate the reduced structure function F(Q) for the given atomic structure. 

420 

421 Parameters: 

422 structure_path (Union[str, Atoms, List[Atoms]]): Path to the atomic structure file in XYZ/CIF format or stored ASE Atoms objects. 

423 keep_on_device (bool): Flag to keep the results on the class device. Default is False. 

424 

425 Returns: 

426 Tuple of torch tensors containing Q-values and reduced structure function F(Q) if keep_on_device is True, otherwise, numpy arrays on CPU. 

427 """ 

428 # Calculate Scattering S(Q) 

429 _, iq = self.iq(structure_path, radii, keep_on_device=True, _total_scattering=True) 

430 

431 fq_output = [] 

432 for i in range(self.num_structures): 

433 sq = iq[i]/self.struc_form_avg_sq[i]/self.struc_size[i] 

434 fq = self.q.squeeze(-1) * sq 

435 fq_output.append(fq) 

436 

437 if keep_on_device: 

438 if self.num_structures == 1: 

439 return self.q.squeeze(-1), fq_output[0] 

440 else: 

441 return self.q.squeeze(-1), fq_output 

442 else: 

443 if self.num_structures == 1: 

444 return self.q.squeeze(-1).cpu().numpy(), fq_output[0].cpu().numpy() 

445 else: 

446 return self.q.squeeze(-1).cpu().numpy(), [fq.cpu().numpy() for fq in fq_output] 

447 

448 def gr( 

449 self, 

450 structure_path: Union[str, Atoms, List[Atoms]], 

451 radii: Union[List[float], float, None] = None, 

452 keep_on_device: bool = False, 

453 ) -> Union[Tuple[np.float32, Union[List[np.float32], np.float32]], Tuple[torch.FloatTensor, Union[List[torch.FloatTensor], torch.FloatTensor]]]: 

454 

455 """ 

456 Calculate the reduced pair distribution function G(r) for the given atomic structure. 

457 

458 Parameters: 

459 structure_path (Union[str, Atoms, List[Atoms]]): Path to the atomic structure file in XYZ/CIF format or stored ASE Atoms objects. 

460 keep_on_device (bool): Flag to keep the results on the class device. Default is False. 

461 

462 Returns: 

463 Tuple of torch tensors containing Q-values and PDF G(r) if keep_on_device is True, otherwise, numpy arrays on CPU. 

464 """ 

465 if self.profile: 

466 self.profiler.reset() 

467 

468 # Calculate Scattering I(Q), S(Q), F(Q) 

469 _, iq = self.iq(structure_path, radii, keep_on_device=True, _total_scattering=True) 

470 

471 gr_output = [] 

472 for i in range(self.num_structures): 

473 sq = iq[i]/self.struc_form_avg_sq[i]/self.struc_size[i] 

474 if self.profile: 

475 self.profiler.time('S(Q)') 

476 fq = self.q.squeeze(-1) * sq 

477 if self.profile: 

478 self.profiler.time('F(Q)') 

479 

480 # Calculate total scattering, G(r) 

481 damp = 1 if self.qdamp == 0.0 else torch.exp(-(self.r.squeeze(-1) * self.qdamp).pow(2) / 2) 

482 lorch_mod = 1 if self.lorch_mod == None else torch.sinc(self.q * self.lorch_mod*(torch.pi / self.qmax)) 

483 if self.profile: 

484 self.profiler.time('Modifications, Qdamp/Lorch') 

485 gr = (2 / torch.pi) * torch.sum(fq.unsqueeze(-1) * torch.sin(self.q * self.r.permute(1,0))*self.qstep * lorch_mod, dim=0) * damp 

486 if self.profile: 

487 self.profiler.time('G(r)') 

488 

489 gr_output.append(gr) 

490 

491 if keep_on_device: 

492 if self.num_structures == 1: 

493 return self.r.squeeze(-1), gr_output[0] 

494 else: 

495 return self.r.squeeze(-1), gr_output 

496 else: 

497 if self.num_structures == 1: 

498 return self.r.squeeze(-1).cpu().numpy(), gr_output[0].cpu().numpy() 

499 else: 

500 return self.r.squeeze(-1).cpu().numpy(), [gr.cpu().numpy() for gr in gr_output] 

501 

502 def _get_all( 

503 self, 

504 structure_path: Union[str, Atoms, List[Atoms]], 

505 radii: Union[List[float], float, None] = None, 

506 keep_on_device: bool = False, 

507 ) -> Union[Tuple[np.float32,np.float32,Union[List[np.float32], np.float32],Union[List[np.float32],np.float32], Union[List[np.float32],np.float32], Union[List[np.float32], np.float32]], 

508 Tuple[torch.FloatTensor,torch.FloatTensor,Union[List[torch.FloatTensor], torch.FloatTensor],Union[List[torch.FloatTensor],torch.FloatTensor], Union[List[torch.FloatTensor],torch.FloatTensor], Union[List[torch.FloatTensor], torch.FloatTensor]]]: 

509 

510 """ 

511 Calculate I(Q), S(Q), F(Q) and G(r) for the given atomic structure and return all. 

512 

513 Parameters: 

514 structure_path (Union[str, Atoms, List[Atoms]]): Path to the atomic structure file in XYZ/CIF format or stored ASE Atoms objects. 

515 keep_on_device (bool): Flag to keep the results on the class device. Default is False. 

516 

517 Returns: 

518 Tuple of torch tensors containing of r-values, Q-values and Union[List[float_vec], float_vec] of I(Q), S(Q), F(Q) and G(r) if keep_on_device is True, otherwise, numpy arrays on CPU. 

519 """ 

520 

521 # Initialise structure 

522 self._initialise_structures(structure_path, radii, disable_pbar = True) 

523 

524 # Calculate I(Q) for all initialised structures 

525 iq_output = [] 

526 sq_output = [] 

527 fq_output = [] 

528 gr_output = [] 

529 for i in range(self.num_structures): 

530 

531 # Calculate distances and batch 

532 if self.batch_size is None: 

533 self.batch_size = self._max_batch_size 

534 dists = pdist(self.struc_xyz[i]).split(self.batch_size) 

535 indices = self.triu_indices[i].split(self.batch_size, dim=1) 

536 inverse_indices = self.unique_inverse[i].split(self.batch_size, dim=1) 

537 

538 # Calculate scattering using Debye Equation 

539 iq = torch.zeros((len(self.q))).to(device=self.device, dtype=torch.float32) 

540 for d, inv_idx, idx in zip(dists, inverse_indices, indices): 

541 mask = d >= self.rthres 

542 occ_product = self.struc_occupancy[i][idx[0]] * self.struc_occupancy[i][idx[1]] 

543 sinc = torch.sinc(d[mask] * self.q / torch.pi) 

544 ffp = self.struc_unique_form_factors[i][inv_idx[0]] * self.struc_unique_form_factors[i][inv_idx[1]] 

545 iq += torch.sum(occ_product.unsqueeze(-1) * ffp * sinc.permute(1,0), dim=0) 

546 

547 # Apply Debye-Weller Isotropic Atomic Displacement 

548 if self.biso != 0.0: 

549 iq *= torch.exp(-self.q.squeeze(-1).pow(2) * self.biso/(8*torch.pi**2)) 

550 

551 # Calculate S(Q), F(Q) and G(r) 

552 sq = iq/self.struc_form_avg_sq[i]/self.struc_size[i] 

553 sq_output.append(sq) 

554 

555 fq = self.q.squeeze(-1) * sq 

556 fq_output.append(fq) 

557 

558 damp = 1 if self.qdamp == 0.0 else torch.exp(-(self.r.squeeze(-1) * self.qdamp).pow(2) / 2) 

559 lorch_mod = 1 if self.lorch_mod == None else torch.sinc(self.q * self.lorch_mod*(torch.pi / self.qmax)) 

560 gr = (2 / torch.pi) * torch.sum(fq.unsqueeze(-1) * torch.sin(self.q * self.r.permute(1,0))*self.qstep * lorch_mod, dim=0) * damp 

561 gr_output.append(gr) 

562 

563 # Self-scattering contribution 

564 sinc = torch.ones((self.struc_size[i], len(self.q))).to(device=self.device) 

565 iq += torch.sum((self.struc_occupancy[i].unsqueeze(-1) * self.struc_unique_form_factors[i][self.struc_inverse[i]])**2 * sinc, dim=0) / 2 

566 iq *= 2 

567 

568 iq_output.append(iq) 

569 

570 if keep_on_device: 

571 if self.num_structures == 1: 

572 return self.r.squeeze(-1), self.q.squeeze(-1), iq_output[0], sq_output[0], fq_output[0], gr_output[0] 

573 else: 

574 return self.r.squeeze(-1), self.q.squeeze(-1), iq_output, sq_output, fq_output, gr_output 

575 else: 

576 if self.num_structures == 1: 

577 return self.r.squeeze(-1).cpu().numpy(), self.q.squeeze(-1).cpu().numpy(), iq_output[0].cpu().numpy(), sq_output[0].cpu().numpy(), fq_output[0].cpu().numpy(), gr_output[0].cpu().numpy() 

578 else: 

579 return self.r.squeeze(-1).cpu().numpy(), self.q.squeeze(-1).cpu().numpy(), [iq.cpu().numpy() for iq in iq_output], [sq.cpu().numpy() for sq in sq_output], [fq.cpu().numpy() for fq in fq_output], [gr.cpu().numpy() for gr in gr_output] 

580 

581 def generate_nanoparticles( 

582 self, 

583 structure_path: str, 

584 radii: Union[List[float], float], 

585 sort_atoms: bool = True, 

586 disable_pbar: bool = False, 

587 _override_device: bool = True, 

588 ) -> Tuple[Union[List[Atoms], Atoms], Union[List[float], float]]: 

589 

590 """ 

591 Generate nanoparticles from a given structure and list of radii. 

592  

593 Args: 

594 structure_path (str): Path to the input structure file. 

595 radii (Union[List[float], float]): List of floats or float of radii for nanoparticles to be generated. 

596 sort_atoms (bool, optional): Whether to sort atoms in the nanoparticle. Defaults to True. 

597 _override_device (bool): Ignore object device and run in CPU 

598  

599 Returns: 

600 list: List of ASE Atoms objects representing the generated nanoparticles. 

601 list: List of nanoparticle sizes (diameter) corresponding to each radius. 

602 """ 

603 

604 # Fix radii type 

605 if isinstance(radii, list): 

606 radii = [float(r) for r in radii] 

607 elif isinstance(radii, float): 

608 radii = [radii] 

609 elif isinstance(radii, int): 

610 radii = [float(radii)] 

611 else: 

612 raise ValueError('FAILED: Please provide valid radii for generation of nanoparticles') 

613 

614 # DEV: Override device 

615 device = 'cpu' if _override_device else self.device 

616 

617 # Read the input unit cell structure 

618 with warnings.catch_warnings(): 

619 warnings.simplefilter("ignore") 

620 unit_cell = read(structure_path) 

621 cell_dims = np.array(unit_cell.cell.cellpar()[:3]) 

622 r_max = np.amax(radii) 

623 

624 # Create a supercell to encompass the entire range of nanoparticles and center it 

625 supercell_matrix = np.diag((np.ceil(r_max / cell_dims)) * 2 + 2) 

626 cell = make_supercell(prim=unit_cell, P=supercell_matrix) 

627 cell.center(about=0.) 

628 

629 # Convert positions to torch and send to device 

630 positions = torch.from_numpy(cell.get_positions()).to(dtype = torch.float32, device = device) 

631 

632 # Find all metals and center around the nearest metal 

633 ligands = ['O', 'Cl', 'H'] # Placeholder 

634 metal_filter = torch.BoolTensor([a not in ligands for a in cell.get_chemical_symbols()]).to(device = device) 

635 center_dists = torch.norm(positions, dim=1) 

636 positions -= positions[metal_filter][torch.argmin(center_dists[metal_filter])] 

637 center_dists = torch.norm(positions, dim=1) 

638 min_metal_dist = torch.min(pdist(positions[metal_filter])) 

639 min_bond_dist = torch.amin(cdist(positions[metal_filter], positions[~metal_filter])) 

640 # Update the cell positions 

641 cell.positions = positions.cpu() 

642 

643 # Initialize nanoparticle lists and progress bar 

644 nanoparticle_list = [] 

645 nanoparticle_sizes = [] 

646 pbar = tqdm(desc=f'Generating nanoparticles in range: [{np.amin(radii)},{np.amax(radii)}]', leave=False, total=len(radii), disable=disable_pbar) 

647 

648 # Generate nanoparticles for each radius 

649 for r in sorted(radii, reverse=True): 

650 

651 # Mask all atoms within radius 

652 incl_mask = (center_dists <= r) | ((center_dists <= r + min_metal_dist) & ~metal_filter) 

653 

654 # Modify objects based on mask 

655 cell = cell[incl_mask.cpu()] 

656 center_dists = center_dists[incl_mask] 

657 metal_filter = metal_filter[incl_mask] 

658 positions = positions[incl_mask] 

659 

660 # Find interdistances from all included atoms and remove 0's from diagonal 

661 interface_dists = cdist(positions, positions).fill_diagonal_(min_metal_dist*2) 

662 

663 # Remove floating atoms 

664 interaction_mask = torch.amin(interface_dists, dim=0) < min_bond_dist*1.2 

665 

666 # Modify objects based on mask 

667 cell = cell[interaction_mask.cpu()] 

668 center_dists = center_dists[interaction_mask] 

669 metal_filter = metal_filter[interaction_mask] 

670 positions = positions[interaction_mask] 

671 

672 # Determine NP size 

673 nanoparticle_size = torch.amax(center_dists) * 2 

674 

675 # Sort the atoms 

676 if sort_atoms: 

677 sorted_cell = ase_sort(cell) 

678 if sorted_cell.get_chemical_symbols()[0] in ligands: 

679 sorted_cell = sorted_cell[::-1] 

680 

681 # Append nanoparticle 

682 nanoparticle_list.append(sorted_cell) 

683 else: 

684 # Append nanoparticle 

685 nanoparticle_list.append(cell) 

686 

687 # Append size 

688 nanoparticle_sizes.append(nanoparticle_size.item()) 

689 

690 pbar.update(1) 

691 pbar.close() 

692 

693 return nanoparticle_list, nanoparticle_sizes 

694 

695 def _is_notebook( 

696 self, 

697 ) -> bool: 

698 

699 """ 

700 Checks if the code is running within a Jupyter Notebook, Google Colab, or other interactive environment. 

701  

702 Returns: 

703 bool: True if running in a Jupyter Notebook or Google Colab, False otherwise. 

704 """ 

705 try: 

706 shell = get_ipython().__class__.__name__ 

707 if shell == 'ZMQInteractiveShell': 

708 return True # Jupyter notebook or qtconsole 

709 elif shell == 'google.colab._shell': 

710 return True # Google Colab 

711 elif shell == 'Shell': 

712 return True # Apparently also Colab? 

713 else: 

714 return False # Other cases 

715 except NameError: 

716 return False # Standard Python Interpreter 

717 

718 def interact( 

719 self, 

720 _cont_updates: bool = False 

721 ) -> None: 

722 

723 """ 

724 Initiates an interactive visualization and data analysis tool within a Jupyter Notebook or Google Colab environment. 

725 

726 Args: 

727 _cont_updates (bool, optional): If True, enables continuous updates for interactive widgets. Defaults to False. 

728 """ 

729 

730 # Check if interaction is valid 

731 if not self._is_notebook(): 

732 print("FAILED: Interactive mode is exlusive to Jupyter Notebook or Google Colab") 

733 return 

734 

735 # Scattering parameters 

736 qmin = self.qmin 

737 qmax = self.qmax 

738 qstep = self.qstep 

739 qdamp = self.qdamp 

740 rmin = self.rmin 

741 rmax = self.rmax 

742 rstep = self.rstep 

743 rthres = self.rthres 

744 biso = self.biso 

745 device = 'cuda' if torch.cuda.is_available() else self.device 

746 batch_size = self.batch_size 

747 lorch_mod = self.lorch_mod 

748 radiation_type = self.radiation_type 

749 profile = False 

750 

751 with open('display_assets/choose_hardware.png', 'rb') as f: 

752 choose_hardware_img = f.read() 

753 with open('display_assets/batch_size.png', 'rb') as f: 

754 batch_size_img = f.read() 

755 

756 """ Utility widgets """ 

757 

758 # Spacing widget 

759 spacing_10px = widgets.Text(description='', layout=Layout(visibility='hidden', height='10px'), disabled=True) 

760 spacing_5px = widgets.Text(description='', layout=Layout(visibility='hidden', height='5px'), disabled=True) 

761 

762 """ File Selection Tab """ 

763 

764 # Load diplay display_assets 

765 with open('display_assets/enter_path.png', 'rb') as f: 

766 enter_path_img = f.read() 

767 with open('display_assets/select_files.png', 'rb') as f: 

768 select_files_img = f.read() 

769 with open('display_assets/radius_a.png', 'rb') as f: 

770 radius_a_img = f.read() 

771 with open('display_assets/file_1.png', 'rb') as f: 

772 file_1_img = f.read() 

773 with open('display_assets/file_2.png', 'rb') as f: 

774 file_2_img = f.read() 

775 

776 # Layout 

777 file_tab_layout = Layout( 

778 display='flex', 

779 flex_flow='column', 

780 align_items='stretch', 

781 order='solid', 

782 width='90%', 

783 ) 

784 

785 # File selection sizes 

786 header_widths = [105*1.8, 130*1.8] 

787 header_widths = [str(i)+'px' for i in header_widths] 

788 

789 # Folder selection 

790 folder = widgets.Text(description='', placeholder='Enter data directory', disabled=False, layout=Layout(width='650px')) 

791 

792 # Dropdown file sections 

793 DEFAULT_MSGS = ['No valid files in entered directory', 'Select data file'] 

794 select_file_1 = widgets.Dropdown(options=DEFAULT_MSGS, value=DEFAULT_MSGS[0], disabled=True, layout=Layout(width='650px')) 

795 select_file_2 = widgets.Dropdown(options=DEFAULT_MSGS, value=DEFAULT_MSGS[0], disabled=True, layout=Layout(width='650px')) 

796 

797 # File 1 

798 select_file_desc_1 = HBox([widgets.Image(value=file_1_img, format='png', layout=Layout(object_fit='contain', object_position='20px', width='32px'))], layout=Layout(width='88px')) 

799 select_radius_desc_1 = HBox([widgets.Image(value=radius_a_img, format='png', layout=Layout(object_fit='contain', object_position='20px', width='60px', visibility='hidden'))], layout=Layout(width='88px')) 

800 select_radius_1 = widgets.FloatText(min = 0, max = 50, step=0.01, value=5, disabled = False, layout = Layout(width='50px', visibility='hidden')) 

801 cif_text_1 = widgets.Text( 

802 value='Given radius, generate spherical nanoparticles (NP) from crystallographic information files (CIFs)', 

803 disabled=True, 

804 layout=Layout(width='595px', visibility='hidden') 

805 ) 

806 

807 # File 2 

808 select_file_desc_2 = HBox([widgets.Image(value=file_2_img, format='png', layout=Layout(object_fit='contain', object_position='20px', width='32px'))], layout=Layout(width='88px')) 

809 select_radius_desc_2 = HBox([widgets.Image(value=radius_a_img, format='png', layout=Layout(object_fit='contain', object_position='20px', width='60px', visibility='hidden'))], layout=Layout(width='88px')) 

810 select_radius_2 = widgets.FloatText(min = 0, max = 50, step=0.01, value=5, disabled = False, layout = Layout(width='50px', visibility='hidden')) 

811 cif_text_2 = widgets.Text( 

812 value='Given radius, generate spherical nanoparticles (NP) from crystallographic information files (CIFs)', 

813 disabled=True, 

814 layout=Layout(width='595px', visibility='hidden') 

815 ) 

816 

817 # File selection Tab 

818 file_tab = VBox([ 

819 # Enter path 

820 widgets.Image(value=enter_path_img, format='png', layout=Layout(object_fit='contain', width=header_widths[0])), 

821 folder, 

822 

823 spacing_10px, 

824 

825 # Select file(s) 

826 widgets.Image(value=select_files_img, format='png', layout=Layout(object_fit='contain', width=header_widths[1])), 

827 

828 # Select file 1 

829 HBox([select_file_desc_1, select_file_1]), 

830 

831 # if CIF, radius options 

832 HBox([select_radius_desc_1, select_radius_1, cif_text_1]), 

833 

834 spacing_10px, 

835 

836 # Select file 2 

837 HBox([select_file_desc_2, select_file_2]), 

838 

839 # If CIF, radius options 

840 HBox([select_radius_desc_2, select_radius_2, cif_text_2]), 

841 ], layout = file_tab_layout) 

842 

843 """ Scattering Options Tab """ 

844 

845 # Load display_assets 

846 with open('display_assets/qslider.png', 'rb') as f: 

847 qslider_img = f.read() 

848 with open('display_assets/rslider.png', 'rb') as f: 

849 rslider_img = f.read() 

850 with open('display_assets/qdamp.png', 'rb') as f: 

851 qdamp_img = f.read() 

852 with open('display_assets/global_biso.png', 'rb') as f: 

853 global_biso_img = f.read() 

854 with open('display_assets/a.png', 'rb') as f: 

855 a_img = f.read() 

856 with open('display_assets/a_inv.png', 'rb') as f: 

857 a_inv_img = f.read() 

858 with open('display_assets/a_sq.png', 'rb') as f: 

859 a_sq_img = f.read() 

860 with open('display_assets/qstep.png', 'rb') as f: 

861 qstep_img = f.read() 

862 with open('display_assets/rstep.png', 'rb') as f: 

863 rstep_img = f.read() 

864 with open('display_assets/rthres.png', 'rb') as f: 

865 rthres_img = f.read() 

866 with open('display_assets/radiation_type.png', 'rb') as f: 

867 radiation_type_img = f.read() 

868 with open('display_assets/scattering_parameters.png', 'rb') as f: 

869 scattering_parameters_img = f.read() 

870 with open('display_assets/presets.png', 'rb') as f: 

871 presets_img = f.read() 

872 

873 # Radiation  

874 radtype_button = widgets.ToggleButtons( 

875 options=['xray', 'neutron'], 

876 value=radiation_type, 

877 layout = Layout(width='800px'), 

878 button_style='primary' 

879 ) 

880 

881 # Q value slider 

882 qslider = widgets.FloatRangeSlider( 

883 value=[qmin, qmax], 

884 min=0.0, max=50.0, step=0.01, 

885 orientation='horizontal', 

886 readout=True, 

887 style={'font_weight':'bold', 'slider_color': 'white', 'description_width': '100px'}, 

888 layout = widgets.Layout(width='80%'), 

889 ) 

890 

891 # r value slider 

892 rslider = widgets.FloatRangeSlider( 

893 value=[rmin, rmax], 

894 min=0, max=100.0, step=rstep, 

895 orientation='horizontal', 

896 readout=True, 

897 style={'font_weight':'bold', 'slider_color': 'white', 'description_width': '100px'}, 

898 layout = widgets.Layout(width='80%'), 

899 ) 

900 

901 # Qdamp box 

902 qdamp_box = widgets.FloatText( 

903 min=0.00,max=0.10, step=0.01, 

904 value=qdamp, 

905 layout = widgets.Layout(width='50px'), 

906 ) 

907 

908 # B iso box 

909 biso_box = widgets.FloatText( 

910 min=0.00, max=1.00, step=0.01, 

911 value=biso, 

912 layout = widgets.Layout(width='50px'), 

913 ) 

914 

915 # Qstep box 

916 qstep_box = widgets.FloatText( 

917 min = 0.001, max = 1, step=0.001, 

918 value=qstep, 

919 layout=Layout(width='50px'), 

920 ) 

921 

922 # rstep box 

923 rstep_box = widgets.FloatText( 

924 min = 0.001, max = 1, step=0.001, 

925 value=rstep, 

926 layout=Layout(width='50px'), 

927 ) 

928 

929 # rthreshold box 

930 rthres_box = widgets.FloatText( 

931 min = 0.001, max = 1, step=0.001, 

932 value=rthres, 

933 layout=Layout(width='50px'), 

934 ) 

935 

936 # Lorch modification button 

937 lorch_mod_button = widgets.ToggleButton( 

938 value=lorch_mod, 

939 description='Lorch modification (OFF)', 

940 layout=Layout(width='250px'), 

941 button_style='primary', 

942 ) 

943 

944 # SAS preset button 

945 sas_preset_button = widgets.Button( 

946 description = 'Small Angle Scattering preset', 

947 layout=Layout(width='300px'), 

948 button_style='primary', 

949 ) 

950 

951 # Powder diffraction preset 

952 pd_preset_button = widgets.Button( 

953 description = 'Powder Diffraction preset', 

954 layout=Layout(width='300px'), 

955 button_style='primary', 

956 ) 

957 

958 # Total scattering preset 

959 ts_preset_button = widgets.Button( 

960 description = 'Total Scattering preset', 

961 layout=Layout(width='300px'), 

962 button_style='primary', 

963 ) 

964 

965 # Total scattering preset 

966 reset_button = widgets.Button( 

967 description = 'Reset scattering options', 

968 layout=Layout(width='300px'), 

969 button_style='danger', 

970 ) 

971 

972 # Scattering Tab sizes 

973 header_widths = [90*1.15, 135*1.15, 110*1.15] 

974 header_widths = [str(i)+'px' for i in header_widths] 

975 a_inv_width = '27px' 

976 a_width = '12px' 

977 a_sq_width = '19px' 

978 

979 # Scattering tab 

980 scattering_tab = VBox([ 

981 # Radiation button 

982 widgets.Image(value=radiation_type_img, format='png', layout=Layout(object_fit='contain', width=header_widths[0])), 

983 radtype_button, 

984 

985 spacing_10px, 

986 

987 # Scattering parameters 

988 widgets.Image(value=scattering_parameters_img, format='png', layout=Layout(object_fit='contain', width=header_widths[1])), 

989 

990 # Q slider 

991 HBox([ 

992 # Q slider img 

993 HBox([widgets.Image(value=qslider_img, format='png', layout=Layout(object_fit='contain', width='120px'))], layout=Layout(width='150px')), 

994 # Q slider 

995 qslider, 

996 # Unit 

997 #¤HBox([widgets.Image(value=a_inv_img, format='png', layout=Layout(object_fit='contain', width=a_inv_width))], layout=Layout(width='50px')), 

998 ]), 

999 

1000 spacing_5px, 

1001 

1002 # r slider 

1003 HBox([ 

1004 # r slider img 

1005 HBox([widgets.Image(value=rslider_img, format='png', layout=Layout(object_fit='contain', width='110px'))], layout=Layout(width='150px')), 

1006 # r slider 

1007 rslider, 

1008 # Unit 

1009 #HBox([widgets.Image(value=a_img, format='png', layout=Layout(object_fit='contain', width=a_width))], layout=Layout(width='50px')), 

1010 ]), 

1011 

1012 spacing_5px, 

1013 

1014 # Other 

1015 HBox([ 

1016 # Qstep img 

1017 HBox([widgets.Image(value=qstep_img, format='png', layout=Layout(object_fit='contain', object_position='', width='65px'))], layout=Layout(width='75px')), 

1018 # Qstep box 

1019 qstep_box, 

1020 # Unit 

1021 #HBox([widgets.Image(value=a_inv_img, format='png', layout=Layout(object_fit='contain', width=a_inv_width))], layout=Layout(width='50px')), 

1022 

1023 # r step img 

1024 widgets.Text(description='', layout=Layout(visibility='hidden', width='60px'), disabled=True), 

1025 HBox([widgets.Image(value=rstep_img, format='png', layout=Layout(object_fit='contain', object_position='', width='55px'))], layout=Layout(width='65px')), 

1026 # r step box 

1027 rstep_box, 

1028 # Unit 

1029 #HBox([widgets.Image(value=a_img, format='png', layout=Layout(object_fit='contain', width=a_width))], layout=Layout(width='50px')), 

1030 

1031 # Q damp img 

1032 widgets.Text(description='', layout=Layout(visibility='hidden', width='60px'), disabled=True), 

1033 HBox([widgets.Image(value=qdamp_img, format='png', layout=Layout(object_fit='contain', object_position='', width='75px'))], layout=Layout(width='85px')), 

1034 # Q damp box 

1035 qdamp_box, 

1036 # Unit 

1037 #HBox([widgets.Image(value=a_inv_img, format='png', layout=Layout(object_fit='contain', width=a_inv_width))], layout=Layout(width='80px')), 

1038 

1039 # r thres img 

1040 widgets.Text(description='', layout=Layout(visibility='hidden', width='60px'), disabled=True), 

1041 HBox([widgets.Image(value=rthres_img, format='png', layout=Layout(object_fit='contain', object_position='', width='55px'))], layout=Layout(width='65px')), 

1042 # r thres 

1043 rthres_box, 

1044 # Unit 

1045 #HBox([widgets.Image(value=a_img, format='png', layout=Layout(object_fit='contain', width=a_width))], layout=Layout(width='50px')), 

1046 

1047 # Global B iso img 

1048 widgets.Text(description='', layout=Layout(visibility='hidden', width='60px'), disabled=True), 

1049 HBox([widgets.Image(value=global_biso_img, format='png', layout=Layout(object_fit='contain', object_position='', width='95px'))], layout=Layout(width='105px')), 

1050 # Global B iso box 

1051 biso_box, 

1052 # Unit 

1053 #HBox([widgets.Image(value=a_sq_img, format='png', layout=Layout(object_fit='contain', width=a_sq_width))], layout=Layout(width='50px')), 

1054 ]), 

1055 

1056 spacing_5px, 

1057 

1058 # Global B iso 

1059 HBox([ 

1060 # Lorch mod button 

1061 lorch_mod_button, 

1062 # Unit 

1063 HBox([widgets.Image(value=a_img, format='png', layout=Layout(object_fit='contain', width=a_width, visibility='hidden'))], layout=Layout(width='50px')), 

1064 ]), 

1065 

1066 spacing_10px, 

1067 

1068 # Presets 

1069 widgets.Image(value=presets_img, format='png', layout=Layout(object_fit='contain', width=header_widths[2])), 

1070 HBox([sas_preset_button, pd_preset_button, ts_preset_button, reset_button]), 

1071 ]) 

1072 

1073 """ Plotting Options """ 

1074 

1075 # Load display display_assets 

1076 with open('display_assets/iq_scaling.png', 'rb') as f: 

1077 iq_scaling_img = f.read() 

1078 with open('display_assets/show_hide.png', 'rb') as f: 

1079 show_hide_img = f.read() 

1080 with open('display_assets/max_norm.png', 'rb') as f: 

1081 max_norm_img = f.read() 

1082 with open('display_assets/iq.png', 'rb') as f: 

1083 iq_img = f.read() 

1084 with open('display_assets/sq.png', 'rb') as f: 

1085 sq_img = f.read() 

1086 with open('display_assets/fq.png', 'rb') as f: 

1087 fq_img = f.read() 

1088 with open('display_assets/gr.png', 'rb') as f: 

1089 gr_img = f.read() 

1090 

1091 # Y-axis I(Q) scale button 

1092 scale_type_button = widgets.ToggleButtons( options=['linear', 'logarithmic'], value='linear', button_style='primary', layout=Layout(width='600')) 

1093 

1094 # Show/Hide buttons 

1095 show_iq_button = widgets.Checkbox(value = True) 

1096 show_sq_button = widgets.Checkbox(value = True) 

1097 show_fq_button = widgets.Checkbox(value = True) 

1098 show_gr_button = widgets.Checkbox(value = True) 

1099 

1100 # Max normalize buttons 

1101 normalize_iq = widgets.Checkbox(value = False) 

1102 normalize_sq = widgets.Checkbox(value = False) 

1103 normalize_fq = widgets.Checkbox(value = False) 

1104 normalize_gr = widgets.Checkbox(value = False) 

1105 

1106 # Plotting tab sizes 

1107 function_offset = '-90px 3px' 

1108 function_size = 35 

1109 header_scale = 0.95 

1110 header_widths = [130, 120, 147] 

1111 header_widths = [str(i * header_scale)+'px' for i in header_widths] 

1112 

1113 # Plotting tab  

1114 plotting_tab = VBox([ 

1115 # I(Q) scaling img 

1116 widgets.Image(value=iq_scaling_img, format='png', layout=Layout(object_fit='contain', width=header_widths[0])), 

1117 scale_type_button, 

1118 

1119 spacing_10px, 

1120 

1121 # Show / Hide img 

1122 widgets.Image(value=show_hide_img, format='png', layout=Layout(object_fit='contain', width=header_widths[1])), 

1123 

1124 # Options 

1125 HBox([ 

1126 HBox([show_iq_button, widgets.Image(value=iq_img, format='png', width=function_size, layout=Layout(object_fit='contain', object_position=function_offset))]), 

1127 HBox([show_sq_button, widgets.Image(value=sq_img, format='png', width=function_size, layout=Layout(object_fit='contain', object_position=function_offset))]), 

1128 HBox([show_fq_button, widgets.Image(value=fq_img, format='png', width=function_size, layout=Layout(object_fit='contain', object_position=function_offset))]), 

1129 HBox([show_gr_button, widgets.Image(value=gr_img, format='png', width=function_size, layout=Layout(object_fit='contain', object_position=function_offset))]), 

1130 ]), 

1131 

1132 spacing_10px, 

1133 

1134 # Max normalization img 

1135 widgets.Image(value=max_norm_img, format='png', layout=Layout(object_fit='contain', width=header_widths[2])), 

1136 

1137 # Options 

1138 HBox([ 

1139 HBox([normalize_iq, widgets.Image(value=iq_img, format='png', width=function_size, layout=Layout(object_fit='contain', object_position=function_offset))]), 

1140 HBox([normalize_sq, widgets.Image(value=sq_img, format='png', width=function_size, layout=Layout(object_fit='contain', object_position=function_offset))]), 

1141 HBox([normalize_fq, widgets.Image(value=fq_img, format='png', width=function_size, layout=Layout(object_fit='contain', object_position=function_offset))]), 

1142 HBox([normalize_gr, widgets.Image(value=gr_img, format='png', width=function_size, layout=Layout(object_fit='contain', object_position=function_offset))]), 

1143 ]), 

1144 ]) 

1145 

1146 

1147 """ Hardware Options Tab """ 

1148 

1149 # Hardware button 

1150 hardware_button = widgets.ToggleButtons(options=['cpu', 'cuda'], value=device, button_style='primary') 

1151 

1152 # Distance batch-size box 

1153 batch_size_box = widgets.IntText(min = 100, max = 10000, value=batch_size) 

1154 

1155 # Hardware tab sizes 

1156 header_scale = 1 

1157 header_widths = [120, 175] 

1158 header_widths = [str(i * header_scale)+'px' for i in header_widths] 

1159 

1160 # Hardware tab 

1161 hardware_tab = VBox([ 

1162 # Choose hardware img 

1163 widgets.Image(value=choose_hardware_img, format='png', layout=Layout(object_fit='contain', width=header_widths[0])), 

1164 

1165 # Hardware box 

1166 hardware_button, 

1167 

1168 spacing_10px, 

1169 

1170 # Distance batch_size img 

1171 widgets.Image(value=batch_size_img, format='png', layout=Layout(object_fit='contain', width=header_widths[1])), 

1172 

1173 # Distance batch size box 

1174 batch_size_box, 

1175 ]) 

1176 

1177 

1178 """ Display tabs """ 

1179 

1180 # Display Tabs 

1181 tabs = widgets.Tab([ 

1182 file_tab, 

1183 scattering_tab, 

1184 plotting_tab, 

1185 hardware_tab, 

1186 ]) 

1187 

1188 # Set tab titles 

1189 tabs.set_title(0, 'File Selection') 

1190 tabs.set_title(1, 'Scattering Options') 

1191 tabs.set_title(2, 'Plotting Options') 

1192 tabs.set_title(3, 'Hardware Options') 

1193 

1194 # Plot button and Download buttons 

1195 plot_button = widgets.Button(description='Plot data', layout=Layout(width='50%', height='90%'), button_style='primary', icon='fa-pencil-square-o') 

1196 download_button = widgets.Button(description="Download- and plot data", layout=Layout(width='50%', height='90%'), button_style='success', icon='fa-download') 

1197 

1198 def display_tabs(): 

1199 display(VBox([tabs, HBox([plot_button, download_button], layout=Layout(width='100%', height='50px'))])) 

1200 

1201 

1202 """ Download utility """ 

1203 

1204 # Download options 

1205 def create_download_link(select_file, select_radius, filename_prefix, data, header=None): 

1206 

1207 # Collect Metadata 

1208 metadata ={ 

1209 'qmin': qslider.value[0], 

1210 'qmax': qslider.value[1], 

1211 'qdamp': qdamp_box.value, 

1212 'qstep': qstep_box.value, 

1213 'rmin': rslider.value[0], 

1214 'rmax': rslider.value[1], 

1215 'rstep': rstep_box.value, 

1216 'rthres': rthres_box.value, 

1217 'biso': biso_box.value, 

1218 'device': hardware_button.value, 

1219 'batch_size': batch_size_box.value, 

1220 'lorch_mod': lorch_mod_button.value, 

1221 'radiation_type': radtype_button.value 

1222 } 

1223 

1224 # Join content 

1225 output = '' 

1226 content = "\n".join([",".join(map(str, np.around(row,len(str(qstep_box.value))))) for row in data]) 

1227 for k,v in metadata.items(): 

1228 output += f'{k}:{v}\n' 

1229 output += '\n' 

1230 if header: 

1231 output += header + '\n' 

1232 output += content 

1233 

1234 # Encode as base64 

1235 b64 = base64.b64encode(output.encode()).decode() 

1236 

1237 # Add Time 

1238 t = datetime.now() 

1239 year = f'{t.year}'[-2:] 

1240 month = f'{t.month}'.zfill(2) 

1241 day = f'{t.day}'.zfill(2) 

1242 hours = f'{t.hour}'.zfill(2) 

1243 minutes = f'{t.minute}'.zfill(2) 

1244 seconds = f'{t.second}'.zfill(2) 

1245 

1246 # Make filename 

1247 if select_radius is not None: 

1248 filename = filename_prefix + '_' + select_file.value.split('/')[-1].split('.')[0] + '_radius' + str(select_radius.value) + '_' + month + day + year + '_' + hours + minutes + seconds + '.csv' 

1249 else: 

1250 filename = filename_prefix + '_' + select_file.value.split('/')[-1].split('.')[0] + '_' + month + day + year + '_' + hours + minutes + seconds + '.csv' 

1251 

1252 # Make href and return 

1253 href = filename_prefix + ':\t' + f'<a href="data:text/csv;base64,{b64}" download="{filename}">{filename}</a>' 

1254 return href 

1255 

1256 def create_structure_download_link(select_file, select_radius, filename_prefix, ase_atoms): 

1257 

1258 # Get atomic properties 

1259 positions = ase_atoms.get_positions() 

1260 elements = ase_atoms.get_chemical_symbols() 

1261 num_atoms = len(ase_atoms) 

1262 

1263 # Make header 

1264 header = str(num_atoms) + "\n\n" 

1265 

1266 # Join content  

1267 content = header + "\n".join([el + '\t' + "\t".join(map(str,np.around(row, 3))) for row, el in zip(positions, elements)]) 

1268 

1269 # Encode as base64 

1270 b64 = base64.b64encode(content.encode()).decode() 

1271 

1272 # Add Time 

1273 t = datetime.now() 

1274 year = f'{t.year}'[-2:] 

1275 month = f'{t.month}'.zfill(2) 

1276 day = f'{t.day}'.zfill(2) 

1277 hours = f'{t.hour}'.zfill(2) 

1278 minutes = f'{t.minute}'.zfill(2) 

1279 seconds = f'{t.second}'.zfill(2) 

1280 

1281 # Make ilename 

1282 filename = filename_prefix + '_' + select_file.value.split('/')[-1].split('.')[0] + '_radius' + str(select_radius.value) + '_' + month + day + year + '_' + hours + minutes + seconds + '.xyz' 

1283 

1284 # Make href and return 

1285 href = filename_prefix + ':\t' + f'<a href="data:text/xyz;base64,{b64}" download="{filename}">{filename}</a>' 

1286 return href 

1287 

1288 

1289 @download_button.on_click 

1290 def on_download_button_click(button): 

1291 global debye_outputs 

1292 # Try to compile all the data and create html link to download files 

1293 try: 

1294 # clear and display 

1295 clear_output(wait=True) 

1296 display_tabs() 

1297 

1298 debye_outputs = [] 

1299 for select_file, select_radius in zip([select_file_1, select_file_2], [select_radius_1, select_radius_2]): 

1300 try: 

1301 path_ext = select_file.value.split('.')[-1] 

1302 except Exception as e: 

1303 return 

1304 if (select_file.value is not None) and (select_file.value not in DEFAULT_MSGS) and (path_ext in ['xyz', 'cif']): 

1305 try: 

1306 debye_calc = DebyeCalculator( 

1307 device=hardware_button.value, 

1308 batch_size=batch_size_box.value, 

1309 radiation_type=radtype_button.value, 

1310 qmin=qslider.value[0], 

1311 qmax=qslider.value[1], 

1312 qstep=qstep_box.value, 

1313 qdamp=qdamp_box.value, 

1314 rmin=rslider.value[0], 

1315 rmax=rslider.value[1], 

1316 rstep=rstep_box.value, 

1317 rthres=rthres_box.value, 

1318 biso=biso_box.value, 

1319 lorch_mod=lorch_mod_button.value 

1320 ) 

1321 if (select_radius.layout.visibility != 'hidden') and (select_radius.value > 8): 

1322 print(f'Generating nanoparticle of radius {select_radius.value} using {select_file.value.split("/")[-1]} ...') 

1323 debye_outputs.append(debye_calc._get_all(select_file.value, select_radius.value)) 

1324 except Exception as e: 

1325 print(f'FAILED: Could not load data file: {path}', end='\r') 

1326 

1327 if len(debye_outputs) < 1: 

1328 print('FAILED: Please select data file(s)', end="\r") 

1329 return 

1330 

1331 i = 0 

1332 for select_file, select_radius in zip([select_file_1, select_file_2], [select_radius_1, select_radius_2]): 

1333 

1334 # Display download links 

1335 if select_file.value not in DEFAULT_MSGS: 

1336 

1337 # Print 

1338 print('Download links for ' + select_file.value.split('/')[-1] + ':') 

1339 

1340 r, q, iq, sq, fq, gr = debye_outputs[i] 

1341 

1342 iq_data = np.column_stack([q, iq]) 

1343 sq_data = np.column_stack([q, sq]) 

1344 fq_data = np.column_stack([q, fq]) 

1345 gr_data = np.column_stack([r, gr]) 

1346 

1347 if select_radius.layout.visibility == 'visible': 

1348 ase_atoms, _ = DebyeCalculator().generate_nanoparticles(select_file.value, select_radius.value) 

1349 display(HTML(create_structure_download_link(select_file, select_radius, f'structure', ase_atoms[0]))) 

1350 display(HTML(create_download_link(select_file, select_radius, 'iq', iq_data, "q,I(Q)"))) 

1351 display(HTML(create_download_link(select_file, select_radius, 'sq', sq_data, "q,S(Q)"))) 

1352 display(HTML(create_download_link(select_file, select_radius, 'fq', fq_data, "q,F(Q)"))) 

1353 display(HTML(create_download_link(select_file, select_radius, 'gr', gr_data, "r,G(r)"))) 

1354 else: 

1355 display(HTML(create_download_link(select_file, None, 'iq', iq_data, "q,I(Q)"))) 

1356 display(HTML(create_download_link(select_file, None, 'sq', sq_data, "q,S(Q)"))) 

1357 display(HTML(create_download_link(select_file, None, 'fq', fq_data, "q,F(Q)"))) 

1358 display(HTML(create_download_link(select_file, None, 'gr', gr_data, "r,G(r)"))) 

1359 print('\n') 

1360 i += 1 

1361 

1362 update_figure(debye_outputs) 

1363 

1364 except Exception as e: 

1365 raise(e) 

1366 print('FAILED: Please select data file(s)', end="\r") 

1367 

1368 """ Observer utility """ 

1369 

1370 # Define a function to update the scattering patterns based on the selected parameters 

1371 def update_options(change): 

1372 folder = change.new 

1373 paths = sorted(glob(os.path.join(folder, '*.xyz')) + glob(os.path.join(folder, '*.cif'))) 

1374 if len(paths): 

1375 for select_file in [select_file_1, select_file_2]: 

1376 select_file.options = ['Select data file'] + paths 

1377 select_file.value = 'Select data file' 

1378 select_file.disabled = False 

1379 else: 

1380 for select_file in [select_file_1, select_file_2]: 

1381 select_file.options = [DEFAULT_MSGS[0]] 

1382 select_file.value = DEFAULT_MSGS[0] 

1383 select_file.disabled = True 

1384 

1385 

1386 def update_options_radius_1(change): 

1387 #select_radius = change.new 

1388 selected_ext = select_file_1.value.split('.')[-1] 

1389 if selected_ext == 'xyz': 

1390 select_radius_desc_1.children[0].layout.visibility = 'hidden' 

1391 select_radius_1.layout.visibility = 'hidden' 

1392 cif_text_1.layout.visibility = 'hidden' 

1393 elif selected_ext == 'cif': 

1394 select_radius_desc_1.children[0].layout.visibility = 'visible' 

1395 select_radius_1.layout.visibility = 'visible' 

1396 cif_text_1.layout.visibility = 'visible' 

1397 else: 

1398 select_radius_desc_1.children[0].layout.visibility = 'hidden' 

1399 select_radius_1.layout.visibility = 'hidden' 

1400 cif_text_1.layout.visibility = 'hidden' 

1401 

1402 def update_options_radius_2(change): 

1403 #select_radius = change.new 

1404 selected_ext = select_file_2.value.split('.')[-1] 

1405 if selected_ext == 'xyz': 

1406 select_radius_desc_2.children[0].layout.visibility = 'hidden' 

1407 select_radius_2.layout.visibility = 'hidden' 

1408 cif_text_2.layout.visibility = 'hidden' 

1409 elif selected_ext == 'cif': 

1410 select_radius_desc_2.children[0].layout.visibility = 'visible' 

1411 select_radius_2.layout.visibility = 'visible' 

1412 cif_text_2.layout.visibility = 'visible' 

1413 else: 

1414 select_radius_desc_2.children[0].layout.visibility = 'hidden' 

1415 select_radius_2.layout.visibility = 'hidden' 

1416 cif_text_2.layout.visibility = 'hidden' 

1417 

1418 # Link the update functions to the dropdown widget's value change event 

1419 folder.observe(update_options, names='value') 

1420 select_file_1.observe(update_options_radius_1, names='value') 

1421 select_file_2.observe(update_options_radius_2, names='value') 

1422 

1423 

1424 """ Plotting utility """ 

1425 

1426 def togglelorch(change): 

1427 if change['new']: 

1428 lorch_mod_button.description = 'Lorch modification (ON)' 

1429 else: 

1430 lorch_mod_button.description = 'Lorch modification (OFF)' 

1431 

1432 lorch_mod_button.observe(togglelorch, 'value') 

1433 

1434 @sas_preset_button.on_click 

1435 def sas_preset(b=None): 

1436 # Change scale type 

1437 scale_type_button.value = 'logarithmic' 

1438 

1439 # Hide all but IQ 

1440 show_iq_button.value = True 

1441 show_fq_button.value = False 

1442 show_sq_button.value = False 

1443 show_gr_button.value = False 

1444 

1445 # Set qmin and qmax 

1446 qslider.value = [0.0, 3.0] 

1447 qstep_box.value = 0.01 

1448 

1449 @pd_preset_button.on_click 

1450 def pd_preset(b=None): 

1451 # Change scale type 

1452 scale_type_button.value = 'linear' 

1453 

1454 # Hide all but IQ 

1455 show_iq_button.value = True 

1456 show_fq_button.value = False 

1457 show_sq_button.value = False 

1458 show_gr_button.value = False 

1459 

1460 # Set qmin and qmax 

1461 qslider.value = [1.0, 8.0] 

1462 qstep_box.value = 0.1 

1463 

1464 @ts_preset_button.on_click 

1465 def ts_preset(b=None): 

1466 # Change scale type 

1467 scale_type_button.value = 'linear' 

1468 

1469 # Hide all but IQ 

1470 show_iq_button.value = True 

1471 show_fq_button.value = True 

1472 show_sq_button.value = False 

1473 show_gr_button.value = True 

1474 

1475 # Set qmin and qmax 

1476 qslider.value = [1.0, 30.0] 

1477 qstep_box.value = 0.1 

1478 

1479 @reset_button.on_click 

1480 def reset(b=None): 

1481 # Change scale type 

1482 scale_type_button.value = 'linear' 

1483 

1484 # Hide all but IQ 

1485 show_iq_button.value = True 

1486 show_fq_button.value = True 

1487 show_sq_button.value = True 

1488 show_gr_button.value = True 

1489 

1490 # Set qmin and qmax 

1491 qslider.value = [1.0, 30.0] 

1492 rslider.value = [0.0, 20.0] 

1493 qstep_box.value = 0.1 

1494 rstep_box.value = 0.01 

1495 biso_box.value = 0.3 

1496 qdamp_box.value = 0.04 

1497 rthres_box.value = 0.0 

1498 

1499 def update_figure(debye_outputs, _unity_sq=True): 

1500 

1501 xseries, yseries = [], [] 

1502 xlabels, ylabels = [], [] 

1503 scales, titles = [], [] 

1504 axis_ids = [] 

1505 

1506 normalize_iq_text = ' [counts]' if not normalize_iq.value else ' [normalized]' 

1507 normalize_sq_text = '' if not normalize_iq.value else ' [normalized]' 

1508 normalize_fq_text = '' if not normalize_iq.value else ' [normalized]' 

1509 normalize_gr_text = '' if not normalize_iq.value else ' [normalized]' 

1510 

1511 for do in debye_outputs: 

1512 if show_iq_button.value: 

1513 axis_ids.append(0) 

1514 xseries.append(do[1]) # q 

1515 iq_ = do[2] if not normalize_iq.value else do[2]/max(do[2]) 

1516 yseries.append(iq_) # iq 

1517 xlabels.append('$Q$ [$\AA^{-1}$]') 

1518 ylabels.append('$I(Q)$' + normalize_iq_text) 

1519 if scale_type_button.value == 'logarithmic': 

1520 scales.append('log') 

1521 else: 

1522 scales.append('linear') 

1523 scale = scale_type_button.value 

1524 titles.append('Scattering Intensity, I(Q)') 

1525 if show_sq_button.value: 

1526 axis_ids.append(1) 

1527 xseries.append(do[1]) # q 

1528 sq_ = do[3] if not normalize_sq.value else do[3]/max(do[3]) 

1529 yseries.append(sq_) # sq 

1530 xlabels.append('$Q$ [$\AA^{-1}$]') 

1531 ylabels.append('$S(Q)$' + normalize_sq_text) 

1532 scales.append('linear') 

1533 titles.append('Structure Function, S(Q)') 

1534 if show_fq_button.value: 

1535 axis_ids.append(2) 

1536 xseries.append(do[1]) # q 

1537 fq_ = do[4] if not normalize_fq.value else do[4]/max(do[4]) 

1538 yseries.append(fq_) # fq 

1539 xlabels.append('$Q$ [$\AA^{-1}$]') 

1540 ylabels.append('$F(Q)$'+ normalize_fq_text) 

1541 scales.append('linear') 

1542 titles.append('Reduced Structure Function, F(Q)') 

1543 if show_gr_button.value: 

1544 axis_ids.append(3) 

1545 xseries.append(do[0]) # r 

1546 gr_ = do[5] if not normalize_gr.value else do[5]/max(do[5]) 

1547 yseries.append(gr_) # gr 

1548 xlabels.append('$r$ [$\AA$]') 

1549 ylabels.append('$G(r)$' + normalize_gr_text) 

1550 scales.append('linear') 

1551 titles.append('Reduced Pair Distribution Function, G(r)') 

1552 

1553 sup_title = [] 

1554 labels = [] 

1555 if select_file_1.value not in ['Select data file', 'No valid files in entered directory']: 

1556 

1557 sup_title.append(select_file_1.value.split('/')[-1]) 

1558 

1559 if select_radius_1.layout.visibility == 'hidden': 

1560 labels.append(sup_title[-1]) 

1561 else: 

1562 labels.append(sup_title[-1] + ', rad.: ' + str(select_radius_1.value) + ' Å') 

1563 

1564 if select_file_2.value not in ['Select data file', 'No valid files in entered directory']: 

1565 

1566 sup_title.append(select_file_2.value.split('/')[-1]) 

1567 

1568 if select_radius_2.layout.visibility == 'hidden': 

1569 labels.append(sup_title[-1]) 

1570 else: 

1571 labels.append(sup_title[-1] + ', rad.: ' + str(select_radius_2.value) + ' Å') 

1572 

1573 if len(labels) == 0: 

1574 return 

1575 

1576 num_plots = int(show_iq_button.value) + int(show_sq_button.value) + int(show_fq_button.value) + int(show_gr_button.value) 

1577 if num_plots == 4: 

1578 fig, axs = plt.subplots(2,2,figsize=(12, 8), dpi=75) 

1579 axs = axs.ravel() 

1580 elif num_plots == 3: 

1581 fig, axs = plt.subplots(3,1,figsize=(12,8), dpi=75) 

1582 elif num_plots == 2: 

1583 fig, axs = plt.subplots(2,1,figsize=(12,8), dpi=75) 

1584 elif num_plots == 1: 

1585 fig, axs = plt.subplots(figsize=(12,6), dpi=75) 

1586 axs = [axs] 

1587 else: 

1588 return 

1589 

1590 for i,(x,y,xl,yl,s,t,l) in enumerate(zip(xseries, yseries, xlabels, ylabels, scales, titles, np.repeat(labels, num_plots))): 

1591 

1592 ii = i % num_plots 

1593 axs[ii].set_xscale(s) 

1594 axs[ii].set_yscale(s) 

1595 axs[ii].plot(x,y, label=l) 

1596 axs[ii].set(xlabel=xl, ylabel=yl, title=t) 

1597 axs[ii].relim() 

1598 axs[ii].autoscale_view() 

1599 axs[ii].grid(alpha=0.2, which='both') 

1600 axs[ii].legend() 

1601 

1602 if len(sup_title) == 1: 

1603 title = f"Showing files: {sup_title[0]}" 

1604 else: 

1605 title = f"Showing files: {sup_title[0]} and {sup_title[1]}" 

1606 fig.suptitle(title) 

1607 fig.tight_layout() 

1608 

1609 @plot_button.on_click 

1610 def update_parameters(b=None): 

1611 global debye_outputs 

1612 

1613 debye_outputs = [] 

1614 for select_file, select_radius in zip([select_file_1, select_file_2], [select_radius_1, select_radius_2]): 

1615 try: 

1616 path_ext = select_file.value.split('.')[-1] 

1617 except Exception as e: 

1618 return 

1619 if (select_file.value is not None) and (select_file.value not in [DEFAULT_MSGS]) and (path_ext in ['xyz', 'cif']): 

1620 try: 

1621 # TODO if not changed, dont make new object 

1622 debye_calc = DebyeCalculator( 

1623 device=hardware_button.value, 

1624 batch_size=batch_size_box.value, 

1625 radiation_type=radtype_button.value, 

1626 qmin=qslider.value[0], 

1627 qmax=qslider.value[1], 

1628 qstep=qstep_box.value, 

1629 qdamp=qdamp_box.value, 

1630 rmin=rslider.value[0], 

1631 rmax=rslider.value[1], 

1632 rstep=rstep_box.value, 

1633 rthres=rthres_box.value, 

1634 biso=biso_box.value, 

1635 lorch_mod=lorch_mod_button.value 

1636 ) 

1637 if not select_radius.disabled and select_radius.value > 8: 

1638 print(f'Generating nanoparticle of radius {select_radius.value} using {select_file.value.split("/")[-1]} ...') 

1639 debye_outputs.append(debye_calc._get_all(select_file.value, select_radius.value)) 

1640 except Exception as e: 

1641 print(f'FAILED: Could not load data file: {path}', end='\r') 

1642 

1643 # Clear and display 

1644 clear_output(wait=True) 

1645 display_tabs() 

1646 

1647 if len(debye_outputs) < 1: 

1648 print('FAILED: Please select data file(s)', end="\r") 

1649 return 

1650 

1651 update_figure(debye_outputs) 

1652 

1653 # Display tabs when function is called 

1654 display_tabs()