Coverage for biobb_gromacs / gromacs_extra / ndx2resttop.py: 75%

111 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-05 08:26 +0000

1#!/usr/bin/env python3 

2 

3"""Module containing the Ndx2resttop class and the command line interface.""" 

4import fnmatch 

5from typing import Optional 

6from typing import Any 

7from pathlib import Path 

8from biobb_common.generic.biobb_object import BiobbObject 

9from biobb_common.tools import file_utils as fu 

10from biobb_common.tools.file_utils import launchlogger 

11 

12 

13class Ndx2resttop(BiobbObject): 

14 """ 

15 | biobb_gromacs Ndx2resttop 

16 | Generate a restrained topology from an index NDX file. 

17 | This module automatizes the process of restrained topology generation starting from an index NDX file. 

18 

19 Args: 

20 input_ndx_path (str): Path to the input NDX index file. File type: input. `Sample file <https://github.com/bioexcel/biobb_gromacs/raw/master/biobb_gromacs/test/data/gromacs_extra/ndx2resttop.ndx>`_. Accepted formats: ndx (edam:format_2033). 

21 input_top_zip_path (str): Path the input TOP topology in zip format. File type: input. `Sample file <https://github.com/bioexcel/biobb_gromacs/raw/master/biobb_gromacs/test/data/gromacs_extra/ndx2resttop.zip>`_. Accepted formats: zip (edam:format_3987). 

22 output_top_zip_path (str): Path the output TOP topology in zip format. File type: output. `Sample file <https://github.com/bioexcel/biobb_gromacs/raw/master/biobb_gromacs/test/reference/gromacs_extra/ref_ndx2resttop.zip>`_. Accepted formats: zip (edam:format_3987). 

23 properties (dict - Python dictionary object containing the tool parameters, not input/output files): 

24 * **posres_names** (*list*) - ("CUSTOM_POSRES") String with names of the position restraints to be included in the topology file separated by spaces. If provided it should match the length of the ref_rest_chain_triplet_list. 

25 * **force_constants** (*str*) - ("500 500 500") Array of three floats defining the force constants. 

26 * **ref_rest_chain_triplet_list** (*str*) - (None) Triplet list composed by (reference group, restrain group, chain) list. 

27 

28 Examples: 

29 This is a use example of how to use the building block from Python:: 

30 

31 from biobb_gromacs.gromacs_extra.ndx2resttop import ndx2resttop 

32 prop = { 'ref_rest_chain_triplet_list': '( Chain_A, Chain_A_noMut, A ), ( Chain_B, Chain_B_noMut, B ), ( Chain_C, Chain_C_noMut, C ), ( Chain_D, Chain_D_noMut, D )' } 

33 ndx2resttop(input_ndx_path='/path/to/myIndex.ndx', 

34 input_top_zip_path='/path/to/myTopology.zip', 

35 output_top_zip_path='/path/to/newTopology.zip', 

36 properties=prop) 

37 

38 Info: 

39 * wrapped_software: 

40 * name: In house 

41 * license: Apache-2.0 

42 * ontology: 

43 * name: EDAM 

44 * schema: http://edamontology.org/EDAM.owl 

45 """ 

46 

47 def __init__(self, input_ndx_path: str, input_top_zip_path: str, output_top_zip_path: str, 

48 properties: Optional[dict] = None, **kwargs) -> None: 

49 properties = properties or {} 

50 

51 # Call parent class constructor 

52 super().__init__(properties) 

53 self.locals_var_dict = locals().copy() 

54 

55 # Input/Output files 

56 self.io_dict = { 

57 "in": {"input_ndx_path": input_ndx_path, "input_top_zip_path": input_top_zip_path}, 

58 "out": {"output_top_zip_path": output_top_zip_path} 

59 } 

60 

61 # Properties specific for BB 

62 self.posres_names = properties.get('posres_names') 

63 self.force_constants = properties.get('force_constants', '500 500 500') 

64 self.ref_rest_chain_triplet_list = properties.get('ref_rest_chain_triplet_list') 

65 

66 # Check the properties 

67 self.check_properties(properties) 

68 self.check_arguments() 

69 

70 @launchlogger 

71 def launch(self) -> int: 

72 """Execute the :class:`Ndx2resttop <gromacs_extra.ndx2resttop.Ndx2resttop>` object.""" 

73 # Setup Biobb 

74 if self.check_restart(): 

75 return 0 

76 

77 top_file = fu.unzip_top(zip_file=self.io_dict['in'].get("input_top_zip_path", ""), out_log=self.out_log) 

78 

79 # Create group dictionary from the ndx index file 

80 # Each group will have a list with its starting and ending lines in the ndx file 

81 groups_dic: dict[str, Any] = {} 

82 current_group = '' 

83 previous_group = '' 

84 

85 # Iterate over the lines of the ndx file 

86 ndx_lines = open(self.io_dict['in'].get("input_ndx_path", "")).read().splitlines() 

87 for index, line in enumerate(ndx_lines): 

88 

89 # Found a new group in the index file 

90 if line.startswith('['): 

91 current_group = line 

92 

93 # Set the starting line of the new group 

94 groups_dic[current_group] = [index, 0] 

95 

96 # Check if there was a previous group to set its ending line 

97 if previous_group != '': 

98 groups_dic[previous_group][1] = index 

99 

100 # Update the previous group variable 

101 previous_group = current_group 

102 

103 # If we reach the last line of the file, set the ending line of the last group 

104 if index == len(ndx_lines)-1: 

105 groups_dic[current_group][1] = index 

106 

107 # Catch groups with just one line 

108 for group_name in groups_dic.keys(): 

109 if (groups_dic[group_name][0]+1) == groups_dic[group_name][1]: 

110 groups_dic[group_name][1] += 1 

111 

112 fu.log('groups_dic: '+str(groups_dic), self.out_log, self.global_log) 

113 

114 self.ref_rest_chain_triplet_list = [tuple(elem.strip(' ()').replace(' ', '').split(',')) for elem in str(self.ref_rest_chain_triplet_list).split('),')] 

115 fu.log('ref_rest_chain_triplet_list: ' + str(self.ref_rest_chain_triplet_list), self.out_log, self.global_log) 

116 

117 if self.posres_names: 

118 self.posres_names = [elem.strip() for elem in self.posres_names.split()] 

119 fu.log('posres_names: ' + str(self.posres_names), self.out_log, self.global_log) 

120 else: 

121 self.posres_names = ['CUSTOM_POSRES']*len(self.ref_rest_chain_triplet_list) 

122 fu.log('posres_names: ' + str(self.posres_names), self.out_log, self.global_log) 

123 

124 # Check if the number of posres_names matches the number of ref_rest_chain_triplet_list 

125 if len(self.posres_names) != len(self.ref_rest_chain_triplet_list): 

126 raise ValueError("If posres_names is provided, it should match the number of ref_rest_chain_triplet_list") 

127 

128 for triplet, posre_name in zip(self.ref_rest_chain_triplet_list, self.posres_names): 

129 

130 reference_group, restrain_group, chain = triplet 

131 

132 fu.log('Reference group: '+reference_group, self.out_log, self.global_log) 

133 fu.log('Restrain group: '+restrain_group, self.out_log, self.global_log) 

134 fu.log('Chain: '+chain, self.out_log, self.global_log) 

135 self.io_dict['out']["output_itp_path"] = fu.create_name(path=str(Path(top_file).parent), prefix=self.prefix, step=self.step, name=restrain_group+'_posre.itp') 

136 

137 # Mapping atoms from absolute enumeration to Chain relative enumeration 

138 fu.log('reference_group_index: start_closed:'+str(groups_dic['[ '+reference_group+' ]'][0]+1)+' stop_open: '+str(groups_dic['[ '+reference_group+' ]'][1]), self.out_log, self.global_log) 

139 reference_group_list = [int(elem) for line in ndx_lines[groups_dic['[ '+reference_group+' ]'][0]+1: groups_dic['[ '+reference_group+' ]'][1]] for elem in line.split()] 

140 fu.log('restrain_group_index: start_closed:'+str(groups_dic['[ '+restrain_group+' ]'][0]+1)+' stop_open: '+str(groups_dic['[ '+restrain_group+' ]'][1]), self.out_log, self.global_log) 

141 restrain_group_list = [int(elem) for line in ndx_lines[groups_dic['[ '+restrain_group+' ]'][0]+1: groups_dic['[ '+restrain_group+' ]'][1]] for elem in line.split()] 

142 selected_list = [reference_group_list.index(atom)+1 for atom in restrain_group_list] 

143 # Creating new ITP with restrains 

144 with open(self.io_dict['out'].get("output_itp_path", ''), 'w') as f: 

145 fu.log('Creating: '+str(f)+' and adding the selected atoms force constants', self.out_log, self.global_log) 

146 f.write('[ position_restraints ]\n') 

147 f.write('; atom type fx fy fz\n') 

148 for atom in selected_list: 

149 f.write(str(atom)+' 1 '+self.force_constants+'\n') 

150 

151 multi_chain = False 

152 # For multi-chain topologies 

153 # Including new ITP in the corresponding ITP-chain file 

154 for file_dir in Path(top_file).parent.iterdir(): 

155 if "posre" not in file_dir.name and not file_dir.name.endswith("_pr.itp"): 

156 if fnmatch.fnmatch(str(file_dir), "*_chain_"+chain+".itp"): 

157 multi_chain = True 

158 with open(str(file_dir), 'a') as f: 

159 fu.log('Opening: '+str(f)+' and adding the ifdef include statement', self.out_log, self.global_log) 

160 f.write('\n') 

161 f.write('; Include Position restraint file\n') 

162 f.write('#ifdef '+str(posre_name)+'\n') 

163 f.write('#include "'+str(Path(self.io_dict['out'].get("output_itp_path", "")).name)+'"\n') 

164 f.write('#endif\n') 

165 f.write('\n') 

166 

167 # For single-chain topologies 

168 # Including new ITP in the TOP file 

169 if not multi_chain: 

170 

171 # Read all lines of the top file 

172 with open(top_file, 'r') as f: 

173 lines = f.readlines() 

174 

175 main_chain = False 

176 index = 0 

177 

178 # Find the index of the line where the custom position restraints are going to be included 

179 for line in lines: 

180 

181 # Find the moleculetype directive of the main chain 

182 if line.startswith('Protein_chain_'+chain): 

183 main_chain = True 

184 index = lines.index(line) + 3 

185 

186 # Find the end of the moleculetype directive of the main chain 

187 if main_chain: 

188 if line.startswith('[system]') or line.startswith('[molecules]') or line.startswith('#include ') or line.startswith('#ifdef POSRES'): 

189 index = lines.index(line) - 1 

190 break 

191 

192 if index == 0: 

193 raise ValueError(f"Protein_chain_{chain} not found in the topology file") 

194 

195 # Include the custom position restraints in the top file 

196 lines.insert(index, '\n') 

197 lines.insert(index + 1, '; Include Position restraint file\n') 

198 lines.insert(index + 2, '#ifdef '+str(posre_name)+'\n') 

199 lines.insert(index + 3, '#include "'+str(Path(self.io_dict['out'].get("output_itp_path", "")).name)+'"\n') 

200 lines.insert(index + 4, '#endif\n') 

201 

202 # Write the new top file 

203 with open(top_file, 'w') as f: 

204 f.writelines(lines) 

205 

206 # zip topology 

207 fu.zip_top(zip_file=self.io_dict['out'].get("output_top_zip_path", ""), top_file=top_file, out_log=self.out_log, remove_original_files=self.remove_tmp) 

208 

209 # Remove temporal files 

210 self.remove_tmp_files() 

211 

212 self.check_arguments(output_files_created=True, raise_exception=False) 

213 return 0 

214 

215 

216def ndx2resttop(input_ndx_path: str, input_top_zip_path: str, output_top_zip_path: str, 

217 properties: Optional[dict] = None, **kwargs) -> int: 

218 """Create :class:`Ndx2resttop <gromacs_extra.ndx2resttop.Ndx2resttop>` class and 

219 execute the :meth:`launch() <gromacs_extra.ndx2resttop.Ndx2resttop.launch>` method.""" 

220 return Ndx2resttop(**dict(locals())).launch() 

221 

222 

223ndx2resttop.__doc__ = Ndx2resttop.__doc__ 

224main = Ndx2resttop.get_main(ndx2resttop, "Generate a restrained topology from an index NDX file.") 

225 

226 

227if __name__ == '__main__': 

228 main()