mat-io 0.1.0__tar.gz

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.
mat_io-0.1.0/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, foreverallama
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
mat_io-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: mat-io
3
+ Version: 0.1.0
4
+ Summary: A package for reading MATLAB .mat files
5
+ Author-email: foreverallama <klockatoo@gmail.com>
6
+ Project-URL: Homepage, https://github.com/foreverallama/matio
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: scipy>=1.15.0
11
+ Requires-Dist: numpy>=2.2.2
12
+ Requires-Dist: pytz
13
+ Requires-Dist: pandas>=2.2.0
14
+ Dynamic: license-file
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: mat-io
3
+ Version: 0.1.0
4
+ Summary: A package for reading MATLAB .mat files
5
+ Author-email: foreverallama <klockatoo@gmail.com>
6
+ Project-URL: Homepage, https://github.com/foreverallama/matio
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: scipy>=1.15.0
11
+ Requires-Dist: numpy>=2.2.2
12
+ Requires-Dist: pytz
13
+ Requires-Dist: pandas>=2.2.0
14
+ Dynamic: license-file
@@ -0,0 +1,17 @@
1
+ LICENSE
2
+ pyproject.toml
3
+ mat_io.egg-info/PKG-INFO
4
+ mat_io.egg-info/SOURCES.txt
5
+ mat_io.egg-info/dependency_links.txt
6
+ mat_io.egg-info/requires.txt
7
+ mat_io.egg-info/top_level.txt
8
+ matio/__init__.py
9
+ matio/class_parser.py
10
+ matio/readmat.py
11
+ matio/subsystem.py
12
+ tests/test_mcos/test_datetime/test_datetime.py
13
+ tests/test_mcos/test_duration/test_duration.py
14
+ tests/test_mcos/test_enum/test_enum.py
15
+ tests/test_mcos/test_simple/test_simple.py
16
+ tests/test_mcos/test_string/test_string.py
17
+ tests/test_mcos/test_user_defined/test_user_defined.py
@@ -0,0 +1,4 @@
1
+ scipy>=1.15.0
2
+ numpy>=2.2.2
3
+ pytz
4
+ pandas>=2.2.0
@@ -0,0 +1,6 @@
1
+ dist
2
+ docs
3
+ examples
4
+ matio
5
+ patches
6
+ tests
@@ -0,0 +1,3 @@
1
+ from .readmat import load_from_mat
2
+
3
+ __all__ = ["load_from_mat"]
@@ -0,0 +1,259 @@
1
+ import warnings
2
+ from datetime import datetime
3
+ from zoneinfo import ZoneInfo
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+
8
+ # TODO: Add support for following classes:
9
+ # 1. dynamicprops
10
+ # 2. function_handle
11
+ # 3. event.proplistener
12
+
13
+
14
+ def get_tz_offset(tz):
15
+ """Get timezone offset in milliseconds
16
+ Inputs:
17
+ 1. tz (str): Timezone string
18
+ Returns:
19
+ 1. offset (int): Timezone offset in milliseconds
20
+ """
21
+ try:
22
+ tzinfo = ZoneInfo(tz)
23
+ utc_offset = tzinfo.utcoffset(datetime.now())
24
+ if utc_offset is not None:
25
+ offset = int(utc_offset.total_seconds() * 1000)
26
+ else:
27
+ offset = 0
28
+ except Exception as e:
29
+ warnings.warn(
30
+ f"Could not get timezone offset for {tz}: {e}. Defaulting to UTC."
31
+ )
32
+ offset = 0
33
+ return offset
34
+
35
+
36
+ def MatDatetime(props):
37
+ """Convert MATLAB datetime to Python datetime
38
+ Datetime returned as numpy.datetime64[ms]
39
+ """
40
+
41
+ data = props[0, 0].get("data", np.array([]))
42
+ if data.size == 0:
43
+ props[0, 0]["data"] = np.array([], dtype="datetime64[ms]")
44
+ return props
45
+ tz = props[0, 0].get("tz", None)
46
+ if tz.size > 0:
47
+ offset = get_tz_offset(tz.item())
48
+ else:
49
+ offset = 0
50
+
51
+ millis = data.real + data.imag * 1e3 + offset
52
+
53
+ props[0, 0]["data"] = millis.astype("datetime64[ms]")
54
+ return props
55
+
56
+
57
+ def MatDuration(props):
58
+ """Convert MATLAB duration to Python timedelta
59
+ Duration returned as numpy.timedelta64
60
+ """
61
+
62
+ millis = props[0, 0]["millis"]
63
+ if millis.size == 0:
64
+ props[0, 0]["millis"] = np.array([], dtype="timedelta64[ms]")
65
+ return props
66
+
67
+ fmt = props[0, 0].get("fmt", None)
68
+ if fmt == "s":
69
+ count = millis / 1000 # Seconds
70
+ dur = count.astype("timedelta64[s]")
71
+ elif fmt == "m":
72
+ count = millis / 60000 # Minutes
73
+ dur = count.astype("timedelta64[m]")
74
+ elif fmt == "h":
75
+ count = millis / 3600000 # Hours
76
+ dur = count.astype("timedelta64[h]")
77
+ elif fmt == "d":
78
+ count = millis / 86400000 # Days
79
+ dur = count.astype("timedelta64[D]")
80
+ else:
81
+ count = millis
82
+ dur = count.astype("timedelta64[ms]")
83
+ # Default case
84
+
85
+ props[0, 0]["millis"] = dur
86
+ return props
87
+
88
+
89
+ def MatString(props, byte_order):
90
+ """Parse string data from MATLAB file
91
+ Strings are stored within a uint64 array with the following format:
92
+ 1. version
93
+ 2. ndims
94
+ 3. shape
95
+ 4. char_counts
96
+ 5. List of null-terminated strings as uint16 integers
97
+ """
98
+
99
+ data = props[0, 0].get("any", np.array([]))
100
+ if data.size == 0:
101
+ data = np.array([[]], dtype="U")
102
+ props[0, 0]["any"] = data
103
+ return props
104
+
105
+ version = data[0, 0]
106
+ if version != 1:
107
+ warnings.warn(
108
+ "String saved from a different MAT-file version. This may work unexpectedly",
109
+ UserWarning,
110
+ )
111
+
112
+ ndims = data[0, 1]
113
+ shape = data[0, 2 : 2 + ndims]
114
+ num_strings = np.prod(shape)
115
+ char_counts = data[0, 2 + ndims : 2 + ndims + num_strings]
116
+ offset = 2 + ndims + num_strings # start of string data
117
+ byte_data = data[0, offset:].tobytes()
118
+
119
+ strings = []
120
+ pos = 0
121
+ encoding = "utf-16-le" if byte_order[0] == "<" else "utf-16-be"
122
+ for char_count in char_counts:
123
+ byte_length = char_count * 2 # UTF-16 encoding
124
+ extracted_string = byte_data[pos : pos + byte_length].decode(encoding)
125
+ strings.append(extracted_string)
126
+ pos += byte_length
127
+
128
+ arr_str = np.reshape(strings, shape, order="F")
129
+ props[0, 0]["any"] = arr_str
130
+ return props
131
+
132
+
133
+ class MatTable:
134
+ # TODO: Collect cases and fix
135
+ def __init__(self, props, defaults):
136
+ self.data = defaults["data"]
137
+
138
+ for field in [
139
+ "data",
140
+ "ndims",
141
+ "nrows",
142
+ "nvars",
143
+ "rownames",
144
+ "varnames",
145
+ "props",
146
+ ]:
147
+ if field in props.dtype.names:
148
+ setattr(self, field, props[field])
149
+
150
+ self.df = self._build_dataframe()
151
+
152
+ def __repr__(self):
153
+ return repr(self.df)
154
+
155
+ def __str__(self):
156
+ return str(self.df)
157
+
158
+ def _extract_cell_value(self, cell):
159
+ if isinstance(cell, np.ndarray) and cell.dtype == object:
160
+ return cell[0, 0]["__fields__"]
161
+ if isinstance(cell, dict):
162
+ return cell["__properties__"]
163
+ return cell
164
+
165
+ def _build_dataframe(self):
166
+ columns = {}
167
+ for i in range(int(self.nvars.item())):
168
+ varname = self._extract_cell_value(self.varnames[0, i]).item()
169
+ coldata = [
170
+ data.item() for data in self._extract_cell_value(self.data[0, i])
171
+ ]
172
+ columns[varname] = coldata
173
+
174
+ df = pd.DataFrame(columns)
175
+ if self.rownames.size > 0:
176
+ rownames = [self._extract_cell_value(rn) for rn in self.rownames[0]]
177
+ if len(rownames) == self.nrows:
178
+ df.index = rownames
179
+
180
+ return df
181
+
182
+
183
+ class MatTimetable:
184
+ # TODO: Collect cases and fix
185
+ def __init__(self, obj_dict):
186
+ self.any = obj_dict.get("any")[0, 0]
187
+ self.data = self.any["data"]
188
+ self.numDims = self.any["numDims"]
189
+ self.dimNames = self.any["dimNames"]
190
+ self.varNames = self.any["varNames"]
191
+ self.numRows = self.any["numRows"]
192
+ self.numVars = self.any["numVars"]
193
+ self.rowTimes = self.any["rowTimes"]
194
+ self.df = self._build_dataframe()
195
+
196
+ def __str__(self):
197
+ return str(self.df)
198
+
199
+ def __repr__(self):
200
+ return repr(self.df)
201
+
202
+ def _extract_cell_value(self, cell):
203
+ if isinstance(cell, np.ndarray) and cell.dtype == object:
204
+ return cell[0, 0]["__fields__"]
205
+ return cell
206
+
207
+ def _build_dataframe(self):
208
+ columns = {}
209
+ for i in range(int(self.numVars.item())):
210
+ varname = self._extract_cell_value(self.varNames[0, i]).item()
211
+ coldata = [
212
+ data.item() for data in self._extract_cell_value(self.data[0, i])
213
+ ]
214
+ columns[varname] = coldata
215
+
216
+ df = pd.DataFrame(columns)
217
+ time_arr = self.rowTimes[0, 0]["__fields__"]
218
+ times = [time_arr[i].item() for i in range(int(self.numRows.item()))]
219
+ df.index = pd.to_datetime(times)
220
+ df.index.name = self._extract_cell_value(self.dimNames[0, 0]).item()
221
+
222
+ return df
223
+
224
+
225
+ def convert_to_object(props, class_name, byte_order):
226
+ """Converts the object to a Python compatible object"""
227
+
228
+ if class_name == "datetime":
229
+ obj = MatDatetime(props)
230
+
231
+ elif class_name == "duration":
232
+ obj = MatDuration(props)
233
+
234
+ elif class_name == "string":
235
+ obj = MatString(props, byte_order)
236
+
237
+ # elif class_name == "table":
238
+ # obj = MatTable(props[0, 0], defaults)
239
+
240
+ # elif class_name == "timetable":
241
+ # if "any" in props.dtype.names:
242
+ # obj = MatTimetable(props[0, 0], defaults)
243
+
244
+ else:
245
+ # For all other classes, return raw data
246
+ obj = props
247
+
248
+ return obj
249
+
250
+
251
+ def wrap_enumeration_instance(enum_array, shapes):
252
+ """Wraps enumeration instance data into a dictionary"""
253
+ wrapped_dict = {"_Values": np.empty(shapes, dtype=object)}
254
+ if len(enum_array) == 0:
255
+ wrapped_dict["_Values"] = np.array([], dtype=object)
256
+ else:
257
+ enum_props = [item.get("_Props", np.array([]))[0, 0] for item in enum_array]
258
+ wrapped_dict["_Values"] = np.array(enum_props).reshape(shapes, order="F")
259
+ return wrapped_dict
@@ -0,0 +1,170 @@
1
+ from io import BytesIO
2
+
3
+ import numpy as np
4
+ from scipy.io import loadmat
5
+ from scipy.io.matlab._mio5 import MatFile5Reader
6
+ from scipy.io.matlab._mio5_params import OPAQUE_DTYPE
7
+
8
+ from .subsystem import SubsystemReader
9
+
10
+
11
+ def get_matfile_version(ssStream):
12
+ """Reads subsystem MAT-file version and endianness
13
+ Inputs
14
+ 1. ssStream (BytesIO): Subsystem data stream
15
+ Returns:
16
+ 1. v_major (int): Major version
17
+ 2. v_minor (int): Minor version
18
+ 3. byte_order (str): Endianness
19
+ """
20
+
21
+ ssStream.seek(0)
22
+ data = ssStream.read(4)
23
+ maj_ind = int(data[2] == b"I"[0])
24
+ v_major = int(data[maj_ind])
25
+ v_minor = int(data[1 - maj_ind])
26
+ byte_order = "<" if data[2] == b"I"[0] else ">"
27
+ if v_major in (1, 2):
28
+ return v_major, v_minor, byte_order
29
+
30
+ # Unsure if Subsystem and MAT-file versions can be different
31
+ raise ValueError(
32
+ "Unknown subsystem data type, version {}, {}".format(v_major, v_minor)
33
+ )
34
+
35
+
36
+ def read_subsystem(ssdata, **kwargs):
37
+ """Reads subsystem data as a MAT-file stream
38
+ Inputs
39
+ 1. ssdata (numpy.ndarray): Subsystem data from "__function_workspace__"
40
+ 2. kwargs: Additional arguments for scipy.io.loadmat
41
+ Returns:
42
+ 1. subsystem data (numpy.ndarray): Parsed subsystem data
43
+ 2. byte_order (str)
44
+ """
45
+ ssStream = BytesIO(ssdata)
46
+
47
+ mjv, mnv, byte_order = get_matfile_version(ssStream)
48
+ if mjv != 1:
49
+ raise NotImplementedError(f"Subsystem version {mjv}.{mnv} not supported")
50
+
51
+ kwargs.pop("byte_order", None)
52
+ kwargs.pop("variable_names", None)
53
+
54
+ ssStream.seek(8) # Skip subsystem header
55
+ MR = MatFile5Reader(ssStream, byte_order=byte_order, **kwargs)
56
+ MR.initialize_read()
57
+ hdr, _ = MR.read_var_header()
58
+ try:
59
+ res = MR.read_var_array(hdr, process=False)
60
+ except Exception as err:
61
+ raise ValueError(f"Error reading subsystem data: {err}")
62
+
63
+ return res, byte_order
64
+
65
+
66
+ def remove_unsupported_args(kwargs):
67
+ """Removes unsupported arguments for scipy.io.loadmat"""
68
+ kwargs.pop("simplify_cells", None)
69
+ kwargs.pop("squeeze_me", None)
70
+ kwargs.pop("struct_as_record", None)
71
+ kwargs.pop("uint16_codec", None)
72
+ kwargs.pop("chars_as_strings", False)
73
+
74
+
75
+ def get_function_workspace(file_path, mdict=None, spmatrix=True, **kwargs):
76
+ """Reads function workspace from MAT-file
77
+ Inputs
78
+ 1. file_path (str): Path to MAT-file
79
+ 2. mdict (dict): Dictionary to store loaded variables
80
+ 3. spmatrix (bool): Whether to load sparse matrices
81
+ 4. kwargs: Additional arguments for scipy.io.loadmat
82
+ Returns:
83
+ 1. matfile_dict (dict): Dictionary of loaded variables
84
+ 2. ssdata: Subsystem data from "__function_workspace__"
85
+ """
86
+ matfile_dict = loadmat(file_path, mdict=mdict, spmatrix=spmatrix, **kwargs)
87
+ ssdata = matfile_dict.pop("__function_workspace__", None)
88
+ return matfile_dict, ssdata
89
+
90
+
91
+ def find_opaque_dtype(arr, subsystem, path=()):
92
+ """Recursively finds and replaces mxOPAQUE_CLASS objects in a numpy array
93
+ with the corresponding MCOS object.
94
+
95
+ This is a hacky solution to find mxOPAQUE_CLASS arrays inside struct arrays or cell arrays.
96
+ """
97
+
98
+ if not isinstance(arr, np.ndarray):
99
+ return arr
100
+
101
+ if arr.dtype == OPAQUE_DTYPE:
102
+ type_system = arr[0]["_TypeSystem"]
103
+ metadata = arr[0]["_Metadata"]
104
+ return subsystem.read_mcos_object(metadata, type_system)
105
+
106
+ elif arr.dtype == object:
107
+ # Iterate through cell arrays
108
+ for idx in np.ndindex(arr.shape):
109
+ cell_item = arr[idx]
110
+ if cell_item.dtype == OPAQUE_DTYPE:
111
+ type_system = cell_item[0]["_TypeSystem"]
112
+ metadata = cell_item[0]["_Metadata"]
113
+ arr[idx] = subsystem.read_mcos_object(metadata, type_system)
114
+ else:
115
+ find_opaque_dtype(cell_item, subsystem, path + (idx,))
116
+
117
+ elif arr.dtype.names:
118
+ # Iterate though struct array
119
+ for idx in np.ndindex(arr.shape):
120
+ for name in arr.dtype.names:
121
+ field_val = arr[idx][name]
122
+ if field_val.dtype == OPAQUE_DTYPE:
123
+ type_system = field_val[0]["_TypeSystem"]
124
+ metadata = field_val[0]["_Metadata"]
125
+ arr[idx][name] = subsystem.read_mcos_object(metadata, type_system)
126
+ else:
127
+ find_opaque_dtype(field_val, subsystem, path + (idx, name))
128
+
129
+ return arr
130
+
131
+
132
+ def load_from_mat(file_path, mdict=None, raw_data=False, *, spmatrix=True, **kwargs):
133
+ """Loads variables from MAT-file
134
+ Calls scipy.io.loadmat to read the MAT-file and then processes the
135
+ "__function_workspace__" variable to extract subsystem data.
136
+ Inputs
137
+ 1. file_path (str): Path to MAT-file
138
+ 2. mdict (dict): Dictionary to store loaded variables
139
+ 3. raw_data (bool): Whether to return raw data for objects
140
+ 4. spmatrix (bool): Additional arguments for scipy.io.loadmat
141
+ 5. kwargs: Additional arguments for scipy.io.loadmat
142
+ Returns:
143
+ 1. mdict (dict): Dictionary of loaded variables
144
+ """
145
+
146
+ remove_unsupported_args(kwargs)
147
+
148
+ matfile_dict, ssdata = get_function_workspace(file_path, mdict, spmatrix, **kwargs)
149
+ if ssdata is None:
150
+ # No subsystem data in file
151
+ if mdict is not None:
152
+ mdict.update(matfile_dict)
153
+ return mdict
154
+ return matfile_dict
155
+
156
+ ss_array, byte_order = read_subsystem(ssdata, **kwargs)
157
+ subsystem = SubsystemReader(ss_array, byte_order, raw_data)
158
+
159
+ for var, data in matfile_dict.items():
160
+ if not isinstance(data, np.ndarray):
161
+ continue
162
+ matfile_dict[var] = find_opaque_dtype(data, subsystem)
163
+
164
+ # Update mdict if present
165
+ if mdict is not None:
166
+ mdict.update(matfile_dict)
167
+ else:
168
+ mdict = matfile_dict
169
+
170
+ return mdict