Coverage for src / sdynpy / core / sdynpy_coordinate.py: 92%
144 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"""
2Defines the CoordinateArray, which specifies arrays of node numbers and directions.
4Coordinates in SDynPy are used to define degrees of freedom. These consist of
5a node number (which corresponds to a node in a SDynPy Geometry object) and a
6direction (which corresponds to the local displacement coordinate system of
7that node in the SDynPy Geometry object). Directions are the translations or
8rotations about the principal axis, and can be positive or negative. The
9direction can also be empty for non-directional data.
10"""
12"""
13Copyright 2022 National Technology & Engineering Solutions of Sandia,
14LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S.
15Government retains certain rights in this software.
17This program is free software: you can redistribute it and/or modify
18it under the terms of the GNU General Public License as published by
19the Free Software Foundation, either version 3 of the License, or
20(at your option) any later version.
22This program is distributed in the hope that it will be useful,
23but WITHOUT ANY WARRANTY; without even the implied warranty of
24MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25GNU General Public License for more details.
27You should have received a copy of the GNU General Public License
28along with this program. If not, see <https://www.gnu.org/licenses/>.
29"""
31import numpy as np
32from .sdynpy_array import SdynpyArray
33import warnings
35# This maps direction integers to their string counterparts
36_string_map = {1: 'X+', 2: 'Y+', 3: 'Z+', 4: 'RX+', 5: 'RY+', 6: 'RZ+',
37 -1: 'X-', -2: 'Y-', -3: 'Z-', -4: 'RX-', -5: 'RY-', -6: 'RZ-',
38 0: ''}
40# This maps direction strings to their integer counterparts
41_direction_map = {'X+': 1, 'X': 1, '+X': 1,
42 'Y+': 2, 'Y': 2, '+Y': 2,
43 'Z+': 3, 'Z': 3, '+Z': 3,
44 'RX+': 4, 'RX': 4, '+RX': 4,
45 'RY+': 5, 'RY': 5, '+RY': 5,
46 'RZ+': 6, 'RZ': 6, '+RZ': 6,
47 'X-': -1, '-X': -1,
48 'Y-': -2, '-Y': -2,
49 'Z-': -3, '-Z': -3,
50 'RX-': -4, '-RX': -4,
51 'RY-': -5, '-RY': -5,
52 'RZ-': -6, '-RZ': -6,
53 '': 0}
55_map_direction_string_array = np.vectorize(_string_map.get)
56_map_direction_array = np.vectorize(_direction_map.get)
59def parse_coordinate_string(coordinate: str):
60 """
61 Parse coordinate string into node and direction integers.
63 Parameters
64 ----------
65 coordinate : str
66 String representation of a coordinate, e.g. '101X+'
68 Returns
69 -------
70 node : int
71 Integer representing the node number
72 direction : int
73 Integer representing the direction, 'X+' = 1, 'Y+' = 2, 'Z+' = 3,
74 'X-' = -1, 'Y-' = -2, 'Z-' = -3
76 """
77 try:
78 node = int(''.join(v for v in coordinate if v in '0123456789'))
79 except ValueError:
80 warnings.warn('Node ID {:} is not Valid. Defaulting to 0.'.format(''.join(v for v in coordinate if v in '0123456789')))
81 node = 0
82 try:
83 direction = _direction_map[''.join(v for v in coordinate if not v in '0123456789')]
84 except KeyError:
85 warnings.warn('Direction {:} is not Valid. Defaulting to no direction.'.format(''.join(v for v in coordinate if not v in '0123456789')))
86 direction = 0
87 return node, direction
90parse_coordinate_string_array = np.vectorize(parse_coordinate_string, otypes=('int64', 'int64'))
93def create_coordinate_string(node: int, direction: int):
94 """
95 Create a string from node and directions integers
97 Parameters
98 ----------
99 node : int
100 Node number
101 direction : int
102 Integer representing the direction, 'X+' = 1, 'Y+' = 2, 'Z+' = 3,
103 'X-' = -1, 'Y-' = -2, 'Z-' = -3
105 Returns
106 -------
107 str
108 String representation of the coordinate, e.g. '101X+'
110 """
111 return str(node) + _string_map[direction]
114create_coordinate_string_array = np.vectorize(create_coordinate_string, otypes=['<U4'])
117class CoordinateArray(SdynpyArray):
118 """Coordinate information specifying Degrees of Freedom (e.g. 101X+).
120 Use the coordinate_array helper function to create the array.
121 """
123 data_dtype = [('node', 'uint64'), ('direction', 'int8')]
124 """Datatype for the underlying numpy structured array"""
126 def __new__(subtype, shape, buffer=None, offset=0,
127 strides=None, order=None):
128 # Create the ndarray instance of our type, given the usual
129 # ndarray input arguments. This will call the standard
130 # ndarray constructor, but return an object of our type.
131 # It also triggers a call to InfoArray.__array_finalize__
132 obj = super(CoordinateArray, subtype).__new__(subtype, shape, CoordinateArray.data_dtype,
133 buffer, offset, strides,
134 order)
135 # Finally, we must return the newly created object:
136 return obj
138 def string_array(self):
139 """
140 Returns a string array representation of the coordinate array
142 Returns
143 -------
144 np.ndarray
145 ndarray of strings representing the CoordinateArray
146 """
147 return create_coordinate_string_array(self.node, self.direction)
149 def direction_string_array(self):
150 """
151 Returns a string array representation of the direction
153 Returns
154 -------
155 np.ndarray
156 ndarray of direction strings representing the CoordinateArray
157 """
158 return _map_direction_string_array(self.direction)
160 def __repr__(self):
161 return 'coordinate_array(string_array=\n' + repr(self.string_array()) + ')'
163 def __str__(self):
164 return str(self.string_array())
166 def __eq__(self, value):
167 value = np.array(value)
168# # A string
169 if np.issubdtype(value.dtype, np.character):
170 value = coordinate_array(string_array=value)
171 if value.dtype.names is None:
172 node_logical = self.node == value[..., 0]
173 direction_logical = self.direction == value[..., 1]
174 else:
175 node_logical = self.node == value['node']
176 direction_logical = self.direction == value['direction']
177 return np.logical_and(node_logical, direction_logical)
179 def __ne__(self, value):
180 return ~self.__eq__(value)
182 def __abs__(self):
183 abs_coord = self.copy()
184 abs_coord.direction = abs(abs_coord.direction)
185 return abs_coord
187 def __neg__(self):
188 neg_coord = self.copy()
189 neg_coord.direction = -neg_coord.direction
190 return neg_coord
192 def __pos__(self):
193 pos_coord = self.copy()
194 pos_coord.direction = +pos_coord.direction
195 return pos_coord
197 def abs(self):
198 """Returns a coordinate array with direction signs flipped positive"""
199 return self.__abs__()
201 def sign(self):
202 """Returns the sign on the directions of the CoordinateArray"""
203 out = np.ones(self.shape)
204 out[self.direction < 0] = -1
205 return out
207 def local_direction(self):
208 """
209 Returns a local direction array
211 Returns
212 -------
213 local_direction_array : np.ndarray
214 Returns a (...,3) array where ... is the dimension of the
215 CoordinateArray. The (...,0), (...,1), and (...,2) indices
216 represent the x,y,z direction of the local coordinate direction.
217 For example, a CoordinateArray with direction X- would return
218 [-1,0,0].
220 """
221 output = np.zeros(self.shape + (3,))
222 signs = self.sign()
223 indices = abs(self.direction) - 1
224 if self.ndim > 0:
225 indices[indices > 2] -= 3
226 else:
227 if indices > 2:
228 indices -= 3
229 for key, index in np.ndenumerate(indices):
230 if index != -1:
231 output[key + (index,)] = signs[key]
232 return output
234 def offset_node_ids(self, offset_value):
235 """
236 Returns a copy of the CoordinateArray with the node IDs offset
238 Parameters
239 ----------
240 offset_value : int
241 The value to offset the node IDs by.
243 Returns
244 -------
245 CoordinateArray
247 """
248 output = self.copy()
249 output.node += offset_value
250 return output
252 def find_indices(self,coordinate):
253 """Finds the indices and signs of the specified coordinates
255 Parameters
256 ----------
257 coordinate : CoordinateArray
258 A CoordinateArray containing the coordinates to find.
260 Returns
261 -------
262 indices : tuple of ndarray
263 An tuple of arrays of indices into this CoordinateArray for each entry in
264 `coordinate`, which can be used directly to index this CoordinateArray.
265 signs : ndarray
266 An array of sign flips between this CoordinateArray at the indices in
267 `indices` for each entry in `coordinate`
269 Raises
270 ------
271 ValueError
272 If more than one coordinate matches, or no coordinate matches.
273 """
274 indices = np.zeros((self.ndim,)+coordinate.shape,dtype=int)
275 signs = np.zeros(coordinate.shape,dtype=int)
276 for check_index,coord in coordinate.ndenumerate():
277 index = np.array(np.where(abs(self)==abs(coord)))
278 if index.shape[-1] == 0:
279 raise ValueError(f'Coordinate {coordinate} not found in {self}')
280 if index.shape[-1] > 1:
281 raise ValueError(f'Coordinate {coordinate} found {index.shape[-1]} times in {self}')
282 indices[(slice(None),)+check_index] = index[...,0]
283 signs[check_index] = self[tuple(index)].sign()*coord.sign()
284 return tuple(indices),signs
286 @classmethod
287 def from_matlab_cellstr(cls, cellstr_data):
288 """
289 Creates a CoordinateArray from a matlab cellstring object loaded from
290 scipy.io.loadmat
292 Parameters
293 ----------
294 cellstr_data : np.ndarray
295 Dictionary entry corresponding to a cell string variable in a mat
296 file loaded from scipy.io.loadmat
298 Returns
299 -------
300 CoordinateArray
301 CoordinateArray built from the provided cell string array
303 """
304 str_array = np.empty(cellstr_data.shape, dtype=object)
305 for key, val in np.ndenumerate(cellstr_data):
306 str_array[key] = val[0]
307 return coordinate_array(string_array=str_array)
309 @classmethod
310 def from_nodelist(cls, nodes, directions=[1, 2, 3], flatten=True):
311 """
312 Returns a coordinate array with a set of nodes with a set of directions
314 Parameters
315 ----------
316 nodes : iterable
317 A list of nodes to create degrees of freedom at
318 directions : iterable, optional
319 A list of directions to create for each node. The default is [1,2,3],
320 which provides the three positive translations (X+, Y+, Z+).
321 flatten : bool, optional
322 Specifies that the array should be flattened prior to output. The
323 default is True. If False, the output will have a dimension one
324 larger than the input node list due to the added direction
325 dimension.
327 Returns
328 -------
329 coordinate_array : CoordinateArray
330 Array of coordinates with each specified direction defined at each
331 node. If flatten is false, this array will have shape
332 nodes.shape + directions.shape. Otherwise, this array will have
333 shape (nodes.size*directions.size,)
335 """
336 ca = coordinate_array(np.array(nodes)[..., np.newaxis], np.array(directions))
337 if flatten:
338 return ca.flatten()
339 else:
340 return ca
343def coordinate_array(node=None, direction=None,
344 structured_array=None,
345 string_array=None, force_broadcast=False):
346 """
347 Creates a coordinate array that specify degrees of freedom.
349 Creates an array of coordinates that specify degrees of freedom in a test
350 or analysis. Coordinate arrays can be created using a numpy structured
351 array or two arrays for node and direction. Multidimensional arrays can
352 be used.
354 Parameters
355 ----------
356 node : ndarray
357 Integer array corresponding to the node ids of the coordinates. Input
358 will be cast to an integer (i.e. 2.0 -> 2, 1.9 -> 1)
359 direction : ndarray
360 Direction corresponding to the coordinate. If a string is passed, it
361 must consist of a direction (RX, RY, RZ, X, Y, Z) and whether or not it
362 is positive or negative (+ or -). If no positive or negative value is
363 given, then positive will be assumed.
364 structured_array : ndarray (structured)
365 Alternatively to node and direction, a single numpy structured array
366 can be passed, which should have names ['node','direction']
367 string_array : ndarray
368 Alternatively to node and direction, a single numpy string array can
369 be passed into the function, which will be parsed to create the
370 data.
371 force_broadcast : bool, optional
372 Return all combinations of nodes and directions regardless of their
373 shapes. This will return a flattened array
375 Returns
376 -------
377 coordinate_array : CoordinateArray
379 """
380 if structured_array is not None:
381 try:
382 node = structured_array['node']
383 direction = structured_array['direction']
384 except (ValueError, TypeError):
385 raise ValueError(
386 'structured_array must be numpy.ndarray with dtype names "node" and "direction"')
387 elif string_array is not None:
388 string_array = np.array(string_array)
389 node, direction = parse_coordinate_string_array(string_array)
390 else:
391 node = np.array(node)
392 direction = np.array(direction)
393 if force_broadcast:
394 node = np.unique(node)
395 direction = np.unique(direction)
396 bc_direction = np.tile(direction, node.size)
397 bc_node = np.repeat(node, direction.size)
398 else:
399 try:
400 bc_node, bc_direction = np.broadcast_arrays(node, direction)
401 except ValueError:
402 raise ValueError('node and direction should be broadcastable to the same shape (node: {:}, direction: {:})'.format(
403 node.shape, direction.shape))
405 # Create the coordinate array
406 coord_array = CoordinateArray(bc_node.shape)
407 coord_array.node = bc_node
408 if not np.issubdtype(direction.dtype.type, np.integer):
409 bc_direction = _map_direction_array(bc_direction)
410 coord_array.direction = bc_direction
412 return coord_array
415def outer_product(*args):
416 """
417 Returns a CoordinateArray consisting of all combinations of the provided
418 CoordinateArrays
420 Parameters
421 ----------
422 *args : CoordinateArray
423 CoordinateArrays to combine into a single CoordinateArray
425 Returns
426 -------
427 CoordinateArray
428 CoordinateArray consisting of combinations of provided CoordinateArrays
430 """
431 ndims = len(args) + 1
432 expanded_coord_array = []
433 for i, array in enumerate(args):
434 index = tuple([Ellipsis]+[slice(None) if i == j else np.newaxis for j in range(ndims)])
435 expanded_coord_array.append(array[index])
436 return np.concatenate(np.broadcast_arrays(*expanded_coord_array), axis=-1)
439def from_matlab_cellstr(cellstr_data):
440 """
441 Creates a CoordinateArray from a matlab cellstring object loaded from
442 scipy.io.loadmat
444 Parameters
445 ----------
446 cellstr_data : np.ndarray
447 Dictionary entry corresponding to a cell string variable in a mat
448 file loaded from scipy.io.loadmat
450 Returns
451 -------
452 CoordinateArray
453 CoordinateArray built from the provided cell string array
455 """
456 return CoordinateArray.from_matlab_cellstr(cellstr_data)
459load = CoordinateArray.load
460from_nodelist = CoordinateArray.from_nodelist