summaryrefslogtreecommitdiff
path: root/src/silx/io/specfilewrapper.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/io/specfilewrapper.py')
-rw-r--r--src/silx/io/specfilewrapper.py371
1 files changed, 371 insertions, 0 deletions
diff --git a/src/silx/io/specfilewrapper.py b/src/silx/io/specfilewrapper.py
new file mode 100644
index 0000000..01e185c
--- /dev/null
+++ b/src/silx/io/specfilewrapper.py
@@ -0,0 +1,371 @@
+# coding: utf-8
+# /*#########################################################################
+# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""This module provides a backward compatibility layer with the legacy
+specfile wrapper.
+
+If you are starting a new project, please consider using :mod:`silx.io.specfile`
+instead of this module.
+
+If you want to use this module for an existing project that used the old
+wrapper through PyMca, you can try replacing::
+
+ from PyMca5.PyMcaIO import specfilewrapper
+
+with::
+
+ from silx.io import specfilewrapper
+
+There might still be differences between this module and the old
+wrapper, due to differences in the underlying implementation.
+Any of these differences that break your code should be reported on
+https://github.com/silx-kit/silx/issues
+
+The documentation mentions only the methods and attributes that are different
+from the ones in :class:`silx.io.specfile.SpecFile` and
+:class:`silx.io.specfile.Scan`. You should refer to the documentation of these
+base classes for more information.
+"""
+from silx.io.specfile import SpecFile, Scan
+
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "15/09/2016"
+
+
+def _format_number_list(number_list):
+ """Return string representation of a list of integers,
+ using ``,`` as a separator and ``:`` as a range separator.
+ """
+ ret = ""
+ first_in_range = number_list[0]
+ last_in_range = number_list[0]
+ previous = number_list[0]
+ for number in number_list[1:]:
+ if number - previous != 1:
+ # reached end of range
+ if last_in_range > first_in_range:
+ ret += "%d:%d," % (first_in_range, last_in_range)
+ # passed isolated number
+ else:
+ ret += "%d," % previous
+ # reinitialize range
+ first_in_range = number
+ last_in_range = number
+ else:
+ # still inside a continuous range
+ last_in_range = number
+
+ previous = number
+
+ # last number
+ if last_in_range > first_in_range:
+ ret += "%d:%d" % (first_in_range, last_in_range)
+ else:
+ ret += "%d" % previous
+
+ return ret
+
+
+class Specfile(SpecFile):
+ """
+ This class is a subclass of :class:`silx.io.specfile.SpecFile`.
+
+ It redefines following methods:
+
+ - :meth:`__getitem__`: returns a :class:`scandata` object instead of
+ a :class:`silx.io.specfile.Scan` object
+ - :meth:`list`: returns a string representation of a list instead of a
+ list of integers
+
+ Following methods are added:
+
+ - :meth:`select`
+ - :meth:`scanno`
+ - :meth:`allmotors`
+ - :meth:`epoch`
+ - :meth:`title`
+ """
+ def __init__(self, filename):
+ SpecFile.__init__(self, filename)
+
+ def __getitem__(self, key):
+ """Get scan by 0-based index
+
+ :param key: 0-based scan index
+ :type key: int
+
+ :return: Scan
+ :rtype: :class:`scandata`
+ """
+ if not isinstance(key, int):
+ raise TypeError("Scan index must be an integer")
+
+ scan_index = key
+ # allow negative index, like lists
+ if scan_index < 0:
+ scan_index += len(self)
+
+ if not 0 <= scan_index < len(self):
+ msg = "Scan index must be in range 0-%d" % (len(self) - 1)
+ raise IndexError(msg)
+
+ return scandata(self, scan_index)
+
+ def list(self):
+ """Return a string representation of a list of scan numbers.
+
+ The scans numbers are listed in the order in which they appear
+ in the file. Continuous ranges of scan numbers are represented
+ as ``first:last``.
+
+ For instance, let's assume our specfile contains following scans:
+ *1, 2, 3, 4, 5, 684, 685, 687, 688, 689, 700, 688, 688*.
+ This method will then return::
+
+ "1:5,684:685,687:689,700,688,688"
+ """
+ number_list = SpecFile.list(self)
+ return _format_number_list(number_list)
+
+ def select(self, key):
+ """Get scan by ``n.m`` key
+
+ :param key: ``"s.o"`` (scan number, scan order)
+ :type key: str
+ :return: Scan
+ :rtype: :class:`scandata`
+ """
+ msg = "Key must be a string 'N.M' with N being the scan"
+ msg += " number and M the order (eg '2.3')."
+
+ if not hasattr(key, "lower") or "." not in key:
+ raise TypeError(msg)
+
+ try:
+ (number, order) = map(int, key.split("."))
+ scan_index = self.index(number, order)
+ except (ValueError, IndexError):
+ # self.index can raise an index error
+ # int() can raise a value error
+ raise KeyError(msg + "\nValid keys: '" +
+ "', '".join(self.keys()) + "'")
+ except AttributeError:
+ # e.g. "AttrErr: 'float' object has no attribute 'split'"
+ raise TypeError(msg)
+
+ if not 0 <= scan_index < len(self):
+ msg = "Scan index must be in range 0-%d" % (len(self) - 1)
+ raise IndexError(msg)
+
+ return scandata(self, scan_index)
+
+ def scanno(self):
+ """Return the number of scans in the SpecFile
+
+ This is an alias for :meth:`__len__`, for compatibility with the old
+ specfile wrapper API.
+ """
+ return len(self)
+
+ def allmotors(self, scan_index=0):
+ """
+ This is an alias for :meth:`motor_names`, for compatibility with
+ the old specfile wrapper API.
+ """
+ return self.motor_names(scan_index)
+
+ def epoch(self):
+ """:return: Epoch, from last word on file header line *#E*
+ :rtype: int
+ :raise: ValueError if *#E* line not found in header or last
+ word on *#E* cannot be converted to type *int*"""
+ fh = self.file_header()
+ for line in fh:
+ if line.startswith("#E "):
+ return int(line.split()[-1])
+ raise ValueError("No #E header found in specfile")
+
+ def title(self):
+ """:return: Title, from second field on *#C* header line (field are
+ strings separated by two spaces)
+ :rtype: str
+ :raise: ValueError if *#C* line not found in header or line is empty"""
+ fh = self.file_header()
+ for line in fh:
+ if line.startswith("#C "):
+ line1 = line.lstrip("#C ")
+ return line1.split(" ")[0]
+ raise ValueError("No #C header found in specfile")
+
+ # # these functions exist in the old API but don't seem to be
+ # # used, and are not easy to implement
+ # def show(self):
+ # raise NotImplementedError
+ #
+ # def user(self):
+ # raise NotImplementedError
+ #
+ # def update(self):
+ # raise NotImplementedError
+
+
+# PEP8 violation in class name is to respect old API
+class scandata(Scan): # noqa
+ """
+ This class is a subclass of :class:`silx.io.specfile.Scan`.
+
+ It redefines following methods/attributes:
+
+ - :meth:`data` becomes a method returning an array, instead of just
+ an array
+ - :meth:`mca`: becomes a method returning an array, instead of
+ a :class:`silx.io.specfile.MCA` object
+ - :meth:`header`: becomes a method returning a list of **scan**
+ header lines (or a list of a single header line, if a key is
+ specified), instead of just a list of all header lines
+
+ Following methods are added:
+
+ - :meth:`allmotors`
+ - :meth:`allmotorpos`
+ - :meth:`alllabels`
+ - :meth:`cols`
+ - :meth:`lines`
+ - :meth:`command`
+ - :meth:`date`
+ - :meth:`datacol`
+ - :meth:`dataline`
+ - :meth:`fileheader`
+ - :meth:`nbmca`
+ """
+ def __init__(self, specfile, scan_index):
+ Scan.__init__(self, specfile, scan_index)
+
+ def allmotors(self):
+ """Return a list of all motor names (identical to
+ :attr:`motor_names`).
+ """
+ return self.motor_names
+
+ def allmotorpos(self):
+ """Return a list of all motor positions (identical to
+ :attr:`motor_positions`).
+ """
+ return self.motor_positions
+
+ def alllabels(self):
+ """
+ Return a list of all labels (:attr:`labels`).
+ """
+ return self.labels
+
+ def cols(self):
+ """Return the number of data columns (number of detectors)"""
+ return super(scandata, self).data.shape[1]
+
+ def command(self):
+ """Return the command called for this scan (``#S`` header line)"""
+ return self._specfile.command(self._index)
+
+ def data(self):
+ """Return the data in this scan as a 2D numpy array.
+
+ The first index corresponds to the columns/detectors in the original
+ file, and the second index is the row index from the original file.
+ Indices are 0-based.
+
+ For instance, this is how you access the 18th data sample for the 3rd
+ detector (assuming ``sc`` is your scan object):
+
+ >>> scdata = sc.data()
+ >>> data_sample = scdata[2, 17]"""
+ return super(scandata, self).data
+
+ def datacol(self, col):
+ """Return a data column (all data for one detector)
+
+ :param col: column number (1-based index)"""
+ return super(scandata, self).data[col - 1, :]
+
+ def dataline(self, line):
+ """Return a data line (one sample for all detectors)
+
+ :param line: line number (1-based index)"""
+ return super(scandata, self).data[:, line - 1]
+
+ def date(self):
+ """Return the date from the scan header line ``#D``"""
+ return self._specfile.date(self._index)
+
+ def fileheader(self, key=''): # noqa
+ """Return a list of file header lines"""
+ # key is there for compatibility
+ return self.file_header
+
+ def header(self, key=""):
+ """Return a list of scan header lines if no key is specified.
+ If a valid key is specified, return a list of a single header line.
+
+ :param key: Header key (e.g. ``S, N, L, @CALIB``…)
+ If ``key`` is an empty string, return complete list of scan header
+ lines.
+ If ``key`` does not match any header line, return empty list.
+ :return: List of scan header lines
+ :rtype: List[str]
+ """
+ if key.strip() == "":
+ return self.scan_header
+ if self.record_exists_in_hdr(key):
+ prefix = "#" + key + " "
+ # there is no leading @ in self.mca_header_dict keys
+ key_mca_dict = key.lstrip("@") if key.startswith("@") else None
+ if key_mca_dict in self.mca_header_dict:
+ return [prefix + self.mca_header_dict[key_mca_dict]]
+ elif key in self.scan_header_dict:
+ return [prefix + self.scan_header_dict[key]]
+ elif key in self.file_header_dict:
+ return [prefix + self.file_header_dict[key]]
+ elif self.record_exists_in_hdr("@" + key):
+ # in case key is a mca header key without the @
+ if key in self.mca_header_dict:
+ prefix = "#@" + key + " "
+ return [prefix + self.mca_header_dict[key]]
+ return []
+
+ def lines(self):
+ """Return the number of data lines (number of data points per
+ detector)"""
+ return super(scandata, self).data.shape[0]
+
+ def mca(self, number):
+ """Return one MCA spectrum
+
+ :param number: MCA number (1-based index)
+ :rtype: 1D numpy array"""
+ # in the base class, mca is an object that can be indexed (but 0-based)
+ return super(scandata, self).mca[number - 1]
+
+ def nbmca(self):
+ """Return number of MCAs in this scan"""
+ return len(super(scandata, self).mca)