Coverage for /builds/kinetik161/ase/ase/cli/diff.py: 85.56%
90 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
1# Note:
2# Try to avoid module level import statements here to reduce
3# import time during CLI execution
4import sys
6from ase.cli.main import CLIError
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).
18* example: ase diff start.cif stop.cif --template
19* i:0:1,el,dx,dy,dz,d,rd
21possible fields:
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
36It is possible to change formatters in the template file."""
39class CLICommand:
40 """Print differences between atoms/calculations.
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.
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 """
53 @staticmethod
54 def add_arguments(parser):
55 add = parser.add_argument
56 add('file',
57 help="""Possible file entries are
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
64 Note deltas are defined as 2 - 1.
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.
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")
101 @staticmethod
102 def run(args, parser):
103 import io
105 if args.template_help:
106 print(template_help)
107 return
109 encoding = 'utf-8'
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)
116 with out:
117 CLICommand.diff(args, out)
119 @staticmethod
120 def diff(args, out):
121 from ase.cli.template import (Table, TableFormat, energy_delta,
122 field_specs_on_conditions, rmsd,
123 slice_split,
124 summary_functions_on_conditions)
125 from ase.io import read
127 if args.template is None:
128 field_specs = field_specs_on_conditions(
129 args.calculator_outputs, args.rank_order)
130 else:
131 field_specs = args.template.split(',')
132 if not args.calculator_outputs:
133 for field_spec in field_specs:
134 if 'f' in field_spec:
135 raise CLIError(
136 "field requiring calculation outputs "
137 "without --calculator-outputs")
139 if args.summary_functions is None:
140 summary_functions = summary_functions_on_conditions(
141 args.calculator_outputs)
142 else:
143 summary_functions_dct = {
144 'rmsd': rmsd,
145 'dE': energy_delta}
146 summary_functions = args.summary_functions.split(',')
147 if not args.calculator_outputs:
148 for sf in summary_functions:
149 if sf == 'dE':
150 raise CLIError(
151 "summary function requiring calculation outputs "
152 "without --calculator-outputs")
153 summary_functions = [summary_functions_dct[i]
154 for i in summary_functions]
156 have_two_files = len(args.file) == 2
157 file1 = args.file[0]
158 actual_filename, index = slice_split(file1)
159 atoms1 = read(actual_filename, index)
160 natoms1 = len(atoms1)
162 if have_two_files:
163 if args.file[1] == '-':
164 atoms2 = atoms1
166 def header_fmt(c):
167 return f'image # {c}'
168 else:
169 file2 = args.file[1]
170 actual_filename, index = slice_split(file2)
171 atoms2 = read(actual_filename, index)
172 natoms2 = len(atoms2)
174 same_length = natoms1 == natoms2
175 one_l_one = natoms1 == 1 or natoms2 == 1
177 if not same_length and not one_l_one:
178 raise CLIError(
179 "Trajectory files are not the same length "
180 "and both > 1\n{}!={}".format(
181 natoms1, natoms2))
182 elif not same_length and one_l_one:
183 print(
184 "One file contains one image "
185 "and the other multiple images,\n"
186 "assuming you want to compare all images "
187 "with one reference image")
188 if natoms1 > natoms2:
189 atoms2 = natoms1 * atoms2
190 else:
191 atoms1 = natoms2 * atoms1
193 def header_fmt(c):
194 return f'sys-ref image # {c}'
195 else:
196 def header_fmt(c):
197 return f'sys2-sys1 image # {c}'
198 else:
199 atoms2 = atoms1.copy()
200 atoms1 = atoms1[:-1]
201 atoms2 = atoms2[1:]
202 natoms2 = natoms1 = natoms1 - 1
204 def header_fmt(c):
205 return f'images {c + 1}-{c}'
207 natoms = natoms1 # = natoms2
209 output = ''
210 tableformat = TableFormat(precision=args.precision,
211 columnwidth=7 + args.precision)
213 table = Table(
214 field_specs,
215 max_lines=args.max_lines,
216 tableformat=tableformat,
217 summary_functions=summary_functions)
219 for counter in range(natoms):
220 table.title = header_fmt(counter)
221 output += table.make(atoms1[counter],
222 atoms2[counter], csv=args.as_csv) + '\n'
223 print(output, file=out)