xopr.matlab_attribute_utils
Utilities and fixes for loading MATLAB format files into Xarray.
This module provides various utilities for correctly reading MATLAB files and converting them to formats compatible with xarray datasets. It handles both modern HDF5-format MATLAB files (.mat v7.3+) and legacy MATLAB file formats.
The module addresses several common issues when loading MATLAB data:
- Dereferencing HDF5 object references in MATLAB files
- Decoding MATLAB char arrays (both uint16 Unicode and uint8 ASCII)
- Converting MATLAB cell arrays to Python lists
- Handling empty MATLAB arrays
- Stripping sensitive data (API keys) from attributes
- Converting object ndarrays to JSON-serializable lists
HDF5-Format MATLAB Files (v7.3+)
- dereference_h5value: Recursively dereference HDF5 object references
- decode_hdf5_matlab_variable: Decode MATLAB variables from HDF5 storage
Legacy MATLAB Files (v4-v7.2)
- extract_legacy_mat_attributes: Extract attributes from legacy .mat files
- strip_api_key: Remove API keys from attribute dictionaries
- convert_object_ndarrays_to_lists: Convert object arrays to lists
Notes
@private This module is not intended for external use.
1""" 2Utilities and fixes for loading MATLAB format files into Xarray. 3 4This module provides various utilities for correctly reading MATLAB files and 5converting them to formats compatible with xarray datasets. It handles both 6modern HDF5-format MATLAB files (.mat v7.3+) and legacy MATLAB file formats. 7 8The module addresses several common issues when loading MATLAB data: 9 10- Dereferencing HDF5 object references in MATLAB files 11- Decoding MATLAB char arrays (both uint16 Unicode and uint8 ASCII) 12- Converting MATLAB cell arrays to Python lists 13- Handling empty MATLAB arrays 14- Stripping sensitive data (API keys) from attributes 15- Converting object ndarrays to JSON-serializable lists 16 17HDF5-Format MATLAB Files (v7.3+) 18-------------------------------- 19- dereference_h5value: Recursively dereference HDF5 object references 20- decode_hdf5_matlab_variable: Decode MATLAB variables from HDF5 storage 21 22Legacy MATLAB Files (v4-v7.2) 23------------------------------ 24- extract_legacy_mat_attributes: Extract attributes from legacy .mat files 25- strip_api_key: Remove API keys from attribute dictionaries 26- convert_object_ndarrays_to_lists: Convert object arrays to lists 27 28Notes 29----- 30@private This module is not intended for external use. 31 32""" 33 34from collections.abc import Iterable 35 36import h5py 37import numpy as np 38import scipy.io 39 40# 41# HDF5-format MATLAB files 42# 43 44def dereference_h5value(value, h5file, make_array=True): 45 if isinstance(value, h5py.Reference): 46 return dereference_h5value(h5file[value], h5file=h5file) 47 elif isinstance(value, h5py.Group): 48 # Pass back to decode_hdf5_matlab_variable to handle groups 49 return decode_hdf5_matlab_variable(value, h5file=h5file) 50 elif isinstance(value, Iterable): 51 v = [dereference_h5value(v, h5file=h5file) for v in value] 52 if make_array: 53 try: 54 return np.squeeze(np.array(v)) 55 except: 56 return v 57 else: 58 return v 59 elif isinstance(value, np.number): 60 return value.item() 61 else: 62 return value 63 64def decode_hdf5_matlab_variable(h5var, skip_variables=False, debug_path="", skip_errors=True, h5file=None): 65 """ 66 Decode a MATLAB variable stored in an HDF5 file. 67 This function assumes the variable is stored as a byte string. 68 """ 69 if h5file is None: 70 h5file = h5var.file 71 matlab_class = h5var.attrs.get('MATLAB_class', None) 72 73 # Handle MATLAB_class as either bytes or string 74 if matlab_class and (matlab_class == b'cell' or matlab_class == 'cell'): 75 return dereference_h5value(h5var[:], h5file=h5file, make_array=False) 76 elif matlab_class and (matlab_class == b'char' or matlab_class == 'char'): 77 # Check if this is an empty MATLAB char array 78 if h5var.attrs.get('MATLAB_empty', 0): 79 return '' 80 81 # MATLAB stores char arrays as uint16 (Unicode code points) 82 # or sometimes uint8 (ASCII). Handle both cases properly. 83 data = h5var[:] 84 85 if data.dtype == np.dtype('uint16'): 86 # Each uint16 value is a Unicode code point (UCS-2/UTF-16) 87 # Convert to string by treating each value as a character code 88 chars = [chr(c) for c in data.flatten() if c != 0] 89 return ''.join(chars).rstrip() 90 elif data.dtype == np.dtype('uint8'): 91 # uint8 data can be decoded directly as UTF-8 92 return data.tobytes().decode('utf-8').rstrip('\x00') 93 else: 94 # Fallback for unexpected dtypes (including uint64 for empty arrays) 95 # First check if it's all zeros (empty string) 96 if np.all(data == 0): 97 return '' 98 # Try the old method that may work for some cases 99 try: 100 return data.astype(dtype=np.uint8).tobytes().decode('utf-8').rstrip('\x00') 101 except UnicodeDecodeError: 102 # If that fails, try to convert assuming Unicode code points 103 chars = [chr(min(c, 0x10FFFF)) for c in data.flatten() if c != 0] 104 return ''.join(chars).rstrip() 105 elif isinstance(h5var, (h5py.Group, h5py.File)): 106 attrs = {} 107 for k in h5var: 108 if k.startswith('#'): 109 continue 110 if 'api_key' in k: 111 attrs[k] = "API_KEY_REMOVED" 112 continue 113 if isinstance(h5var[k], h5py.Dataset): 114 if not skip_variables: 115 try: 116 attrs[k] = decode_hdf5_matlab_variable(h5var[k], debug_path=debug_path + "/" + k, skip_errors=skip_errors, h5file=h5file) 117 except Exception as e: 118 print(f"Failed to decode variable {k} at {debug_path}: {e}") 119 if not skip_errors: 120 raise e 121 else: 122 attrs[k] = decode_hdf5_matlab_variable(h5var[k], debug_path=debug_path + "/" + k, skip_errors=skip_errors, h5file=h5file) 123 return attrs 124 elif isinstance(h5var, h5py.Dataset): 125 if h5var.dtype == 'O': 126 return dereference_h5value(h5var[:], h5file=h5file) 127 else: 128 return np.squeeze(h5var[:]) 129 else: 130 return h5var[:] 131 132# 133# Legacy MATLAB files (non-HDF5) 134# 135 136def extract_legacy_mat_attributes(file, skip_keys=[], skip_errors=True): 137 m = scipy.io.loadmat(file, mat_dtype=False, simplify_cells=True, squeeze_me=True) 138 139 attrs = {key: value for key, value in m.items() 140 if not key.startswith('__') and key not in skip_keys} 141 142 attrs = strip_api_key(attrs) 143 attrs = convert_object_ndarrays_to_lists(attrs) 144 return attrs 145 146def strip_api_key(attrs): 147 attrs_clean = {} 148 for key, value in attrs.items(): 149 if 'api_key' in key: 150 attrs_clean[key] = "API_KEY_REMOVED" 151 elif isinstance(value, dict): 152 attrs_clean[key] = strip_api_key(value) 153 else: 154 attrs_clean[key] = value 155 return attrs_clean 156 157def convert_object_ndarrays_to_lists(attrs): 158 """ 159 Convert any object ndarray attributes to lists. 160 """ 161 for key, value in attrs.items(): 162 if isinstance(value, np.ndarray) and value.dtype == 'object': 163 attrs[key] = value.tolist() 164 elif isinstance(value, dict): 165 convert_object_ndarrays_to_lists(value) 166 else: 167 attrs[key] = value 168 return attrs
def
dereference_h5value(value, h5file, make_array=True):
45def dereference_h5value(value, h5file, make_array=True): 46 if isinstance(value, h5py.Reference): 47 return dereference_h5value(h5file[value], h5file=h5file) 48 elif isinstance(value, h5py.Group): 49 # Pass back to decode_hdf5_matlab_variable to handle groups 50 return decode_hdf5_matlab_variable(value, h5file=h5file) 51 elif isinstance(value, Iterable): 52 v = [dereference_h5value(v, h5file=h5file) for v in value] 53 if make_array: 54 try: 55 return np.squeeze(np.array(v)) 56 except: 57 return v 58 else: 59 return v 60 elif isinstance(value, np.number): 61 return value.item() 62 else: 63 return value
def
decode_hdf5_matlab_variable( h5var, skip_variables=False, debug_path='', skip_errors=True, h5file=None):
65def decode_hdf5_matlab_variable(h5var, skip_variables=False, debug_path="", skip_errors=True, h5file=None): 66 """ 67 Decode a MATLAB variable stored in an HDF5 file. 68 This function assumes the variable is stored as a byte string. 69 """ 70 if h5file is None: 71 h5file = h5var.file 72 matlab_class = h5var.attrs.get('MATLAB_class', None) 73 74 # Handle MATLAB_class as either bytes or string 75 if matlab_class and (matlab_class == b'cell' or matlab_class == 'cell'): 76 return dereference_h5value(h5var[:], h5file=h5file, make_array=False) 77 elif matlab_class and (matlab_class == b'char' or matlab_class == 'char'): 78 # Check if this is an empty MATLAB char array 79 if h5var.attrs.get('MATLAB_empty', 0): 80 return '' 81 82 # MATLAB stores char arrays as uint16 (Unicode code points) 83 # or sometimes uint8 (ASCII). Handle both cases properly. 84 data = h5var[:] 85 86 if data.dtype == np.dtype('uint16'): 87 # Each uint16 value is a Unicode code point (UCS-2/UTF-16) 88 # Convert to string by treating each value as a character code 89 chars = [chr(c) for c in data.flatten() if c != 0] 90 return ''.join(chars).rstrip() 91 elif data.dtype == np.dtype('uint8'): 92 # uint8 data can be decoded directly as UTF-8 93 return data.tobytes().decode('utf-8').rstrip('\x00') 94 else: 95 # Fallback for unexpected dtypes (including uint64 for empty arrays) 96 # First check if it's all zeros (empty string) 97 if np.all(data == 0): 98 return '' 99 # Try the old method that may work for some cases 100 try: 101 return data.astype(dtype=np.uint8).tobytes().decode('utf-8').rstrip('\x00') 102 except UnicodeDecodeError: 103 # If that fails, try to convert assuming Unicode code points 104 chars = [chr(min(c, 0x10FFFF)) for c in data.flatten() if c != 0] 105 return ''.join(chars).rstrip() 106 elif isinstance(h5var, (h5py.Group, h5py.File)): 107 attrs = {} 108 for k in h5var: 109 if k.startswith('#'): 110 continue 111 if 'api_key' in k: 112 attrs[k] = "API_KEY_REMOVED" 113 continue 114 if isinstance(h5var[k], h5py.Dataset): 115 if not skip_variables: 116 try: 117 attrs[k] = decode_hdf5_matlab_variable(h5var[k], debug_path=debug_path + "/" + k, skip_errors=skip_errors, h5file=h5file) 118 except Exception as e: 119 print(f"Failed to decode variable {k} at {debug_path}: {e}") 120 if not skip_errors: 121 raise e 122 else: 123 attrs[k] = decode_hdf5_matlab_variable(h5var[k], debug_path=debug_path + "/" + k, skip_errors=skip_errors, h5file=h5file) 124 return attrs 125 elif isinstance(h5var, h5py.Dataset): 126 if h5var.dtype == 'O': 127 return dereference_h5value(h5var[:], h5file=h5file) 128 else: 129 return np.squeeze(h5var[:]) 130 else: 131 return h5var[:]
Decode a MATLAB variable stored in an HDF5 file. This function assumes the variable is stored as a byte string.
def
extract_legacy_mat_attributes(file, skip_keys=[], skip_errors=True):
137def extract_legacy_mat_attributes(file, skip_keys=[], skip_errors=True): 138 m = scipy.io.loadmat(file, mat_dtype=False, simplify_cells=True, squeeze_me=True) 139 140 attrs = {key: value for key, value in m.items() 141 if not key.startswith('__') and key not in skip_keys} 142 143 attrs = strip_api_key(attrs) 144 attrs = convert_object_ndarrays_to_lists(attrs) 145 return attrs
def
strip_api_key(attrs):
def
convert_object_ndarrays_to_lists(attrs):
158def convert_object_ndarrays_to_lists(attrs): 159 """ 160 Convert any object ndarray attributes to lists. 161 """ 162 for key, value in attrs.items(): 163 if isinstance(value, np.ndarray) and value.dtype == 'object': 164 attrs[key] = value.tolist() 165 elif isinstance(value, dict): 166 convert_object_ndarrays_to_lists(value) 167 else: 168 attrs[key] = value 169 return attrs
Convert any object ndarray attributes to lists.