npTDMS 1.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. nptdms/__init__.py +11 -0
  2. nptdms/base_segment.py +179 -0
  3. nptdms/channel_data.py +205 -0
  4. nptdms/common.py +99 -0
  5. nptdms/daqmx.py +340 -0
  6. nptdms/export/__init__.py +0 -0
  7. nptdms/export/hdf_export.py +96 -0
  8. nptdms/export/pandas_export.py +122 -0
  9. nptdms/log.py +38 -0
  10. nptdms/reader.py +527 -0
  11. nptdms/scaling.py +667 -0
  12. nptdms/tdms.py +1013 -0
  13. nptdms/tdms_segment.py +661 -0
  14. nptdms/tdmsinfo.py +61 -0
  15. nptdms/test/__init__.py +0 -0
  16. nptdms/test/data/Digital_Input.tdms +0 -0
  17. nptdms/test/data/big_endian.tdms +0 -0
  18. nptdms/test/data/raw1.tdms +0 -0
  19. nptdms/test/data/raw_timestamps.tdms +0 -0
  20. nptdms/test/scenarios.py +996 -0
  21. nptdms/test/test_benchmarks.py +377 -0
  22. nptdms/test/test_daqmx.py +1085 -0
  23. nptdms/test/test_example_files.py +89 -0
  24. nptdms/test/test_hdf.py +328 -0
  25. nptdms/test/test_pandas.py +466 -0
  26. nptdms/test/test_scaling.py +830 -0
  27. nptdms/test/test_tdms_file.py +1243 -0
  28. nptdms/test/test_tdmsinfo.py +49 -0
  29. nptdms/test/test_thermocouples.py +213 -0
  30. nptdms/test/test_timestamps.py +246 -0
  31. nptdms/test/test_types.py +87 -0
  32. nptdms/test/util.py +341 -0
  33. nptdms/test/writer/__init__.py +0 -0
  34. nptdms/test/writer/test_tdms_segment.py +235 -0
  35. nptdms/test/writer/test_writer.py +582 -0
  36. nptdms/thermocouples.py +722 -0
  37. nptdms/timestamp.py +138 -0
  38. nptdms/types.py +317 -0
  39. nptdms/utils.py +46 -0
  40. nptdms/version.py +5 -0
  41. nptdms/writer.py +495 -0
  42. nptdms-1.11.0.dist-info/METADATA +151 -0
  43. nptdms-1.11.0.dist-info/RECORD +47 -0
  44. nptdms-1.11.0.dist-info/WHEEL +5 -0
  45. nptdms-1.11.0.dist-info/entry_points.txt +2 -0
  46. nptdms-1.11.0.dist-info/licenses/COPYING.LESSER +165 -0
  47. nptdms-1.11.0.dist-info/top_level.txt +1 -0
nptdms/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Module for reading binary TDMS files produced by LabView"""
2
+
3
+ from __future__ import absolute_import
4
+
5
+
6
+ # Make version number available
7
+ from .version import __version_info__, __version__
8
+
9
+ # Export public objects
10
+ from .tdms import TdmsFile, TdmsGroup, TdmsChannel, DataChunk, GroupDataChunk, ChannelDataChunk
11
+ from .writer import TdmsWriter, RootObject, GroupObject, ChannelObject
nptdms/base_segment.py ADDED
@@ -0,0 +1,179 @@
1
+ from io import UnsupportedOperation
2
+ import numpy as np
3
+
4
+ from nptdms.log import log_manager
5
+
6
+
7
+ log = log_manager.get_logger(__name__)
8
+
9
+
10
+ class BaseSegmentObject(object):
11
+ """ Abstract base class for an object in a TDMS segment
12
+ """
13
+
14
+ __slots__ = [
15
+ 'path', 'number_values', 'data_size',
16
+ 'has_data', 'data_type']
17
+
18
+ def __init__(self, path):
19
+ self.path = path
20
+ self.number_values = 0
21
+ self.data_size = 0
22
+ self.has_data = False
23
+ self.data_type = None
24
+
25
+ def read_raw_data_index(self, file, raw_data_index_header, endianness):
26
+ """ Read the raw data index for a single object in a segment
27
+ """
28
+ raise NotImplementedError("Segment metadata reading must be implemented in base classes")
29
+
30
+ @property
31
+ def scaler_data_types(self):
32
+ return None
33
+
34
+ def __repr__(self):
35
+ return "%s(%s)" % (self.__class__.__name__, self.path)
36
+
37
+
38
+ class BaseDataReader(object):
39
+ """ Abstract base class for reading data in a segment
40
+ """
41
+
42
+ def __init__(self, num_chunks, final_chunk_lengths_override, endianness):
43
+ self.num_chunks = num_chunks
44
+ self.final_chunk_lengths_override = final_chunk_lengths_override
45
+ self.endianness = endianness
46
+
47
+ def read_data_chunks(self, file, data_objects, num_chunks):
48
+ """ Read multiple data chunks at once
49
+ In the base case we read each chunk individually but subclasses can override this
50
+ """
51
+ for chunk in range(num_chunks):
52
+ yield self._read_data_chunk(file, data_objects, chunk)
53
+
54
+ def _read_data_chunk(self, file, data_objects, chunk_index):
55
+ """ Read data from a chunk for all channels
56
+ """
57
+ raise NotImplementedError("Data chunk reading must be implemented in base classes")
58
+
59
+ def read_channel_data_chunks(self, file, data_objects, channel_path, chunk_offset, stop_chunk):
60
+ """ Read multiple data chunks for a single channel at once
61
+ In the base case we read each chunk individually but subclasses can override this
62
+ """
63
+ for chunk_index in range(chunk_offset, stop_chunk):
64
+ yield self._read_channel_data_chunk(file, data_objects, chunk_index, channel_path)
65
+
66
+ def _read_channel_data_chunk(self, file, data_objects, chunk_index, channel_path):
67
+ """ Read data from a chunk for a single channel
68
+ """
69
+ # In the base case we can read data for all channels
70
+ # and then select only the requested channel.
71
+ # Derived classes can implement more optimised reading.
72
+ data_chunk = self._read_data_chunk(file, data_objects, chunk_index)
73
+ return data_chunk_to_channel_chunk(data_chunk, channel_path)
74
+
75
+
76
+ class RawDataChunk(object):
77
+ """Data read from a single chunk in a TDMS segment
78
+
79
+ :ivar channel_data: A dictionary of channel data chunks.
80
+ Keys are object paths and values are RawChannelDataChunk instances.
81
+ """
82
+
83
+ def __init__(self, channel_data):
84
+ self.channel_data = channel_data
85
+
86
+ @staticmethod
87
+ def empty():
88
+ return RawDataChunk({})
89
+
90
+ @staticmethod
91
+ def channel_data(data):
92
+ channel_chunks = {
93
+ path: RawChannelDataChunk.channel_data(d)
94
+ for (path, d) in data.items()
95
+ }
96
+ return RawDataChunk(channel_chunks)
97
+
98
+ @staticmethod
99
+ def scaler_data(data):
100
+ channel_chunks = {
101
+ path: RawChannelDataChunk.scaler_data(d)
102
+ for (path, d) in data.items()
103
+ }
104
+ return RawDataChunk(channel_chunks)
105
+
106
+
107
+ class RawChannelDataChunk(object):
108
+ """Data read for a single channel from a single chunk in a TDMS segment
109
+
110
+ :ivar data: Raw data in this chunk for a standard TDMS channel.
111
+ :ivar scaler_data: A dictionary of scaler data in this segment for
112
+ DAQmx raw data. Keys are the scaler id and values are data arrays.
113
+ """
114
+
115
+ def __init__(self, data, scaler_data):
116
+ self.data = data
117
+ self.scaler_data = scaler_data
118
+
119
+ def __len__(self):
120
+ if self.data is not None:
121
+ return len(self.data)
122
+ elif self.scaler_data is not None:
123
+ return next(len(d) for d in self.scaler_data.values())
124
+ return 0
125
+
126
+ @staticmethod
127
+ def empty():
128
+ return RawChannelDataChunk(None, None)
129
+
130
+ @staticmethod
131
+ def channel_data(data):
132
+ return RawChannelDataChunk(data, None)
133
+
134
+ @staticmethod
135
+ def scaler_data(data):
136
+ return RawChannelDataChunk(None, data)
137
+
138
+
139
+ def fromfile(file, dtype, count, *args, **kwargs):
140
+ """Read from any file-like object into a numpy array"""
141
+
142
+ itemsize = np.dtype(dtype).itemsize
143
+ buffer = np.zeros(count * itemsize, np.uint8)
144
+ bytes_read = -1
145
+ offset = 0
146
+ while bytes_read != 0:
147
+ bytes_read = file.readinto(buffer[offset:])
148
+ offset += bytes_read
149
+ rounded_bytes = (offset // itemsize) * itemsize
150
+ buffer = buffer[:rounded_bytes]
151
+ buffer.dtype = dtype
152
+ return buffer
153
+
154
+
155
+ def read_interleaved_segment_bytes(f, bytes_per_row, num_values):
156
+ """ Read a segment of interleaved data as rows of bytes
157
+ """
158
+ number_bytes = bytes_per_row * num_values
159
+ combined_data = fromfile(f, dtype=np.uint8, count=number_bytes)
160
+
161
+ try:
162
+ # Reshape, so that one row is all bytes for all objects
163
+ combined_data = combined_data.reshape(-1, bytes_per_row)
164
+ except ValueError:
165
+ # Probably incomplete segment at the end => try to clip data
166
+ crop_len = (combined_data.shape[0] // bytes_per_row)
167
+ crop_len *= bytes_per_row
168
+ log.warning("Cropping data from %d to %d bytes to match segment "
169
+ "size derived from channels",
170
+ combined_data.shape[0], crop_len)
171
+ combined_data = combined_data[:crop_len].reshape(-1, bytes_per_row)
172
+ return combined_data
173
+
174
+
175
+ def data_chunk_to_channel_chunk(data_chunk, channel_path):
176
+ try:
177
+ return data_chunk.channel_data[channel_path]
178
+ except KeyError:
179
+ return RawChannelDataChunk.empty()
nptdms/channel_data.py ADDED
@@ -0,0 +1,205 @@
1
+ """ Responsible for storing data read from TDMS files
2
+ """
3
+
4
+ import tempfile
5
+ import numpy as np
6
+
7
+ from nptdms import types
8
+ from nptdms.timestamp import TimestampArray
9
+ from nptdms.log import log_manager
10
+
11
+ log = log_manager.get_logger(__name__)
12
+
13
+
14
+ def get_data_receiver(obj, num_values, raw_timestamps, memmap_dir=None):
15
+ """Return a new channel data receiver to use for the given TDMS object
16
+
17
+ :param obj: TDMS channel object to receive data for
18
+ :param num_values: Number of values to be stored
19
+ :param raw_timestamps: Whether to store timestamp data as raw TDMS timestamps or a numpy datetime64 array
20
+ :param memmap_dir: Optional directory to store memory map files,
21
+ or None to not use memory map files
22
+ """
23
+ if obj.data_type == types.DaqMxRawData:
24
+ return DaqmxDataReceiver(obj, num_values, memmap_dir)
25
+
26
+ if obj.data_type == types.TimeStamp:
27
+ return TimestampDataReceiver(obj, num_values, raw_timestamps, memmap_dir)
28
+
29
+ if obj.data_type is None or obj.data_type.nptype is None:
30
+ return ListDataReceiver(obj)
31
+
32
+ return NumpyDataReceiver(obj, num_values, memmap_dir)
33
+
34
+
35
+ class ListDataReceiver(object):
36
+ """Simple list based data receiver for objects that don't have a
37
+ corresponding numpy data type
38
+
39
+ :ivar data: List of data points
40
+ """
41
+
42
+ def __init__(self, channel):
43
+ """Initialise new data receiver for a TDMS object
44
+ """
45
+ if channel.data_type == types.String:
46
+ self._dtype = np.dtype('O')
47
+ else:
48
+ self._dtype = None
49
+ self._data = []
50
+ self.scaler_data = {}
51
+
52
+ def append_data(self, data):
53
+ """Append data from a segment
54
+ """
55
+ self._data.extend(data)
56
+
57
+ @property
58
+ def data(self):
59
+ dtype = self._dtype if self._dtype is not None else np.dtype('V8')
60
+ return np.array(self._data, dtype=dtype)
61
+
62
+
63
+ class NumpyDataReceiver(object):
64
+ """Receives data for a TDMS object and stores it in a numpy array
65
+
66
+ :ivar data: Data that has been read for the object
67
+ """
68
+
69
+ def __init__(self, obj, num_values, memmap_dir=None):
70
+ """Initialise data receiver backed by a numpy array
71
+
72
+ :param obj: Object to store data for
73
+ :param num_values: Number of values to be stored
74
+ :param memmap_dir: Optional directory to store memory map files in.
75
+ """
76
+
77
+ self.path = obj.path
78
+ self.data = _new_numpy_array(
79
+ obj.data_type.nptype, num_values, memmap_dir)
80
+ self.scaler_data = {}
81
+ self._data_insert_position = 0
82
+ log.debug("Allocated %d sample slots for %s", len(self.data), obj.path)
83
+
84
+ def append_data(self, new_data):
85
+ """Update the object data with a new array of data"""
86
+
87
+ log.debug("Adding %d data points to data for %s", len(new_data), self.path)
88
+ start_pos = self._data_insert_position
89
+ end_pos = self._data_insert_position + len(new_data)
90
+ self.data[start_pos:end_pos] = new_data
91
+ self._data_insert_position += len(new_data)
92
+
93
+
94
+ class DaqmxDataReceiver(object):
95
+ """Receives raw scaler data for a DAQmx object and stores it in numpy
96
+ arrays
97
+
98
+ :ivar scaler_data: Dictionary mapping from scaler id to data for a scaler
99
+ """
100
+
101
+ def __init__(self, obj, num_values, memmap_dir=None):
102
+ """Initialise data receiver for DAQmx backed by a numpy array
103
+
104
+ :param obj: Object to store data for
105
+ :param memmap_dir: Optional directory to store memory mmap files in.
106
+ """
107
+
108
+ self.path = obj.path
109
+ self.data = None
110
+ self.scaler_data = {}
111
+ self._scaler_insert_positions = {}
112
+ for scaler_id, scaler_type in obj.scaler_data_types.items():
113
+ self.scaler_data[scaler_id] = _new_numpy_array(
114
+ scaler_type.nptype, num_values, memmap_dir)
115
+ self._scaler_insert_positions[scaler_id] = 0
116
+
117
+ def append_scaler_data(self, scale_id, new_data):
118
+ """Append new DAQmx scaler data read from a segment
119
+ """
120
+
121
+ log.debug("Adding %d data points for object %s, scaler %d",
122
+ len(new_data), self.path, scale_id)
123
+ data_array = self.scaler_data[scale_id]
124
+ start_pos = self._scaler_insert_positions[scale_id]
125
+ end_pos = start_pos + len(new_data)
126
+ data_array[start_pos:end_pos] = new_data
127
+ self._scaler_insert_positions[scale_id] += len(new_data)
128
+
129
+
130
+ class TimestampDataReceiver(object):
131
+ """Receives timestamp data for a TDMS object and stores it in a numpy array
132
+
133
+ :ivar data: Data that has been read for the object as either a TimestampArray
134
+ or datetime64 array depending on whether raw_timestamps is True or False
135
+ """
136
+
137
+ def __init__(self, obj, num_values, raw_timestamps=False, memmap_dir=None):
138
+ """Initialise timestamp data receiver backed by a numpy array
139
+
140
+ :param obj: Object to store data for
141
+ :param num_values: Number of values to be stored
142
+ :param raw_timestamps: Whether to store data as raw TDMS timestamps or a numpy datetime64 array
143
+ :param memmap_dir: Optional directory to store memory map files in.
144
+ """
145
+
146
+ self.path = obj.path
147
+ self._raw_timestamps = raw_timestamps
148
+ if raw_timestamps:
149
+ byte_array = _new_numpy_array(np.dtype('uint8'), num_values * 16, memmap_dir)
150
+ dtype = np.dtype([('second_fractions', 'uint64'), ('seconds', 'int64')])
151
+ self.data = TimestampArray(byte_array.view(dtype))
152
+ else:
153
+ self.data = _new_numpy_array(np.dtype('datetime64[us]'), num_values, memmap_dir)
154
+ self.scaler_data = {}
155
+ self._data_insert_position = 0
156
+ log.debug("Allocated %d sample slots for %s", len(self.data), obj.path)
157
+
158
+ def append_data(self, new_data):
159
+ """Update the object data with a new array of data"""
160
+
161
+ log.debug("Adding %d data points to data for %s", len(new_data), self.path)
162
+ start_pos = self._data_insert_position
163
+ end_pos = self._data_insert_position + len(new_data)
164
+ if self._raw_timestamps:
165
+ # Need to be careful about potential endianness mismatch, so order of fields can differ
166
+ self.data['seconds'][start_pos:end_pos] = new_data['seconds']
167
+ self.data['second_fractions'][start_pos:end_pos] = new_data['second_fractions']
168
+ else:
169
+ self.data[start_pos:end_pos] = new_data.as_datetime64()
170
+ self._data_insert_position += len(new_data)
171
+
172
+
173
+ class RawDataSlice(object):
174
+ def __init__(self, data, scaler_data):
175
+ self.data = data
176
+ self.scaler_data = scaler_data
177
+
178
+
179
+ def slice_raw_data(raw_data, offset, length=None):
180
+ if offset == 0 and length is None:
181
+ return raw_data
182
+ end = None if length is None else offset + length
183
+ data = None if raw_data.data is None else raw_data.data[offset:end]
184
+ scaler_data = dict(
185
+ (scale_id, scaler_data[offset:end]) for (scale_id, scaler_data) in raw_data.scaler_data.items())
186
+ return RawDataSlice(data, scaler_data)
187
+
188
+
189
+ def _new_numpy_array(dtype, num_values, memmap_dir=None):
190
+ """Initialise a new numpy array for data
191
+
192
+ :param dtype: Numpy data type for array
193
+ :param num_values: Capacity required
194
+ :param memmap_dir: Optional directory to store memory mmap files
195
+ """
196
+ if memmap_dir:
197
+ memmap_file = tempfile.NamedTemporaryFile(
198
+ mode='w+b', prefix="nptdms_", dir=memmap_dir)
199
+ return np.memmap(
200
+ memmap_file.file,
201
+ mode='w+',
202
+ shape=(num_values,),
203
+ dtype=dtype)
204
+
205
+ return np.zeros(num_values, dtype=dtype)
nptdms/common.py ADDED
@@ -0,0 +1,99 @@
1
+ from itertools import zip_longest
2
+
3
+
4
+ toc_properties = {
5
+ 'kTocMetaData': (1 << 1),
6
+ 'kTocRawData': (1 << 3),
7
+ 'kTocDAQmxRawData': (1 << 7),
8
+ 'kTocInterleavedData': (1 << 5),
9
+ 'kTocBigEndian': (1 << 6),
10
+ 'kTocNewObjList': (1 << 2)
11
+ }
12
+
13
+
14
+ class ObjectPath(object):
15
+ """ Represents the path of an object in a TDMS file
16
+
17
+ :ivar group: Group name or None for the root object
18
+ :ivar channel: Channel name or None for the root object or a group objecct
19
+ """
20
+ def __init__(self, *path_components):
21
+ self.group = None
22
+ self.channel = None
23
+ if len(path_components) > 0:
24
+ self.group = path_components[0]
25
+ if len(path_components) > 1:
26
+ self.channel = path_components[1]
27
+ if len(path_components) > 2:
28
+ raise ValueError("Object path may only have up to two components")
29
+ self._path = _components_to_path(self.group, self.channel)
30
+
31
+ @property
32
+ def is_root(self):
33
+ return self.group is None
34
+
35
+ @property
36
+ def is_group(self):
37
+ return self.group is not None and self.channel is None
38
+
39
+ @property
40
+ def is_channel(self):
41
+ return self.channel is not None
42
+
43
+ def group_path(self):
44
+ """ For channel paths, returns the path of the channel's group as a string
45
+ """
46
+ return _components_to_path(self.group, None)
47
+
48
+ @staticmethod
49
+ def from_string(path_string):
50
+ components = list(_path_components(path_string))
51
+ return ObjectPath(*components)
52
+
53
+ def __str__(self):
54
+ """ String representation of the object path
55
+ """
56
+ return self._path
57
+
58
+
59
+ def _path_components(path):
60
+ """ Generator that yields components within an object path
61
+ """
62
+ # Iterate over each character and the next character
63
+ chars = zip_longest(path, path[1:])
64
+ try:
65
+ # Iterate over components
66
+ while True:
67
+ char, next_char = next(chars)
68
+ if char != '/':
69
+ raise ValueError("Invalid path, expected \"/\"")
70
+ elif next_char is not None and next_char != "'":
71
+ raise ValueError("Invalid path, expected \"'\"")
72
+ else:
73
+ # Consume "'" or raise StopIteration if at the end
74
+ next(chars)
75
+ component = []
76
+ # Iterate over characters in component name
77
+ while True:
78
+ char, next_char = next(chars)
79
+ if char == "'" and next_char == "'":
80
+ component += "'"
81
+ # Consume second "'"
82
+ next(chars)
83
+ elif char == "'":
84
+ yield "".join(component)
85
+ break
86
+ else:
87
+ component += char
88
+ except StopIteration:
89
+ return
90
+
91
+
92
+ def _components_to_path(group, channel):
93
+ components = []
94
+ if group is not None:
95
+ components.append(group)
96
+ if channel is not None:
97
+ components.append(channel)
98
+ return ('/' + '/'.join(
99
+ ["'" + c.replace("'", "''") + "'" for c in components]))