Coverage for src / sdynpy / core / sdynpy_array.py: 63%

118 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-11 16:22 +0000

1# -*- coding: utf-8 -*- 

2""" 

3Base class for all SDynPy object arrays. 

4 

5SDynPy object arrays are subclasses of numpy's ndarray. SDynPy uses structured 

6arrays to store the underlying data objects, resulting in potentially complex 

7data types while still achieving the efficiency and flexibility of numpy arrays. 

8 

9This module defines the SdynpyArray, which is a subclass of numpy ndarray. The 

10core SDynPy objects inherit from this class. The main contribution of this 

11array is allowing users to access the underlying structured array fields using 

12attribute notation rather than the index notation used by numpy 

13(e.g. object.field rather than object["field"]). 

14""" 

15""" 

16Copyright 2022 National Technology & Engineering Solutions of Sandia, 

17LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. 

18Government retains certain rights in this software. 

19 

20This program is free software: you can redistribute it and/or modify 

21it under the terms of the GNU General Public License as published by 

22the Free Software Foundation, either version 3 of the License, or 

23(at your option) any later version. 

24 

25This program is distributed in the hope that it will be useful, 

26but WITHOUT ANY WARRANTY; without even the implied warranty of 

27MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

28GNU General Public License for more details. 

29 

30You should have received a copy of the GNU General Public License 

31along with this program. If not, see <https://www.gnu.org/licenses/>. 

32""" 

33 

34import numpy as np 

35from scipy.io import savemat as scipy_savemat 

36 

37 

38class SdynpyArray(np.ndarray): 

39 """Superclass of the core SDynPy objects 

40 

41 The core SDynPy object arrays inherit from this class. The class is a 

42 subclass of numpy's ndarray. The underlying data structure is stored as a 

43 structured array, but the class's implementation allows accessing the array 

44 fields as if they were attributes. 

45 """ 

46 def __new__(subtype, shape, dtype=float, buffer=None, offset=0, 

47 strides=None, order=None): 

48 # Create the ndarray instance of our type, given the usual 

49 # ndarray input arguments. This will call the standard 

50 # ndarray constructor, but return an object of our type. 

51 # It also triggers a call to InfoArray.__array_finalize__ 

52 obj = super(SdynpyArray, subtype).__new__(subtype, shape, dtype, 

53 buffer, offset, strides, 

54 order) 

55 # Finally, we must return the newly created object: 

56 return obj 

57 

58 def __array_finalize__(self, obj): 

59 # ``self`` is a new object resulting from 

60 # ndarray.__new__(InfoArray, ...), therefore it only has 

61 # attributes that the ndarray.__new__ constructor gave it - 

62 # i.e. those of a standard ndarray. 

63 # 

64 # We could have got to the ndarray.__new__ call in 3 ways: 

65 # From an explicit constructor - e.g. InfoArray(): 

66 # obj is None 

67 # (we're in the middle of the InfoArray.__new__ 

68 # constructor, and self.info will be set when we return to 

69 # InfoArray.__new__) 

70 # if obj is None: 

71 # return 

72 # From view casting - e.g arr.view(InfoArray): 

73 # obj is arr 

74 # (type(obj) can be InfoArray) 

75 # From new-from-template - e.g infoarr[:3] 

76 # type(obj) is InfoArray 

77 # 

78 # Note that it is here, rather than in the __new__ method, 

79 # that we set the default value for 'info', because this 

80 # method sees all creation of default objects - with the 

81 # InfoArray.__new__ constructor, but also with 

82 # arr.view(InfoArray). 

83 # We do not need to return anything 

84 pass 

85 

86 # this method is called whenever you use a ufunc 

87 def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): 

88 f = { 

89 "reduce": ufunc.reduce, 

90 "accumulate": ufunc.accumulate, 

91 "reduceat": ufunc.reduceat, 

92 "outer": ufunc.outer, 

93 "at": ufunc.at, 

94 "__call__": ufunc, 

95 } 

96 # print('In ufunc\n ufunc: {:}\n method: {:}\n inputs: {:}\n kwargs: {:}\n'.format( 

97 # ufunc,method,inputs,kwargs)) 

98 # convert the inputs to np.ndarray to prevent recursion, call the function, then cast it back as CoordinateArray 

99 output = f[method](*(i.view(np.ndarray) for i in inputs), **kwargs).view(self.__class__) 

100 return output 

101 

102 def __array_function__(self, func, types, args, kwargs): 

103 # print('In Array Function\n func: {:}\n types: {:}\n args: {:}\n kwargs: {:}'.format( 

104 # func,types,args,kwargs)) 

105 output = super().__array_function__(func, types, args, kwargs) 

106 # print(' Output Type: {:}'.format(type(output))) 

107 # if isinstance(output,tuple): 

108 # print('Tuple Types: {:}'.format(type(val) for val in output)) 

109 # print('Output: {:}'.format(output)) 

110 if output is NotImplemented: 

111 return NotImplemented 

112 else: 

113 if isinstance(output, np.ndarray) and (self.dtype == output.dtype): 

114 return output.view(self.__class__) 

115 elif isinstance(output, np.ndarray): 

116 return output.view(np.ndarray) 

117 else: 

118 output_values = [] 

119 for val in output: 

120 # print(' Tuple Output Type: {:}'.format(type(val))) 

121 if isinstance(val, np.ndarray): 

122 # print(' Output dtypes {:},{:}'.format(self.dtype,val.dtype)) 

123 if self.dtype == val.dtype: 

124 # print(' Appending {:}'.format(self.__class__)) 

125 output_values.append(val.view(self.__class__)) 

126 else: 

127 # print(' Appending {:}'.format(np.ndarray)) 

128 output_values.append(val.view(np.ndarray)) 

129 else: 

130 output_values.append(val) 

131 return output_values 

132 

133 def __getattr__(self, attr): 

134 try: 

135 return self[attr] 

136 except ValueError: 

137 raise AttributeError("'{:}' object has no attribute '{:}'".format(self.__class__, attr)) 

138 

139 def __setattr__(self, attr, value): 

140 try: 

141 self[attr] = value 

142 except (ValueError, IndexError) as e: 

143 # # Check and make sure you don't have an attribute already with that 

144 # # name 

145 if attr in self.dtype.fields: 

146 # print('ERROR: Assignment to item failed, attempting to assign item to attribute!') 

147 raise e 

148 super().__setattr__(attr, value) 

149 

150 def __getitem__(self, key): 

151 # print('Key is type {:}'.format(type(key))) 

152 # print('Key is {:}'.format(key)) 

153 return_val = super().__getitem__(key) 

154 try: 

155 if isinstance(key, str) and (not isinstance(return_val, np.void)) and key in self.dtype.names: 

156 return_val = return_val.view(np.ndarray) 

157 if return_val.ndim == 0: 

158 return_val = return_val[()] 

159 except TypeError: 

160 pass 

161 if isinstance(return_val, np.void): 

162 return_val = np.asarray(return_val).view(self.__class__) 

163 # print('Returning a {:}'.format(type(return_val))) 

164 return return_val 

165 

166 def __setitem__(self, key, value): 

167 try: 

168 if key in self.dtype.fields: 

169 self[key][...] = value 

170 else: 

171 super().__setitem__(key, value) 

172 except TypeError: 

173 super().__setitem__(key, value) 

174 

175 def ndenumerate(self): 

176 """ 

177 Enumerates over all entries in the array 

178 

179 Yields 

180 ------ 

181 tuple 

182 indices corresponding to each entry in the array 

183 array 

184 entry in the array corresponding to the index 

185 """ 

186 for key, val in np.ndenumerate(self): 

187 yield (key, np.asarray(val).view(self.__class__)) 

188 

189 def __str__(self): 

190 return self.__repr__() 

191 

192 def __repr__(self): 

193 return 'Shape {:} {:} with fields {:}'.format(' x '.join(str(v) for v in self.shape), self.__class__.__name__, self.dtype.names) 

194 

195 def save(self, filename: str): 

196 """ 

197 Save the array to a numpy file 

198 

199 Parameters 

200 ---------- 

201 filename : str 

202 Filename that the array will be saved to. Will be appended with 

203 .npy if not specified in the filename 

204 

205 """ 

206 np.save(filename, self.view(np.ndarray)) 

207 

208 def assemble_mat_dict(self): 

209 """ 

210 Assembles a dictionary of fields 

211 

212 Returns 

213 ------- 

214 output_dict : dict 

215 A dictionary of contents of the file 

216 """ 

217 output_dict = {} 

218 for field in self.fields: 

219 val = self[field] 

220 if isinstance(val, SdynpyArray): 

221 val = val.assemble_mat_dict() 

222 else: 

223 val = np.ascontiguousarray(val) 

224 output_dict[field] = val 

225 return output_dict 

226 

227 def savemat(self, filename): 

228 """ 

229 Save array to a Matlab `*.mat` file. 

230 

231 Parameters 

232 ---------- 

233 filename : str 

234 Name of the file in which the data will be saved 

235 

236 Returns 

237 ------- 

238 None. 

239 

240 """ 

241 scipy_savemat(filename, self.assemble_mat_dict()) 

242 

243 @classmethod 

244 def load(cls, filename): 

245 """ 

246 Load in the specified file into a SDynPy array object 

247 

248 Parameters 

249 ---------- 

250 filename : str 

251 Filename specifying the file to load. If the filename has 

252 extension .unv or .uff, it will be loaded as a universal file. 

253 Otherwise, it will be loaded as a NumPy file. 

254 

255 Raises 

256 ------ 

257 AttributeError 

258 Raised if a unv file is loaded from a class that does not have a 

259 from_unv attribute defined. 

260 

261 Returns 

262 ------- 

263 cls 

264 SDynpy array of the appropriate type from the loaded file. 

265 

266 """ 

267 if filename[-4:].lower() in ['.unv', '.uff']: 

268 try: 

269 from ..fileio.sdynpy_uff import readunv 

270 unv_dict = readunv(filename) 

271 return cls.from_unv(unv_dict) 

272 except AttributeError: 

273 raise AttributeError('Class {:} has no from_unv attribute defined'.format(cls)) 

274 else: 

275 return np.load(filename, allow_pickle=True).view(cls) 

276 

277 def __eq__(self, other): 

278 if not isinstance(self, other.__class__): 

279 return NotImplemented 

280 equal_array = [] 

281 for field, (dtype, extra) in self.dtype.fields.items(): 

282 if dtype.kind == 'O': 

283 self_data = self[field] 

284 other_data = other[field] 

285 if self.ndim == 0: 

286 obj_arr = np.ndarray((), 'object') 

287 obj_arr[()] = self_data 

288 self_data = obj_arr 

289 if other.ndim == 0: 

290 obj_arr = np.ndarray((), 'object') 

291 obj_arr[()] = other_data 

292 other_data = obj_arr 

293 self_data, other_data = np.broadcast_arrays(self_data, other_data) 

294 truth_array = np.zeros(self_data.shape, dtype=bool) 

295 for key in np.ndindex(truth_array.shape): 

296 truth_array[key] = np.array_equal(self_data[key], other_data[key]) 

297 else: 

298 truth_array = self[field] == other[field] 

299 if len(dtype.shape) != 0: 

300 truth_array = np.all(truth_array, axis=tuple(-1 - np.arange(len(dtype.shape)))) 

301 equal_array.append(truth_array) 

302 return np.all(equal_array, axis=0) 

303 

304 def __ne__(self, other): 

305 return ~self.__eq__(other) 

306 

307 @property 

308 def fields(self): 

309 """Returns the fields of the structured array. 

310 

311 These fields can be accessed through attribute syntax.""" 

312 return self.dtype.names