Coverage for biobb_analysis/ambertools/common.py: 63%

274 statements  

« prev     ^ index     » next       coverage.py v7.15.3, created at 2026-08-07 12:30 +0000

1""" Common functions for package biobb_analysis.ambertools """ 

2from pathlib import Path, PurePath 

3import zipfile 

4import shutil 

5from biobb_common.tools import file_utils as fu 

6 

7 

8def check_top_path(path, out_log, classname): 

9 """ Checks topology input file """ 

10 orig_path = path 

11 if not Path(path).exists(): 

12 fu.log(classname + ': Unexisting topology input file, exiting', out_log) 

13 raise SystemExit(classname + ': Unexisting topology input file') 

14 file_extension = PurePath(path).suffix 

15 if not is_valid_topology(file_extension[1:]): 

16 fu.log(classname + ': Format %s in topology input file is not compatible' % file_extension[1:], out_log) 

17 raise SystemExit(classname + ': Format %s in topology input file is not compatible' % file_extension[1:]) 

18 if zipfile.is_zipfile(path): 

19 top_file = fu.unzip_top(zip_file=path, out_log=out_log) 

20 path = top_file 

21 return path, orig_path 

22 

23 

24def check_traj_path(path, out_log, classname): 

25 """ Checks trajectory input file """ 

26 if not Path(path).exists(): 

27 fu.log(classname + ': Unexisting trajectory input file, exiting', out_log) 

28 raise SystemExit(classname + ': Unexisting trajectory input file') 

29 file_extension = PurePath(path).suffix 

30 if not is_valid_trajectory(file_extension[1:]): 

31 fu.log(classname + ': Format %s in trajectory input file is not compatible' % file_extension[1:], out_log) 

32 raise SystemExit(classname + ': Format %s in trajectory input file is not compatible' % file_extension[1:]) 

33 return path 

34 

35 

36def check_out_path(path, out_log, classname): 

37 """ Checks if output folder exists """ 

38 if PurePath(path).parent and not Path(PurePath(path).parent).exists(): 

39 fu.log(classname + ': Unexisting output folder, exiting', out_log) 

40 raise SystemExit(classname + ': Unexisting output folder') 

41 return path 

42 

43 

44def get_parameters(properties, type, classname, out_log): 

45 """ Gets in_parameters and out_parameters """ 

46 if not properties.get(type, dict()): 

47 fu.log('No %s parameters provided' % type, out_log) 

48 return get_default_value(classname)[type] 

49 

50 return {k: v for k, v in properties.get(type, dict()).items()} 

51 

52 

53def get_binary_path(properties, type): 

54 """ Gets binary path """ 

55 return properties.get(type, get_default_value(type)) 

56 

57 

58def check_in_path(path, out_log, classname): 

59 """ Checks input instructions file """ 

60 if not Path(path).exists(): 

61 fu.log(classname + ': Unexisting input instructions file, exiting', out_log) 

62 raise SystemExit(classname + ': Unexisting input instructions file') 

63 

64 # check syntax for instructions file 

65 syntax_instructions = True 

66 err_instructions = '' 

67 if 'parm ' not in open(path).read(): 

68 syntax_instructions = False 

69 err_instructions += 'No topology provided' 

70 if 'trajin ' not in open(path).read(): 

71 syntax_instructions = False 

72 err_instructions += ' No input trajectory provided' 

73 if not syntax_instructions: 

74 fu.log(classname + ': Incorrect syntax for instructions file: %s, exiting' % err_instructions, out_log) 

75 raise SystemExit(classname + ': Incorrect syntax for instructions file: %s, exiting' % err_instructions) 

76 

77 

78def get_default_value(key): 

79 """ Gives default values according to the given key """ 

80 default_values = { 

81 "start": 1, 

82 "end": -1, 

83 "step": 1, 

84 "snapshot": 1, 

85 "format": "netcdf", 

86 "mask": "all-atoms", 

87 "reference": "first", 

88 "average": "MyAvg", 

89 "instructions_file": "instructions.in", 

90 "binary_path": "cpptraj", 

91 "algorithm": "hieragglo", 

92 "metric": "rms", 

93 "linkage": "linkage", 

94 "clusters": 10, 

95 "minpoints": 4, 

96 "cluster_set": "MyClusters", 

97 # default conf for Average 

98 "Average": { 

99 "in_parameters": { 

100 "start": 1, 

101 "end": -1, 

102 "step": 1, 

103 "mask": "all-atoms" 

104 }, 

105 "out_parameters": { 

106 "format": "pdb" 

107 } 

108 }, 

109 # default conf for Bfactor 

110 "Bfactor": { 

111 "in_parameters": { 

112 "start": 1, 

113 "end": -1, 

114 "step": 1, 

115 "mask": "all-atoms", 

116 "reference": "first" 

117 } 

118 }, 

119 # default conf for Cluster 

120 "Cluster": { 

121 "in_parameters": { 

122 "start": 1, 

123 "end": -1, 

124 "step": 1, 

125 "mask": "all-atoms" 

126 } 

127 }, 

128 # default conf for Convert 

129 "Convert": { 

130 "in_parameters": { 

131 "start": 1, 

132 "end": -1, 

133 "step": 1, 

134 "mask": "all-atoms" 

135 }, 

136 "out_parameters": { 

137 "format": "netcdf" 

138 } 

139 }, 

140 # default conf for Dry 

141 "Dry": { 

142 "in_parameters": { 

143 "start": 1, 

144 "end": -1, 

145 "step": 1, 

146 "mask": "all-atoms" 

147 }, 

148 "out_parameters": { 

149 "format": "netcdf" 

150 } 

151 }, 

152 # default conf for Image 

153 "Image": { 

154 "in_parameters": { 

155 "start": 1, 

156 "end": -1, 

157 "step": 1, 

158 "mask": "all-atoms" 

159 }, 

160 "out_parameters": { 

161 "format": "netcdf" 

162 } 

163 }, 

164 # default conf for Mask 

165 "Mask": { 

166 "in_parameters": { 

167 "start": 1, 

168 "end": -1, 

169 "step": 1, 

170 "mask": "all-atoms" 

171 }, 

172 "out_parameters": { 

173 "format": "netcdf" 

174 } 

175 }, 

176 # default conf for Rgyr 

177 "Rgyr": { 

178 "in_parameters": { 

179 "start": 1, 

180 "end": -1, 

181 "step": 1, 

182 "mask": "all-atoms" 

183 } 

184 }, 

185 # default conf for Rms 

186 "Rms": { 

187 "in_parameters": { 

188 "start": 1, 

189 "end": -1, 

190 "step": 1, 

191 "mask": "all-atoms", 

192 "reference": "first" 

193 } 

194 }, 

195 # default conf for Rmsf 

196 "Rmsf": { 

197 "in_parameters": { 

198 "start": 1, 

199 "end": -1, 

200 "step": 1, 

201 "mask": "all-atoms", 

202 "reference": "first" 

203 } 

204 }, 

205 # default conf for Slice 

206 "Slice": { 

207 "in_parameters": { 

208 "start": 1, 

209 "end": -1, 

210 "step": 1, 

211 "mask": "all-atoms" 

212 }, 

213 "out_parameters": { 

214 "format": "netcdf" 

215 } 

216 }, 

217 # default conf for Snapshot 

218 "Snapshot": { 

219 "in_parameters": { 

220 "snapshot": 12, 

221 "mask": "all-atoms" 

222 }, 

223 "out_parameters": { 

224 "format": "pdb" 

225 } 

226 }, 

227 # default conf for Strip 

228 "Strip": { 

229 "in_parameters": { 

230 "start": 1, 

231 "end": -1, 

232 "step": 1, 

233 "mask": "all-atoms" 

234 }, 

235 "out_parameters": { 

236 "format": "netcdf" 

237 } 

238 } 

239 } 

240 

241 return default_values[key] 

242 

243 

244def is_valid_topology(ext): 

245 """ Checks if trajectory format is compatible with Cpptraj """ 

246 formats = 'top', 'pdb', 'prmtop', 'parmtop', 'zip' 

247 return ext in formats 

248 

249 

250def is_valid_trajectory(traj): 

251 """ Checks if trajectory format is compatible with Cpptraj """ 

252 formats = 'mdcrd', 'crd', 'cdf', 'netcdf', 'nc', 'restart', 'ncrestart', 'restartnc', 'dcd', 'charmm', 'cor', 'pdb', 'mol2', 'trr', 'gro', 'binpos', 'xtc', 'cif', 'arc', 'sqm', 'sdf', 'conflib' 

253 return traj in formats 

254 

255 

256def is_valid_reference(ref): 

257 """ Checks if reference is correct """ 

258 references = 'first', 'average', 'experimental' 

259 return ref in references 

260 

261 

262def is_valid_cluster_algorithm(algorithm): 

263 """ Checks if clustering algorithm is compatible with Cpptraj """ 

264 algorithms = 'hieragglo', 'dbscan', 'kmeans', 'dpeaks' 

265 return algorithm in algorithms 

266 

267 

268def is_valid_cluster_metric(metric): 

269 """ Checks if clustering distance metric is compatible with Cpptraj """ 

270 metrics = 'rms', 'srmsd', 'dme', 'qrmsd' 

271 return metric in metrics 

272 

273 

274def is_valid_cluster_linkage(linkage): 

275 """ Checks if hierarchical agglomerative linkage is compatible with Cpptraj """ 

276 linkages = 'linkage', 'averagelinkage', 'complete' 

277 return linkage in linkages 

278 

279 

280def get_cluster_algorithm(algorithm, clusters, epsilon, minpoints, linkage, out_log): 

281 """ Return string with the clustering algorithm keywords """ 

282 if not algorithm or algorithm == 'None': 

283 algorithm = get_default_value('algorithm') 

284 fu.log('No algorithm provided in configuration file, assigned default value: %s' % get_default_value('algorithm'), out_log) 

285 

286 if not is_valid_cluster_algorithm(algorithm): 

287 fu.log('Algorithm %s is not compatible, assigned default value: %s' % (algorithm, get_default_value('algorithm')), out_log) 

288 algorithm = get_default_value('algorithm') 

289 

290 if algorithm == 'hieragglo': 

291 if not is_valid_cluster_linkage(linkage): 

292 fu.log('Linkage %s is not compatible, assigned default value: %s' % (linkage, get_default_value('linkage')), out_log) 

293 linkage = get_default_value('linkage') 

294 keywords = ['hieragglo', 'clusters', str(clusters), linkage] 

295 # epsilon is an additional stop condition for hieragglo, only added when provided 

296 if epsilon is not None and epsilon > 0: 

297 keywords += ['epsilon', str(epsilon)] 

298 elif algorithm == 'kmeans': 

299 keywords = ['kmeans', 'clusters', str(clusters)] 

300 else: 

301 # both dbscan and dpeaks need epsilon 

302 if epsilon is None or epsilon <= 0: 

303 fu.log('Algorithm %s needs a positive epsilon, assigned default value: 1.0' % algorithm, out_log) 

304 epsilon = 1.0 

305 if algorithm == 'dbscan': 

306 keywords = ['dbscan', 'minpoints', str(minpoints), 'epsilon', str(epsilon)] 

307 else: 

308 keywords = ['dpeaks', 'epsilon', str(epsilon), 'choosepoints', 'auto'] 

309 

310 return ' '.join(keywords) 

311 

312 

313def get_cluster_metric(metric, mass, nofit, metric_mask, out_log): 

314 """ Return string with the clustering distance metric keywords """ 

315 if not metric or metric == 'None': 

316 metric = get_default_value('metric') 

317 fu.log('No metric provided in configuration file, assigned default value: %s' % get_default_value('metric'), out_log) 

318 

319 if not is_valid_cluster_metric(metric): 

320 fu.log('Metric %s is not compatible, assigned default value: %s' % (metric, get_default_value('metric')), out_log) 

321 metric = get_default_value('metric') 

322 

323 keywords = [metric] 

324 if mass: 

325 keywords.append('mass') 

326 if nofit: 

327 keywords.append('nofit') 

328 

329 if not metric_mask or metric_mask == 'None': 

330 metric_mask = get_default_value('mask') 

331 fu.log('No metric_mask provided in configuration file, assigned default value: %s' % get_default_value('mask'), out_log) 

332 # the metric mask selects atoms, it does not remove them from the clustered coordinates 

333 keywords.append(get_mask(metric_mask, out_log)) 

334 

335 return ' '.join(keywords) 

336 

337 

338def get_traj_format(path, out_log): 

339 """ Return the Cpptraj trajectory format matching the extension of the given path """ 

340 format = PurePath(path).suffix[1:] 

341 if not is_valid_trajectory(format): 

342 fu.log('Format %s is not compatible, assigned default value: %s' % (format, get_default_value('format')), out_log) 

343 format = get_default_value('format') 

344 

345 return format 

346 

347 

348def get_mask_atoms(key): 

349 """ Gives mask atoms according to the given key """ 

350 masks = { 

351 "c-alpha": "@CA", 

352 "backbone": "@C,CA,N,O,C3',O3',C4',C5',O5',P", 

353 "all-atoms": ":*", 

354 "heavy-atoms": "!@H*,1H*,2H*,3H*", 

355 "side-chain": "!@CA,C,N,O,H,HA,C3',O3',C4',C5',O5',P", 

356 "solute": "!:WAT,HOH,SOL,TIP3,TP3,SOD,CLA,Na+,Cl-,NA,CL,K+,K", 

357 "ions": ":SOD,CLA,Na+,Cl-,NA,CL,K+,K", 

358 "solvent": ":WAT,HOH,SOL,TIP3,TP3" 

359 } 

360 

361 # if key incorrect, return default value and message 

362 if key in masks: 

363 return masks[key], None 

364 else: 

365 return key, None # Allow for Amber mask 

366 

367 

368def get_in_parameters(list, out_log, type='None'): 

369 """ Return string with input parameters """ 

370 # if strip or mask, no mandatory trajin parameters 

371 if type == 'strip' or type == 'mask': 

372 start = '' if 'start' not in list else str(list['start']) 

373 end = '' if 'end' not in list else str(list['end']) 

374 step = '' if 'step' not in list else str(list['step']) 

375 if (not start or start == 'None') and (not end or end == 'None') and (not step or step == 'None'): 

376 return '' 

377 else: 

378 if not start: 

379 start = str(get_default_value("start")) 

380 fu.log('No start value provided in configuration file or incorrect format, assigned default value: %s' % get_default_value('start'), out_log) 

381 if not end: 

382 end = str(get_default_value("end")) 

383 fu.log('No end value provided in configuration file or incorrect format, assigned default value: %s' % get_default_value('end'), out_log) 

384 if not step: 

385 step = str(get_default_value("step")) 

386 fu.log('No step value provided in configuration file or incorrect format, assigned default value: %s' % get_default_value('step'), out_log) 

387 else: 

388 # check if trajin parameters are provided and have correct format 

389 if (type == 'snapshot'): 

390 snapshot = str(get_default_value("snapshot")) if 'snapshot' not in list else str(list['snapshot']) 

391 if 'snapshot' not in list or not list['snapshot'] or not isinstance(list['snapshot'], int): 

392 snapshot = str(get_default_value("snapshot")) 

393 fu.log('No snapshot value provided in configuration file or incorrect format, assigned default value: %s' % get_default_value('snapshot'), out_log) 

394 start = snapshot 

395 end = snapshot 

396 step = '1' 

397 else: 

398 start = str(get_default_value("start")) if 'start' not in list else str(list['start']) 

399 if 'start' not in list or not list['start'] or not isinstance(list['start'], int): 

400 start = str(get_default_value("start")) 

401 fu.log('No start value provided in configuration file or incorrect format, assigned default value: %s' % get_default_value('start'), out_log) 

402 end = str(get_default_value("end")) if 'end' not in list else str(list['end']) 

403 if 'end' not in list or not list['end'] or not isinstance(list['end'], int): 

404 end = str(get_default_value("end")) 

405 fu.log('No end value provided in configuration file or incorrect format, assigned default value: %s' % get_default_value('end'), out_log) 

406 step = str(get_default_value("step")) if 'step' not in list else str(list['step']) 

407 if 'step' not in list or not list['step'] or not isinstance(list['step'], int): 

408 step = str(get_default_value("step")) 

409 fu.log('No step value provided in configuration file or incorrect format, assigned default value: %s' % get_default_value('step'), out_log) 

410 

411 # checking start <= end 

412 if end != '-1' and start > end: 

413 fu.log('End must be -1 (indicating the end of the trajectory) or more or equal than start. Your values are start: %s, end: %s' % (start, end), out_log) 

414 raise SystemExit('End must be -1 (indicating the end of the trajectory) or greater or equal than start. Your values are start: %s, end: %s' % (start, end)) 

415 

416 return start + " " + end + " " + step 

417 

418 

419def setup_structure(out_log): 

420 """ Sets up the structure """ 

421 instructions_list = [] 

422 mask_atoms = get_mask('heavy-atoms', out_log) 

423 instructions_list.append('center ' + mask_atoms + ' origin') 

424 instructions_list.append('autoimage') 

425 instructions_list.append('rms first ' + mask_atoms) 

426 mask_solvent = get_mask('solvent', out_log) 

427 mask_ions = get_mask('ions', out_log) 

428 instructions_list.append('strip ' + mask_solvent + ',' + mask_ions[1:]) 

429 

430 return instructions_list 

431 

432 

433def get_negative_mask(key, out_log): 

434 """ Gives the negative mask according to the given key """ 

435 atoms, msg = get_mask_atoms(key) 

436 if atoms[0] == '!': 

437 mask = atoms[1:] 

438 else: 

439 mask = '!' + atoms 

440 # if mask incorrect, give message 

441 if msg: 

442 fu.log(msg, out_log) 

443 

444 return mask 

445 

446 

447def get_mask(key, out_log): 

448 """ Gives mask according to the given key """ 

449 mask, msg = get_mask_atoms(key) 

450 # if mask incorrect, give message 

451 if msg: 

452 fu.log(msg, out_log) 

453 

454 return mask 

455 

456 

457def get_reference(ref, output_cpptraj_path, input_exp_path, mask, output, classname, out_log): 

458 """ Gives reference instructions according to the given key """ 

459 instructions_list = [] 

460 if not ref or ref == 'None': 

461 ref = get_default_value('reference') 

462 fu.log('No reference provided in configuration file, assigned default value: %s' % get_default_value('reference'), out_log) 

463 

464 if not is_valid_reference(ref): 

465 fu.log('Reference %s is not compatible, assigned default value: %s' % (ref, get_default_value('reference')), out_log) 

466 ref = get_default_value('reference') 

467 

468 if ref == 'first': 

469 if output: 

470 instructions_list.append('rms first out ' + output_cpptraj_path) 

471 else: 

472 instructions_list.append('rms first') 

473 

474 if ref == 'average': 

475 instructions_list.append('average crdset ' + get_default_value('average')) 

476 instructions_list.append('run') 

477 if output: 

478 instructions_list.append('rms ref ' + get_default_value('average') + ' ' + mask + ' out ' + output_cpptraj_path) 

479 else: 

480 instructions_list.append('rms ref ' + get_default_value('average') + ' ' + mask) 

481 

482 if ref == 'experimental': 

483 if not input_exp_path: 

484 fu.log('No experimental structure provided, exiting', out_log) 

485 raise SystemExit(classname + ': input_exp_path is mandatory') 

486 instructions_list.append('parm ' + input_exp_path + ' noconect [exp]') 

487 solute, msg = get_mask_atoms('solute') 

488 instructions_list.append('reference ' + input_exp_path + ' ' + solute + ' parm [exp]') 

489 backbone, msg = get_mask_atoms('backbone') 

490 if output: 

491 instructions_list.append('rms reference ' + mask + ' out ' + output_cpptraj_path) 

492 else: 

493 instructions_list.append('rms reference ' + mask) 

494 

495 return instructions_list 

496 

497 

498def get_reference_rms(ref, output_cpptraj_path, input_exp_path, mask, output, classname, out_log, nofit=False, norotate=False, nomod=False): 

499 """ Gives reference instructions according to the given key """ 

500 instructions_list = [] 

501 if not ref or ref == 'None': 

502 ref = get_default_value('reference') 

503 fu.log('No reference provided in configuration file, assigned default value: %s' % get_default_value('reference'), out_log) 

504 

505 if not is_valid_reference(ref): 

506 fu.log('Reference %s is not compatible, assigned default value: %s' % (ref, get_default_value('reference')), out_log) 

507 ref = get_default_value('reference') 

508 

509 flags = [] 

510 if nofit: 

511 flags.append("nofit") 

512 if norotate: 

513 flags.append("norotate") 

514 if nomod: 

515 flags.append("nomod") 

516 

517 flags_str = " ".join(flags) 

518 

519 if ref == 'first': 

520 if output: 

521 instructions_list.append('rms first out ' + output_cpptraj_path + f' {flags_str}') 

522 else: 

523 instructions_list.append('rms first' + f' {flags_str}') 

524 

525 if ref == 'average': 

526 instructions_list.append('average crdset ' + get_default_value('average')) 

527 instructions_list.append('run') 

528 if output: 

529 instructions_list.append('rms ref ' + get_default_value('average') + ' ' + mask + ' out ' + output_cpptraj_path + f' {flags_str}') 

530 else: 

531 instructions_list.append('rms ref ' + get_default_value('average') + ' ' + mask + f' {flags_str}') 

532 

533 if ref == 'experimental': 

534 if not input_exp_path: 

535 fu.log('No experimental structure provided, exiting', out_log) 

536 raise SystemExit(classname + ': input_exp_path is mandatory') 

537 instructions_list.append('parm ' + input_exp_path + ' noconect [exp]') 

538 solute, msg = get_mask_atoms('solute') 

539 instructions_list.append('reference ' + input_exp_path + ' ' + solute + ' parm [exp]') 

540 backbone, msg = get_mask_atoms('backbone') 

541 if output: 

542 instructions_list.append('rms reference ' + mask + ' out ' + output_cpptraj_path + f' {flags_str}') 

543 else: 

544 instructions_list.append('rms reference ' + mask + f' {flags_str}') 

545 

546 return instructions_list 

547 

548 

549def get_out_parameters(list, out_log): 

550 """ Return string with output parameters """ 

551 format = list['format'] 

552 # msg = None 

553 # check if format provided 

554 if not format: 

555 format = get_default_value('format') 

556 fu.log('No format provided in configuration file, assigned default value: %s' % get_default_value('format'), out_log) 

557 # check if valid format 

558 if not is_valid_trajectory(format): 

559 fu.log('Format %s is not compatible, assigned default value: %s' % (format, get_default_value('format')), out_log) 

560 format = get_default_value('format') 

561 

562 return format 

563 

564 

565def copy_instructions_file_to_container(instructions_file, unique_dir): 

566 shutil.copy2(instructions_file, unique_dir) 

567 

568 

569def remove_tmp_files(list, remove_tmp, out_log, input_top_path_orig=None, input_top_path=None): 

570 """ Removes temporal files generated by the wrapper """ 

571 tmp_files = list 

572 if zipfile.is_zipfile(str(input_top_path_orig)): 

573 tmp_files.append(PurePath(str(input_top_path)).parent) 

574 

575 if remove_tmp: 

576 removed_files = [f for f in tmp_files if fu.rm(f)] 

577 fu.log('Removed: %s' % str(removed_files), out_log)