Coverage for /builds/ase/ase/ase/cli/diff.py: 85.56%

90 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3# Note: 

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

5# import time during CLI execution 

6import sys 

7 

8from ase.cli.main import CLIError 

9 

10template_help = """ 

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

12expects the comma separated list of the fields to include 

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

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

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

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

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

18ranked to get ascending ranks). 

19 

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

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

22 

23possible fields: 

24 

25* i: index 

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

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

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

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

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

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

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

33* an: atomic number 

34* el: atomic element 

35* t: atom tag 

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

37 

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

39 

40 

41class CLICommand: 

42 """Print differences between atoms/calculations. 

43 

44 Supports taking differences between different calculation runs of 

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

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

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

48 and image 2 are also provided. 

49 

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

51 customization requires changing the input arguments to the Table 

52 initialization and/or editing the templates file. 

53 """ 

54 

55 @staticmethod 

56 def add_arguments(parser): 

57 add = parser.add_argument 

58 add('file', 

59 help="""Possible file entries are 

60 

61 * 2 non-trajectory files: difference between them 

62 * 1 trajectory file: difference between consecutive images 

63 * 2 trajectory files: difference between corresponding image numbers 

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

65 

66 Note deltas are defined as 2 - 1. 

67 

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

69 """, 

70 nargs='+') 

71 add('-r', 

72 '--rank-order', 

73 metavar='FIELD', 

74 nargs='?', 

75 const='d', 

76 type=str, 

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

78fields. 

79 

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

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

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

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

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

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

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

87 ", useful if rank ordering") 

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

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

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

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

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

93 help="""Specify the summary functions. 

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

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

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

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

98 help="output table in csv format") 

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

100 default=2, type=int, 

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

102 

103 @staticmethod 

104 def run(args, parser): 

105 import io 

106 

107 if args.template_help: 

108 print(template_help) 

109 return 

110 

111 encoding = 'utf-8' 

112 

113 if args.log_file is None: 

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

115 else: 

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

117 

118 with out: 

119 CLICommand.diff(args, out) 

120 

121 @staticmethod 

122 def diff(args, out): 

123 from ase.cli.template import ( 

124 Table, 

125 TableFormat, 

126 energy_delta, 

127 field_specs_on_conditions, 

128 rmsd, 

129 slice_split, 

130 summary_functions_on_conditions, 

131 ) 

132 from ase.io import read 

133 

134 if args.template is None: 

135 field_specs = field_specs_on_conditions( 

136 args.calculator_outputs, args.rank_order) 

137 else: 

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

139 if not args.calculator_outputs: 

140 for field_spec in field_specs: 

141 if 'f' in field_spec: 

142 raise CLIError( 

143 "field requiring calculation outputs " 

144 "without --calculator-outputs") 

145 

146 if args.summary_functions is None: 

147 summary_functions = summary_functions_on_conditions( 

148 args.calculator_outputs) 

149 else: 

150 summary_functions_dct = { 

151 'rmsd': rmsd, 

152 'dE': energy_delta} 

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

154 if not args.calculator_outputs: 

155 for sf in summary_functions: 

156 if sf == 'dE': 

157 raise CLIError( 

158 "summary function requiring calculation outputs " 

159 "without --calculator-outputs") 

160 summary_functions = [summary_functions_dct[i] 

161 for i in summary_functions] 

162 

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

164 file1 = args.file[0] 

165 actual_filename, index = slice_split(file1) 

166 atoms1 = read(actual_filename, index) 

167 natoms1 = len(atoms1) 

168 

169 if have_two_files: 

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

171 atoms2 = atoms1 

172 

173 def header_fmt(c): 

174 return f'image # {c}' 

175 else: 

176 file2 = args.file[1] 

177 actual_filename, index = slice_split(file2) 

178 atoms2 = read(actual_filename, index) 

179 natoms2 = len(atoms2) 

180 

181 same_length = natoms1 == natoms2 

182 one_l_one = natoms1 == 1 or natoms2 == 1 

183 

184 if not same_length and not one_l_one: 

185 raise CLIError( 

186 "Trajectory files are not the same length " 

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

188 natoms1, natoms2)) 

189 elif not same_length and one_l_one: 

190 print( 

191 "One file contains one image " 

192 "and the other multiple images,\n" 

193 "assuming you want to compare all images " 

194 "with one reference image") 

195 if natoms1 > natoms2: 

196 atoms2 = natoms1 * atoms2 

197 else: 

198 atoms1 = natoms2 * atoms1 

199 

200 def header_fmt(c): 

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

202 else: 

203 def header_fmt(c): 

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

205 else: 

206 atoms2 = atoms1.copy() 

207 atoms1 = atoms1[:-1] 

208 atoms2 = atoms2[1:] 

209 natoms2 = natoms1 = natoms1 - 1 

210 

211 def header_fmt(c): 

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

213 

214 natoms = natoms1 # = natoms2 

215 

216 output = '' 

217 tableformat = TableFormat(precision=args.precision, 

218 columnwidth=7 + args.precision) 

219 

220 table = Table( 

221 field_specs, 

222 max_lines=args.max_lines, 

223 tableformat=tableformat, 

224 summary_functions=summary_functions) 

225 

226 for counter in range(natoms): 

227 table.title = header_fmt(counter) 

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

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

230 print(output, file=out)