File: diff.py

package info (click to toggle)
python-ase 3.22.1-3
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 14,344 kB
  • sloc: python: 126,379; xml: 946; makefile: 111; javascript: 47
file content (222 lines) | stat: -rw-r--r-- 8,376 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
import sys
import io
from ase.io import read
from ase.cli.main import CLIError

template_help = """
Without argument, looks for ~/.ase/template.py.  Otherwise,
expects the comma separated list of the fields to include
in their left-to-right order.  Optionally, specify the
lexicographical sort hierarchy (0 is outermost sort) and if the
sort should be ascending or descending (1 or -1).  By default,
sorting is descending, which makes sense for most things except
index (and rank, but one can just sort by the thing which is
ranked to get ascending ranks).

* example: ase diff start.cif stop.cif --template
* i:0:1,el,dx,dy,dz,d,rd

possible fields:

*    i: index
*    dx,dy,dz,d: displacement/displacement components
*    dfx,dfy,dfz,df: difference force/force components
*    afx,afy,afz,af: average force/force components
*    p1x,p1y,p1z,p: first image positions/position components
*    p2x,p2y,p2z,p: second image positions/position components
*    f1x,f1y,f1z,f: first image forces/force components
*    f2x,f2y,f2z,f: second image forces/force components
*    an: atomic number
*    el: atomic element
*    t: atom tag
*    r<col>: the rank of that atom with respect to the column

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


class CLICommand:
    """Print differences between atoms/calculations.

    Supports taking differences between different calculation runs of
    the same system as well as neighboring geometric images for one
    calculation run of a system. As part of a difference table or as a
    standalone display table, fields for non-difference quantities of image 1
    and image 2 are also provided.

    See the --template-help for the formatting exposed in the CLI.  More
    customization requires changing the input arguments to the Table
    initialization and/or editing the templates file.
    """

    @staticmethod
    def add_arguments(parser):
        add = parser.add_argument
        add('file',
            help="""Possible file entries are

    * 2 non-trajectory files: difference between them
    * 1 trajectory file: difference between consecutive images
    * 2 trajectory files: difference between corresponding image numbers
    * 1 trajectory file followed by hyphen-minus (ASCII 45): for display

    Note deltas are defined as 2 - 1.
    
    Use [FILE]@[SLICE] to select images.
                    """,
            nargs='+')
        add('-r',
            '--rank-order',
            metavar='FIELD',
            nargs='?',
            const='d',
            type=str,
            help="""Order atoms by rank, see --template-help for possible
fields.

The default value, when specified, is d.  When not
specified, ordering is the same as that provided by the
generator.  For hierarchical sorting, see template.""")
        add('-c', '--calculator-outputs', action="store_true",
            help="display calculator outputs of forces and energy")
        add('--max-lines', metavar='N', type=int,
            help="show only so many lines (atoms) in each table "
            ", useful if rank ordering")
        add('-t', '--template', metavar='TEMPLATE', nargs='?', const='rc',
            help="""See --template-help for the help on this option.""")
        add('--template-help', help="""Prints the help for the template file.
                Usage `ase diff - --template-help`""", action="store_true")
        add('-s', '--summary-functions', metavar='SUMFUNCS', nargs='?',
            help="""Specify the summary functions.
            Possible values are `rmsd` and `dE`.
            Comma separate more than one summary function.""")
        add('--log-file', metavar='LOGFILE', help="print table to file")
        add('--as-csv', action="store_true",
            help="output table in csv format")
        add('--precision', metavar='PREC',
            default=2, type=int,
            help="precision used in both display and sorting")

    @staticmethod
    def run(args, parser):
        if args.template_help:
            print(template_help)
            return

        encoding = 'utf-8'

        if args.log_file is None:
            out = io.TextIOWrapper(sys.stdout.buffer, encoding=encoding)
        else:
            out = open(args.log_file, 'w', encoding=encoding)

        with out:
            CLICommand.diff(args, out)

    @staticmethod
    def diff(args, out):
        from ase.cli.template import (
            Table,
            TableFormat,
            slice_split,
            field_specs_on_conditions,
            summary_functions_on_conditions,
            rmsd,
            energy_delta)

        if args.template is None:
            field_specs = field_specs_on_conditions(
                args.calculator_outputs, args.rank_order)
        else:
            field_specs = args.template.split(',')
            if not args.calculator_outputs:
                for field_spec in field_specs:
                    if 'f' in field_spec:
                        raise CLIError(
                            "field requiring calculation outputs "
                            "without --calculator-outputs")

        if args.summary_functions is None:
            summary_functions = summary_functions_on_conditions(
                args.calculator_outputs)
        else:
            summary_functions_dct = {
                'rmsd': rmsd,
                'dE': energy_delta}
            summary_functions = args.summary_functions.split(',')
            if not args.calculator_outputs:
                for sf in summary_functions:
                    if sf == 'dE':
                        raise CLIError(
                            "summary function requiring calculation outputs "
                            "without --calculator-outputs")
            summary_functions = [summary_functions_dct[i]
                                 for i in summary_functions]

        have_two_files = len(args.file) == 2
        file1 = args.file[0]
        actual_filename, index = slice_split(file1)
        atoms1 = read(actual_filename, index)
        natoms1 = len(atoms1)

        if have_two_files:
            if args.file[1] == '-':
                atoms2 = atoms1

                def header_fmt(c):
                    return 'image # {}'.format(c)
            else:
                file2 = args.file[1]
                actual_filename, index = slice_split(file2)
                atoms2 = read(actual_filename, index)
                natoms2 = len(atoms2)

                same_length = natoms1 == natoms2
                one_l_one = natoms1 == 1 or natoms2 == 1

                if not same_length and not one_l_one:
                    raise CLIError(
                        "Trajectory files are not the same length "
                        "and both > 1\n{}!={}".format(
                            natoms1, natoms2))
                elif not same_length and one_l_one:
                    print(
                        "One file contains one image "
                        "and the other multiple images,\n"
                        "assuming you want to compare all images "
                        "with one reference image")
                    if natoms1 > natoms2:
                        atoms2 = natoms1 * atoms2
                    else:
                        atoms1 = natoms2 * atoms1

                    def header_fmt(c):
                        return 'sys-ref image # {}'.format(c)
                else:
                    def header_fmt(c):
                        return 'sys2-sys1 image # {}'.format(c)
        else:
            atoms2 = atoms1.copy()
            atoms1 = atoms1[:-1]
            atoms2 = atoms2[1:]
            natoms2 = natoms1 = natoms1 - 1

            def header_fmt(c):
                return 'images {}-{}'.format(c + 1, c)

        natoms = natoms1  # = natoms2

        output = ''
        tableformat = TableFormat(precision=args.precision,
                                  columnwidth=7 + args.precision)

        table = Table(
            field_specs,
            max_lines=args.max_lines,
            tableformat=tableformat,
            summary_functions=summary_functions)

        for counter in range(natoms):
            table.title = header_fmt(counter)
            output += table.make(atoms1[counter],
                                 atoms2[counter], csv=args.as_csv) + '\n'
        print(output, file=out)