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
« 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.
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.
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.
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.
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.
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"""
34import numpy as np
35from scipy.io import savemat as scipy_savemat
38class SdynpyArray(np.ndarray):
39 """Superclass of the core SDynPy objects
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
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
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
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
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))
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)
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
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)
175 def ndenumerate(self):
176 """
177 Enumerates over all entries in the array
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__))
189 def __str__(self):
190 return self.__repr__()
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)
195 def save(self, filename: str):
196 """
197 Save the array to a numpy file
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
205 """
206 np.save(filename, self.view(np.ndarray))
208 def assemble_mat_dict(self):
209 """
210 Assembles a dictionary of fields
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
227 def savemat(self, filename):
228 """
229 Save array to a Matlab `*.mat` file.
231 Parameters
232 ----------
233 filename : str
234 Name of the file in which the data will be saved
236 Returns
237 -------
238 None.
240 """
241 scipy_savemat(filename, self.assemble_mat_dict())
243 @classmethod
244 def load(cls, filename):
245 """
246 Load in the specified file into a SDynPy array object
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.
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.
261 Returns
262 -------
263 cls
264 SDynpy array of the appropriate type from the loaded file.
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)
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)
304 def __ne__(self, other):
305 return ~self.__eq__(other)
307 @property
308 def fields(self):
309 """Returns the fields of the structured array.
311 These fields can be accessed through attribute syntax."""
312 return self.dtype.names