File: mrcfile.py

package info (click to toggle)
python-mrcfile 1.5.4-2
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 63,824 kB
  • sloc: python: 3,871; sh: 28; makefile: 16
file content (238 lines) | stat: -rw-r--r-- 10,272 bytes parent folder | download
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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# Copyright (c) 2016, Science and Technology Facilities Council
# This software is distributed under a BSD licence. See LICENSE.txt.
"""
mrcfile
-------

Module which exports the :class:`MrcFile` class.

Classes:
    :class:`MrcFile`: An object which represents an MRC file.

"""

# Import Python 3 features for future-proofing
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)


import os
import warnings

from .mrcinterpreter import MrcInterpreter


class MrcFile(MrcInterpreter):
    
    """An object which represents an MRC file.
    
    The header and data are handled as numpy arrays - see
    :class:`~mrcfile.mrcobject.MrcObject` for details.
        
    :class:`MrcFile` supports a permissive read mode for attempting to open
    corrupt or invalid files. See
    :class:`mrcfile.mrcinterpreter.MrcInterpreter` or the :doc:`usage guide
    <../usage_guide>` for more information.
    
    Usage:
        To create a new MrcFile object, pass a file name and optional mode. To
        ensure the file is written to disk and closed correctly, it's best to
        use the :keyword:`with` statement:
        
        >>> with MrcFile('tmp.mrc', 'w+') as mrc:
        ...     mrc.set_data(np.zeros((10, 10), dtype=np.int8))
        
        In mode ``r`` or ``r+``, the named file is opened from disk and read.
        In mode ``w+`` a new empty file is created and will be written to disk
        at the end of the :keyword:`with` block (or when
        :meth:`~.MrcInterpreter.flush` or :meth:`close` is called).
    
    """
    
    def __init__(self, name, mode='r', overwrite=False, permissive=False,
                 header_only=False, **kwargs):
        """Initialise a new :class:`MrcFile` object.
        
        The given file name is opened in the given mode. For mode ``r`` or
        ``r+`` the header, extended header and data are read from the file. For
        mode ``w+`` a new file is created with a default header and empty
        extended header and data arrays.
        
        Args:
            name: The file name to open, as a string or pathlib Path.
            mode: The file mode to use. This should be one of the following:
                ``r`` for read-only, ``r+`` for read and write, or ``w+`` for a
                new empty file. The default is ``r``.
            overwrite: Flag to force overwriting of an existing file if the
                mode is ``w+``. If :data:`False` and a file of the same name
                already exists, the file is not overwritten and an exception is
                raised. The default is :data:`False`.
            permissive: Read the file in permissive mode. (See
                :class:`mrcfile.mrcinterpreter.MrcInterpreter` for details.)
                The default is :data:`False`.
            header_only: Only read the header (and extended header) from the
                file. The default is :data:`False`.
        
        Raises:
            :exc:`ValueError`: If the mode is not one of ``r``, ``r+`` or
                ``w+``.
            :exc:`ValueError`: If the file is not a valid MRC file and
                ``permissive`` is :data:`False`.
            :exc:`ValueError`: If the mode is ``w+``, the file already exists
                and overwrite is :data:`False`.
            :exc:`OSError`: If the mode is ``r`` or ``r+`` and the file does
                not exist.
        
        Warns:
            RuntimeWarning: If the file appears to be a valid MRC file but the
                data block is longer than expected from the dimensions in the
                header.
            RuntimeWarning: If the file is not a valid MRC file and
                ``permissive`` is :data:`True`.
            RuntimeWarning: If the header's ``exttyp`` field is set to a known
                value but the extended header's size is not a multiple of the
                number of bytes in the corresponding dtype.
        """
        super(MrcFile, self).__init__(permissive=permissive, **kwargs)
        
        if mode not in ['r', 'r+', 'w+']:
            raise ValueError("Mode '{0}' not supported".format(mode))

        name = str(name)  # in case name is a pathlib Path
        if ('w' in mode and os.path.exists(name) and not overwrite):
            raise ValueError("File '{0}' already exists; set overwrite=True "
                             "to overwrite it".format(name))
        
        self._mode = mode
        self._read_only = (self._mode == 'r')
        
        self._open_file(name)
        
        try:
            if 'w' in mode:
                self._create_default_attributes()
            else:
                self._read(header_only)
        except Exception:
            self._close_file()
            raise
    
    def __repr__(self):
        return "MrcFile('{0}', mode='{1}')".format(self._iostream.name,
                                                   self._mode)
    
    def _open_file(self, name):
        """Open a file object to use as the I/O stream."""
        self._iostream = open(name, self._mode + 'b')
    
    def _read(self, header_only=False):
        """Override _read() to move back to start of file first."""
        self._iostream.seek(0)
        super(MrcFile, self)._read(header_only)

    def _read_data(self):
        """Override _read_data() to check file size matches data block size."""
        file_size = self._get_file_size()
        # Need to use self.header.nsymbt rather than self.extended_header.nbytes because
        # self.extended_header might be None in permissive read mode. Need to convert to
        # Python int (rather than numpy int32) to avoid possible overflow.
        header_size = self.header.nbytes + int(self.header.nsymbt)
        remaining_file_size = file_size - header_size

        super(MrcFile, self)._read_data(max_bytes=remaining_file_size)

        # Check if the file is the expected size.
        if self.data is not None:
            data_size = self.data.nbytes
            if data_size < remaining_file_size:
                msg = ("MRC file is {0} bytes larger than expected"
                       .format(remaining_file_size - data_size))
                warnings.warn(msg, RuntimeWarning)
    
    def _get_file_size(self):
        """Return the size of the underlying file object, in bytes."""
        pos = self._iostream.tell()
        self._iostream.seek(0, os.SEEK_END)
        size = self._iostream.tell()
        self._iostream.seek(pos, os.SEEK_SET)
        return size
    
    def close(self):
        """Flush any changes to disk and close the file.
        
        This override calls :meth:`.MrcInterpreter.close` to ensure the stream
        is flushed and closed, then closes the file object.
        """
        super(MrcFile, self).close()
        self._close_file()
    
    def _close_file(self):
        """Close the file object."""
        self._iostream.close()
    
    def validate(self, print_file=None):
        """Validate this MRC file.
        
        The tests are:
        
        #. MRC format ID string: The ``map`` field in the header should
           contain "MAP ".
        #. Machine stamp: The machine stamp should contain one of
           ``0x44 0x44 0x00 0x00``, ``0x44 0x41 0x00 0x00`` or
           ``0x11 0x11 0x00 0x00``.
        #. MRC mode: the ``mode`` field should be one of the supported mode
           numbers: 0, 1, 2, 4, 6 or 12. (Note that MRC modes 3 and 101 are
           also valid according to the MRC 2014 specification but are not
           supported by mrcfile.)
        #. Map and cell dimensions: The header fields ``nx``, ``ny``, ``nz``,
           ``mx``, ``my``, ``mz``, ``cella.x``, ``cella.y`` and ``cella.z``
           must all be positive numbers.
        #. Axis mapping: Header fields ``mapc``, ``mapr`` and ``maps`` must
           contain the values 1, 2, and 3 (in any order).
        #. Volume stack dimensions: If the spacegroup is in the range 401--630,
           representing a volume stack, the ``nz`` field should be exactly
           divisible by ``mz`` to represent the number of volumes in the stack.
        #. Header labels: The ``nlabl`` field should be set to indicate the
           number of labels in use, and the labels in use should appear first
           in the label array.
        #. MRC format version: The ``nversion`` field should be 20140 or 20141
           for compliance with the MRC2014 standard.
        #. Extended header type: If an extended header is present, the
           ``exttyp`` field should be set to indicate the type of extended
           header.
        #. Data statistics: The statistics in the header should be correct for
           the actual data in the file, or marked as undetermined.
        #. File size: The size of the file on disk should match the expected
           size calculated from the MRC header.
        
        Args:
            print_file: The output text stream to use for printing messages
                about the validation. This is passed directly to the ``file``
                argument of Python's :func:`print` function. The default is
                :data:`None`, which means output will be printed to
                :data:`sys.stdout`.
        
        Returns:
            :data:`True` if the file is valid, or :data:`False` if the file
            does not meet the MRC format specification in any way.
        """
        valid = super(MrcFile, self).validate(print_file=print_file)
        
        if self.data is not None:
            # Check file size
            file_size = self._get_file_size()
            mrc_size = (self.header.nbytes
                        + self.extended_header.nbytes
                        + self.data.nbytes)
            if (file_size != mrc_size):
                print("File is larger than expected. Actual size: {0} bytes; "
                      "expected size: {1} bytes (calculated from header)"
                      .format(file_size, mrc_size),
                      file=print_file)
                valid = False
        else:
            print("Data block could not be read - file size not checked",
                  file=print_file)
            valid = False
        
        return valid