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
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
1# fmt: off
3# Note:
4# Try to avoid module level import statements here to reduce
5# import time during CLI execution
6import sys
8from ase.cli.main import CLIError
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).
20* example: ase diff start.cif stop.cif --template
21* i:0:1,el,dx,dy,dz,d,rd
23possible fields:
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
38It is possible to change formatters in the template file."""
41class CLICommand:
42 """Print differences between atoms/calculations.
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.
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 """
55 @staticmethod
56 def add_arguments(parser):
57 add = parser.add_argument
58 add('file',
59 help="""Possible file entries are
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
66 Note deltas are defined as 2 - 1.
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.
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")
103 @staticmethod
104 def run(args, parser):
105 import io
107 if args.template_help:
108 print(template_help)
109 return
111 encoding = 'utf-8'
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)
118 with out:
119 CLICommand.diff(args, out)
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
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")
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]
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)
169 if have_two_files:
170 if args.file[1] == '-':
171 atoms2 = atoms1
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)
181 same_length = natoms1 == natoms2
182 one_l_one = natoms1 == 1 or natoms2 == 1
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
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
211 def header_fmt(c):
212 return f'images {c + 1}-{c}'
214 natoms = natoms1 # = natoms2
216 output = ''
217 tableformat = TableFormat(precision=args.precision,
218 columnwidth=7 + args.precision)
220 table = Table(
221 field_specs,
222 max_lines=args.max_lines,
223 tableformat=tableformat,
224 summary_functions=summary_functions)
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)