Coverage for /builds/debichem-team/python-ase/ase/cli/diff.py: 85.56%

90 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-03-06 04:00 +0000

1# Note: 

2# Try to avoid module level import statements here to reduce 

3# import time during CLI execution 

4import sys 

5 

6from ase.cli.main import CLIError 

7 

8template_help = """ 

9Without argument, looks for ~/.ase/template.py. Otherwise, 

10expects the comma separated list of the fields to include 

11in their left-to-right order. Optionally, specify the 

12lexicographical sort hierarchy (0 is outermost sort) and if the 

13sort should be ascending or descending (1 or -1). By default, 

14sorting is descending, which makes sense for most things except 

15index (and rank, but one can just sort by the thing which is 

16ranked to get ascending ranks). 

17 

18* example: ase diff start.cif stop.cif --template 

19* i:0:1,el,dx,dy,dz,d,rd 

20 

21possible fields: 

22 

23* i: index 

24* dx,dy,dz,d: displacement/displacement components 

25* dfx,dfy,dfz,df: difference force/force components 

26* afx,afy,afz,af: average force/force components 

27* p1x,p1y,p1z,p: first image positions/position components 

28* p2x,p2y,p2z,p: second image positions/position components 

29* f1x,f1y,f1z,f: first image forces/force components 

30* f2x,f2y,f2z,f: second image forces/force components 

31* an: atomic number 

32* el: atomic element 

33* t: atom tag 

34* r<col>: the rank of that atom with respect to the column 

35 

36It is possible to change formatters in the template file.""" 

37 

38 

39class CLICommand: 

40 """Print differences between atoms/calculations. 

41 

42 Supports taking differences between different calculation runs of 

43 the same system as well as neighboring geometric images for one 

44 calculation run of a system. As part of a difference table or as a 

45 standalone display table, fields for non-difference quantities of image 1 

46 and image 2 are also provided. 

47 

48 See the --template-help for the formatting exposed in the CLI. More 

49 customization requires changing the input arguments to the Table 

50 initialization and/or editing the templates file. 

51 """ 

52 

53 @staticmethod 

54 def add_arguments(parser): 

55 add = parser.add_argument 

56 add('file', 

57 help="""Possible file entries are 

58 

59 * 2 non-trajectory files: difference between them 

60 * 1 trajectory file: difference between consecutive images 

61 * 2 trajectory files: difference between corresponding image numbers 

62 * 1 trajectory file followed by hyphen-minus (ASCII 45): for display 

63 

64 Note deltas are defined as 2 - 1. 

65 

66 Use [FILE]@[SLICE] to select images. 

67 """, 

68 nargs='+') 

69 add('-r', 

70 '--rank-order', 

71 metavar='FIELD', 

72 nargs='?', 

73 const='d', 

74 type=str, 

75 help="""Order atoms by rank, see --template-help for possible 

76fields. 

77 

78The default value, when specified, is d. When not 

79specified, ordering is the same as that provided by the 

80generator. For hierarchical sorting, see template.""") 

81 add('-c', '--calculator-outputs', action="store_true", 

82 help="display calculator outputs of forces and energy") 

83 add('--max-lines', metavar='N', type=int, 

84 help="show only so many lines (atoms) in each table " 

85 ", useful if rank ordering") 

86 add('-t', '--template', metavar='TEMPLATE', nargs='?', const='rc', 

87 help="""See --template-help for the help on this option.""") 

88 add('--template-help', help="""Prints the help for the template file. 

89 Usage `ase diff - --template-help`""", action="store_true") 

90 add('-s', '--summary-functions', metavar='SUMFUNCS', nargs='?', 

91 help="""Specify the summary functions. 

92 Possible values are `rmsd` and `dE`. 

93 Comma separate more than one summary function.""") 

94 add('--log-file', metavar='LOGFILE', help="print table to file") 

95 add('--as-csv', action="store_true", 

96 help="output table in csv format") 

97 add('--precision', metavar='PREC', 

98 default=2, type=int, 

99 help="precision used in both display and sorting") 

100 

101 @staticmethod 

102 def run(args, parser): 

103 import io 

104 

105 if args.template_help: 

106 print(template_help) 

107 return 

108 

109 encoding = 'utf-8' 

110 

111 if args.log_file is None: 

112 out = io.TextIOWrapper(sys.stdout.buffer, encoding=encoding) 

113 else: 

114 out = open(args.log_file, 'w', encoding=encoding) 

115 

116 with out: 

117 CLICommand.diff(args, out) 

118 

119 @staticmethod 

120 def diff(args, out): 

121 from ase.cli.template import ( 

122 Table, 

123 TableFormat, 

124 energy_delta, 

125 field_specs_on_conditions, 

126 rmsd, 

127 slice_split, 

128 summary_functions_on_conditions, 

129 ) 

130 from ase.io import read 

131 

132 if args.template is None: 

133 field_specs = field_specs_on_conditions( 

134 args.calculator_outputs, args.rank_order) 

135 else: 

136 field_specs = args.template.split(',') 

137 if not args.calculator_outputs: 

138 for field_spec in field_specs: 

139 if 'f' in field_spec: 

140 raise CLIError( 

141 "field requiring calculation outputs " 

142 "without --calculator-outputs") 

143 

144 if args.summary_functions is None: 

145 summary_functions = summary_functions_on_conditions( 

146 args.calculator_outputs) 

147 else: 

148 summary_functions_dct = { 

149 'rmsd': rmsd, 

150 'dE': energy_delta} 

151 summary_functions = args.summary_functions.split(',') 

152 if not args.calculator_outputs: 

153 for sf in summary_functions: 

154 if sf == 'dE': 

155 raise CLIError( 

156 "summary function requiring calculation outputs " 

157 "without --calculator-outputs") 

158 summary_functions = [summary_functions_dct[i] 

159 for i in summary_functions] 

160 

161 have_two_files = len(args.file) == 2 

162 file1 = args.file[0] 

163 actual_filename, index = slice_split(file1) 

164 atoms1 = read(actual_filename, index) 

165 natoms1 = len(atoms1) 

166 

167 if have_two_files: 

168 if args.file[1] == '-': 

169 atoms2 = atoms1 

170 

171 def header_fmt(c): 

172 return f'image # {c}' 

173 else: 

174 file2 = args.file[1] 

175 actual_filename, index = slice_split(file2) 

176 atoms2 = read(actual_filename, index) 

177 natoms2 = len(atoms2) 

178 

179 same_length = natoms1 == natoms2 

180 one_l_one = natoms1 == 1 or natoms2 == 1 

181 

182 if not same_length and not one_l_one: 

183 raise CLIError( 

184 "Trajectory files are not the same length " 

185 "and both > 1\n{}!={}".format( 

186 natoms1, natoms2)) 

187 elif not same_length and one_l_one: 

188 print( 

189 "One file contains one image " 

190 "and the other multiple images,\n" 

191 "assuming you want to compare all images " 

192 "with one reference image") 

193 if natoms1 > natoms2: 

194 atoms2 = natoms1 * atoms2 

195 else: 

196 atoms1 = natoms2 * atoms1 

197 

198 def header_fmt(c): 

199 return f'sys-ref image # {c}' 

200 else: 

201 def header_fmt(c): 

202 return f'sys2-sys1 image # {c}' 

203 else: 

204 atoms2 = atoms1.copy() 

205 atoms1 = atoms1[:-1] 

206 atoms2 = atoms2[1:] 

207 natoms2 = natoms1 = natoms1 - 1 

208 

209 def header_fmt(c): 

210 return f'images {c + 1}-{c}' 

211 

212 natoms = natoms1 # = natoms2 

213 

214 output = '' 

215 tableformat = TableFormat(precision=args.precision, 

216 columnwidth=7 + args.precision) 

217 

218 table = Table( 

219 field_specs, 

220 max_lines=args.max_lines, 

221 tableformat=tableformat, 

222 summary_functions=summary_functions) 

223 

224 for counter in range(natoms): 

225 table.title = header_fmt(counter) 

226 output += table.make(atoms1[counter], 

227 atoms2[counter], csv=args.as_csv) + '\n' 

228 print(output, file=out)