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
« 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
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 (
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
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")
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]
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)
167 if have_two_files:
168 if args.file[1] == '-':
169 atoms2 = atoms1
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)
179 same_length = natoms1 == natoms2
180 one_l_one = natoms1 == 1 or natoms2 == 1
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
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
209 def header_fmt(c):
210 return f'images {c + 1}-{c}'
212 natoms = natoms1 # = natoms2
214 output = ''
215 tableformat = TableFormat(precision=args.precision,
216 columnwidth=7 + args.precision)
218 table = Table(
219 field_specs,
220 max_lines=args.max_lines,
221 tableformat=tableformat,
222 summary_functions=summary_functions)
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)