# -*- coding: utf-8 -*-
# kgraph.py
# Copyright (c) 2007, Christoph Gohlke
# Copyright (c) 2007-2008, The Regents of the University of California
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the copyright holders nor the names of any
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""Read and write KaleidaGraph(tm) QDA data files.
KaleidaGraph is a registered trademark of `Abelbeck Software
<http://www.synergy.com>`__.
:Author:
`Christoph Gohlke <http://www.lfd.uci.edu/~gohlke/>`__,
Laboratory for Fluorescence Dynamics, University of California, Irvine
:Version: 20080115
Requirements
------------
* `Python 2.5 <http://www.python.org>`__
* `Numpy 1.0 <http://numpy.scipy.org>`__
Examples
--------
>>> from kgraph import KGraph
>>> KGraph().save('_empty.qda')
>>> KGraph(
... [[1.0, 2.0, 0.], [3.0, 4.0, 5.0], [6.0, 7.0, 0.]],
... rows=[2, 3, '2'],
... headers=['X', 'Y', 'Z'],
... dtypes=['>f8', '>i4', '>f4'],
... ).save('_test.qda')
>>> kg = KGraph("_test.qda")
>>> print kg.headers[2], kg[2, :kg.rows[2]]
Z [ 6. 7.]
"""
from __future__ import with_statement
import struct
import numpy
__docformat__ = "restructuredtext en"
class KGraph(object):
"""Read or write KaleidaGraph(tm) version 3.x QDA data files.
Only numeric data types (float, double, and int) are supported.
All data are converted to double on import. The byte order of the
binary files is big endian (Motorola).
Raises IOError or ValueError on failure.
Instance Attributes
-------------------
name :
file name
data :
2D numpy array
columns :
number of columns
headers :
sequence of column headers
rows :
sequence of number of rows in column
dtypes :
sequence of column data types ('>f4', '>f8', or '>i4')
"""
_fileid = {'\x00\x06': 6, '\x00\x08': 8, '\x00\x0C': 12}
_dtypes = {0: '>f4', 3: '>f8', 4: '>i4', '>f4': 0, '>f8': 3, '>i4': 4}
def __init__(self, arg=None, **kwargs):
"""Initialize instance using file name/descriptor or data array.
If arg is an array, keyword arguments can be used to initialize
name, headers, rows, and dtypes attributes.
Raises IOError or ValueError on failure.
"""
self.fid = 12
self.name = "Untitled"
self.data = None
self.columns = None
self.rows = None
self.headers = None
self.dtypes = None
if arg is None:
self._fromdata([], **kwargs)
elif isinstance(arg, basestring):
with open(arg, 'rb') as fd:
self._fromfile(fd)
elif isinstance(arg, file):
self._fromfile(arg)
else:
self._fromdata(arg, **kwargs)
def save(self, arg=None):
"""Save data to QDA file."""
if arg is None:
arg = self.name
if isinstance(arg, file):
self._tofile(arg)
else:
with open(arg, 'wb') as fd:
self._tofile(fd)
def _fromfile(self, fd):
"""Initialize instance from open file object.
Raises IOError if file can not be read.
"""
fid = fd.read(2)
try:
self.fid = self._fileid[fid]
except KeyError:
raise IOError("Not a KGraph file or unsupported version.")
columns = numpy.fromfile(fd, dtype='>i2', count=1)[0]
if 1000 < columns < 0:
raise IOError("Not a KGraph file.")
fd.read(512-4)
rows = list(numpy.fromfile(fd, count=columns,
dtype='>i4' if self.fid == 12 else '>i2'))
try:
dtypes = [self._dtypes[dt] for dt in
numpy.fromfile(fd, dtype='>i2', count=columns)]
except KeyError:
raise IOError("The file contains data of unsupported type.")
headers = [s.split('\x00', 1)[0] for s in
numpy.fromfile(fd, dtype='S40', count=columns)]
data = numpy.empty((columns, max(rows) if rows else 0),
dtype=numpy.float64)
data[:] = numpy.NaN
for i, (row, dtype) in enumerate(zip(rows, dtypes)):
data[i, 0:row] = numpy.fromfile(fd, dtype=dtype, count=row)
fd.read(136 + 2*row)
self.name = fd.name
self.data = data
self.dtypes = dtypes
self.columns = columns
self.rows = rows
self.headers = headers
def _fromdata(self, data, name="Untitled.qda",
headers=None, rows=None, dtypes=None):
"""Initialize instance from data array and optional arguments.
Raises ValueError if data is incompatible with file format.
"""
data = numpy.array(data, dtype='>f8')
data = numpy.atleast_2d(data)
if len(data.shape) > 2:
raise ValueError("Data array must be 2 dimensional or less.")
try:
columns = data.shape[0]
except IndexError:
columns = 0
else:
if (columns > 1000):
raise ValueError("Dimensions of data array are too large.")
if rows:
try:
rows = [int(rows[i]) for i in xrange(columns)]
except (IndexError, TypeError, ValueError):
raise ValueError("Invalid rows argument.")
else:
try:
rows = [data.shape[1]] * columns
except IndexError:
rows = [0]
if (max(rows) > 32768):
raise ValueError("Data array dimensions are too large.")
if headers:
try:
headers = [headers[i][0:40] for i in xrange(columns)]
except IndexError:
raise ValueError("Invalid headers argument.")
else:
headers = _unique_headers(columns)
if dtypes:
try:
_ = [self._dtypes[
str(dtypes[i])] for i in xrange(columns)]
except (IndexError, KeyError):
raise ValueError("Invalid dtypes argument.")
else:
dtypes = ['>f8']*columns
if len(dtypes)!=columns or len(headers)!=columns or len(rows)!=columns:
raise ValueError("Invalid argument(s).")
self.fid = 12
self.name = name
self.data = data
self.columns = columns
self.rows = rows
self.headers = headers
self.dtypes = dtypes
def _tofile(self, fd):
"""Write data to an open file."""
fd.write('\x00\x0C')
fd.write(struct.pack('>h', self.columns))
fd.write('\x00\x0E\x01\x02\x00\x05\x00\x01')
fd.write('\x00' * (512-12))
for r in self.rows:
fd.write(struct.pack('>i', r))
for t in self.dtypes:
fd.write(struct.pack('>h', self._dtypes[t]))
for h in self.headers:
fd.write(h + '\x00' * (40-len(h)))
for i, (r, t, h) in enumerate(zip(self.rows, self.dtypes,
self.headers)):
self.data[i, 0:r].astype(t).tofile(fd, format=t)
fd.write('\x00\x01' * r)
fd.write('\x0E\x02\x01\x00\x05\x00\x00\x01')
fd.write(h + '\x00' * (128-len(h)))
def __str__(self):
return "\n".join("%14s: %s" % t for t in (
("File Name", self.name),
("File ID", self.fid),
("Columns", self.columns),
("Rows", self.rows),
("Headers", self.headers),
("Data Types", self.dtypes), ))
def __len__(self):
return len(self.data)
def __getitem__(self, key):
return self.data[key]
def _unique_headers(number):
"""Return list of unique column headers."""
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
headers = []
for i in chars:
if number:
headers.append(i)
else:
return headers
number -= 1
for i in chars:
for j in chars:
if number:
headers.append(i+j)
else:
return headers
number -= 1
for i in chars:
for j in chars:
for k in chars:
if number:
headers.append(i+j+k)
else:
return headers
number -= 1
raise NotImplementedError()
if __name__ == "__main__":
import doctest
doctest.testmod()