codekingpro/portable-devtools
114k
1"""
2Collection of utilities to manipulate structured arrays.
3
4Most of these functions were initially implemented by John Hunter for
5matplotlib. They have been rewritten and extended for convenience.
6
7"""
8import itertools
9
10import numpy as np
11import numpy.ma as ma
12import numpy.ma.mrecords as mrec
13from numpy._core.overrides import array_function_dispatch
14from numpy.lib._iotools import _is_string_like
15
16__all__ = [
17 'append_fields', 'apply_along_fields', 'assign_fields_by_name',
18 'drop_fields', 'find_duplicates', 'flatten_descr',
19 'get_fieldstructure', 'get_names', 'get_names_flat',
20 'join_by', 'merge_arrays', 'rec_append_fields',
21 'rec_drop_fields', 'rec_join', 'recursive_fill_fields',
22 'rename_fields', 'repack_fields', 'require_fields',
23 'stack_arrays', 'structured_to_unstructured', 'unstructured_to_structured',
24 ]
25
26
27def _recursive_fill_fields_dispatcher(input, output):
28 return (input, output)
29
30
31@array_function_dispatch(_recursive_fill_fields_dispatcher)
32def recursive_fill_fields(input, output):
33 """
34 Fills fields from output with fields from input,
35 with support for nested structures.
36
37 Parameters
38 ----------
39 input : ndarray
40 Input array.
41 output : ndarray
42 Output array.
43
44 Notes
45 -----
46 * `output` should be at least the same size as `input`
47
48 Examples
49 --------
50 >>> import numpy as np
51 >>> from numpy.lib import recfunctions as rfn
52 >>> a = np.array([(1, 10.), (2, 20.)], dtype=[('A', np.int64), ('B', np.float64)])
53 >>> b = np.zeros((3,), dtype=a.dtype)
54 >>> rfn.recursive_fill_fields(a, b)
55 array([(1, 10.), (2, 20.), (0, 0.)], dtype=[('A', '<i8'), ('B', '<f8')])
56
57 """
58 newdtype = output.dtype
59 for field in newdtype.names:
60 try:
61 current = input[field]
62 except ValueError:
63 continue
64 if current.dtype.names is not None:
65 recursive_fill_fields(current, output[field])
66 else:
67 output[field][:len(current)] = current
68 return output
69
70
71def _get_fieldspec(dtype):
72 """
73 Produce a list of name/dtype pairs corresponding to the dtype fields
74
75 Similar to dtype.descr, but the second item of each tuple is a dtype, not a
76 string. As a result, this handles subarray dtypes
77
78 Can be passed to the dtype constructor to reconstruct the dtype, noting that
79 this (deliberately) discards field offsets.
80
81 Examples
82 --------
83 >>> import numpy as np
84 >>> dt = np.dtype([(('a', 'A'), np.int64), ('b', np.double, 3)])
85 >>> dt.descr
86 [(('a', 'A'), '<i8'), ('b', '<f8', (3,))]
87 >>> _get_fieldspec(dt)
88 [(('a', 'A'), dtype('int64')), ('b', dtype(('<f8', (3,))))]
89
90 """
91 if dtype.names is None:
92 # .descr returns a nameless field, so we should too
93 return [('', dtype)]
94 else:
95 fields = ((name, dtype.fields[name]) for name in dtype.names)
96 # keep any titles, if present
97 return [
98 (name if len(f) == 2 else (f[2], name), f[0])
99 for name, f in fields
100 ]
101
102
103def get_names(adtype):
104 """
105 Returns the field names of the input datatype as a tuple. Input datatype
106 must have fields otherwise error is raised.
107
108 Parameters
109 ----------
110 adtype : dtype
111 Input datatype
112
113 Examples
114 --------
115 >>> import numpy as np
116 >>> from numpy.lib import recfunctions as rfn
117 >>> rfn.get_names(np.empty((1,), dtype=[('A', int)]).dtype)
118 ('A',)
119 >>> rfn.get_names(np.empty((1,), dtype=[('A',int), ('B', float)]).dtype)
120 ('A', 'B')
121 >>> adtype = np.dtype([('a', int), ('b', [('ba', int), ('bb', int)])])
122 >>> rfn.get_names(adtype)
123 ('a', ('b', ('ba', 'bb')))
124 """
125 listnames = []
126 names = adtype.names
127 for name in names:
128 current = adtype[name]
129 if current.names is not None:
130 listnames.append((name, tuple(get_names(current))))
131 else:
132 listnames.append(name)
133 return tuple(listnames)
134
135
136def get_names_flat(adtype):
137 """
138 Returns the field names of the input datatype as a tuple. Input datatype
139 must have fields otherwise error is raised.
140 Nested structure are flattened beforehand.
141
142 Parameters
143 ----------
144 adtype : dtype
145 Input datatype
146
147 Examples
148 --------
149 >>> import numpy as np
150 >>> from numpy.lib import recfunctions as rfn
151 >>> rfn.get_names_flat(np.empty((1,), dtype=[('A', int)]).dtype) is None
152 False
153 >>> rfn.get_names_flat(np.empty((1,), dtype=[('A',int), ('B', str)]).dtype)
154 ('A', 'B')
155 >>> adtype = np.dtype([('a', int), ('b', [('ba', int), ('bb', int)])])
156 >>> rfn.get_names_flat(adtype)
157 ('a', 'b', 'ba', 'bb')
158 """
159 listnames = []
160 names = adtype.names
161 for name in names:
162 listnames.append(name)
163 current = adtype[name]
164 if current.names is not None:
165 listnames.extend(get_names_flat(current))
166 return tuple(listnames)
167
168
169def flatten_descr(ndtype):
170 """
171 Flatten a structured data-type description.
172
173 Examples
174 --------
175 >>> import numpy as np
176 >>> from numpy.lib import recfunctions as rfn
177 >>> ndtype = np.dtype([('a', '<i4'), ('b', [('ba', '<f8'), ('bb', '<i4')])])
178 >>> rfn.flatten_descr(ndtype)
179 (('a', dtype('int32')), ('ba', dtype('float64')), ('bb', dtype('int32')))
180
181 """
182 names = ndtype.names
183 if names is None:
184 return (('', ndtype),)
185 else:
186 descr = []
187 for field in names:
188 (typ, _) = ndtype.fields[field]
189 if typ.names is not None:
190 descr.extend(flatten_descr(typ))
191 else:
192 descr.append((field, typ))
193 return tuple(descr)
194
195
196def _zip_dtype(seqarrays, flatten=False):
197 newdtype = []
198 if flatten:
199 for a in seqarrays:
200 newdtype.extend(flatten_descr(a.dtype))
201 else:
202 for a in seqarrays:
203 current = a.dtype
204 if current.names is not None and len(current.names) == 1:
205 # special case - dtypes of 1 field are flattened
206 newdtype.extend(_get_fieldspec(current))
207 else:
208 newdtype.append(('', current))
209 return np.dtype(newdtype)
210
211
212def _zip_descr(seqarrays, flatten=False):
213 """
214 Combine the dtype description of a series of arrays.
215
216 Parameters
217 ----------
218 seqarrays : sequence of arrays
219 Sequence of arrays
220 flatten : {boolean}, optional
221 Whether to collapse nested descriptions.
222 """
223 return _zip_dtype(seqarrays, flatten=flatten).descr
224
225
226def get_fieldstructure(adtype, lastname=None, parents=None,):
227 """
228 Returns a dictionary with fields indexing lists of their parent fields.
229
230 This function is used to simplify access to fields nested in other fields.
231
232 Parameters
233 ----------
234 adtype : np.dtype
235 Input datatype
236 lastname : optional
237 Last processed field name (used internally during recursion).
238 parents : dictionary
239 Dictionary of parent fields (used internally during recursion).
240
241 Examples
242 --------
243 >>> import numpy as np
244 >>> from numpy.lib import recfunctions as rfn
245 >>> ndtype = np.dtype([('A', int),
246 ... ('B', [('BA', int),
247 ... ('BB', [('BBA', int), ('BBB', int)])])])
248 >>> rfn.get_fieldstructure(ndtype)
249 ... # XXX: possible regression, order of BBA and BBB is swapped
250 {'A': [], 'B': [], 'BA': ['B'], 'BB': ['B'], 'BBA': ['B', 'BB'], 'BBB': ['B', 'BB']}
251
252 """
253 if parents is None:
254 parents = {}
255 names = adtype.names
256 for name in names:
257 current = adtype[name]
258 if current.names is not None:
259 if lastname:
260 parents[name] = [lastname, ]
261 else:
262 parents[name] = []
263 parents.update(get_fieldstructure(current, name, parents))
264 else:
265 lastparent = list(parents.get(lastname, []) or [])
266 if lastparent:
267 lastparent.append(lastname)
268 elif lastname:
269 lastparent = [lastname, ]
270 parents[name] = lastparent or []
271 return parents
272
273
274def _izip_fields_flat(iterable):
275 """
276 Returns an iterator of concatenated fields from a sequence of arrays,
277 collapsing any nested structure.
278
279 """
280 for element in iterable:
281 if isinstance(element, np.void):
282 yield from _izip_fields_flat(tuple(element))
283 else:
284 yield element
285
286
287def _izip_fields(iterable):
288 """
289 Returns an iterator of concatenated fields from a sequence of arrays.
290
291 """
292 for element in iterable:
293 if (hasattr(element, '__iter__') and
294 not isinstance(element, str)):
295 yield from _izip_fields(element)
296 elif isinstance(element, np.void) and len(tuple(element)) == 1:
297 # this statement is the same from the previous expression
298 yield from _izip_fields(element)
299 else:
300 yield element
301
302
303def _izip_records(seqarrays, fill_value=None, flatten=True):
304 """
305 Returns an iterator of concatenated items from a sequence of arrays.
306
307 Parameters
308 ----------
309 seqarrays : sequence of arrays
310 Sequence of arrays.
311 fill_value : {None, integer}
312 Value used to pad shorter iterables.
313 flatten : {True, False},
314 Whether to
315 """
316
317 # Should we flatten the items, or just use a nested approach
318 if flatten:
319 zipfunc = _izip_fields_flat
320 else:
321 zipfunc = _izip_fields
322
323 for tup in itertools.zip_longest(*seqarrays, fillvalue=fill_value):
324 yield tuple(zipfunc(tup))
325
326
327def _fix_output(output, usemask=True, asrecarray=False):
328 """
329 Private function: return a recarray, a ndarray, a MaskedArray
330 or a MaskedRecords depending on the input parameters
331 """
332 if not isinstance(output, ma.MaskedArray):
333 usemask = False
334 if usemask:
335 if asrecarray:
336 output = output.view(mrec.MaskedRecords)
337 else:
338 output = ma.filled(output)
339 if asrecarray:
340 output = output.view(np.recarray)
341 return output
342
343
344def _fix_defaults(output, defaults=None):
345 """
346 Update the fill_value and masked data of `output`
347 from the default given in a dictionary defaults.
348 """
349 names = output.dtype.names
350 (data, mask, fill_value) = (output.data, output.mask, output.fill_value)
351 for (k, v) in (defaults or {}).items():
352 if k in names:
353 fill_value[k] = v
354 data[k][mask[k]] = v
355 return output
356
357
358def _merge_arrays_dispatcher(seqarrays, fill_value=None, flatten=None,
359 usemask=None, asrecarray=None):
360 return seqarrays
361
362
363@array_function_dispatch(_merge_arrays_dispatcher)
364def merge_arrays(seqarrays, fill_value=-1, flatten=False,
365 usemask=False, asrecarray=False):
366 """
367 Merge arrays field by field.
368
369 Parameters
370 ----------
371 seqarrays : sequence of ndarrays
372 Sequence of arrays
373 fill_value : {float}, optional
374 Filling value used to pad missing data on the shorter arrays.
375 flatten : {False, True}, optional
376 Whether to collapse nested fields.
377 usemask : {False, True}, optional
378 Whether to return a masked array or not.
379 asrecarray : {False, True}, optional
380 Whether to return a recarray (MaskedRecords) or not.
381
382 Examples
383 --------
384 >>> import numpy as np
385 >>> from numpy.lib import recfunctions as rfn
386 >>> rfn.merge_arrays((np.array([1, 2]), np.array([10., 20., 30.])))
387 array([( 1, 10.), ( 2, 20.), (-1, 30.)],
388 dtype=[('f0', '<i8'), ('f1', '<f8')])
389
390 >>> rfn.merge_arrays((np.array([1, 2], dtype=np.int64),
391 ... np.array([10., 20., 30.])), usemask=False)
392 array([(1, 10.0), (2, 20.0), (-1, 30.0)],
393 dtype=[('f0', '<i8'), ('f1', '<f8')])
394 >>> rfn.merge_arrays((np.array([1, 2]).view([('a', np.int64)]),
395 ... np.array([10., 20., 30.])),
396 ... usemask=False, asrecarray=True)
397 rec.array([( 1, 10.), ( 2, 20.), (-1, 30.)],
398 dtype=[('a', '<i8'), ('f1', '<f8')])
399
400 Notes
401 -----
402 * Without a mask, the missing value will be filled with something,
403 depending on what its corresponding type:
404
405 * ``-1`` for integers
406 * ``-1.0`` for floating point numbers
407 * ``'-'`` for characters
408 * ``'-1'`` for strings
409 * ``True`` for boolean values
410 * XXX: I just obtained these values empirically
411 """
412 # Only one item in the input sequence ?
413 if (len(seqarrays) == 1):
414 seqarrays = np.asanyarray(seqarrays[0])
415 # Do we have a single ndarray as input ?
416 if isinstance(seqarrays, (np.ndarray, np.void)):
417 seqdtype = seqarrays.dtype
418 # Make sure we have named fields
419 if seqdtype.names is None:
420 seqdtype = np.dtype([('', seqdtype)])
421 if not flatten or _zip_dtype((seqarrays,), flatten=True) == seqdtype:
422 # Minimal processing needed: just make sure everything's a-ok
423 seqarrays = seqarrays.ravel()
424 # Find what type of array we must return
425 if usemask:
426 if asrecarray:
427 seqtype = mrec.MaskedRecords
428 else:
429 seqtype = ma.MaskedArray
430 elif asrecarray:
431 seqtype = np.recarray
432 else:
433 seqtype = np.ndarray
434 return seqarrays.view(dtype=seqdtype, type=seqtype)
435 else:
436 seqarrays = (seqarrays,)
437 else:
438 # Make sure we have arrays in the input sequence
439 seqarrays = [np.asanyarray(_m) for _m in seqarrays]
440 # Find the sizes of the inputs and their maximum
441 sizes = tuple(a.size for a in seqarrays)
442 maxlength = max(sizes)
443 # Get the dtype of the output (flattening if needed)
444 newdtype = _zip_dtype(seqarrays, flatten=flatten)
445 # Initialize the sequences for data and mask
446 seqdata = []
447 seqmask = []
448 # If we expect some kind of MaskedArray, make a special loop.
449 if usemask:
450 for (a, n) in zip(seqarrays, sizes):
451 nbmissing = (maxlength - n)
452 # Get the data and mask
453 data = a.ravel().__array__()
454 mask = ma.getmaskarray(a).ravel()
455 # Get the filling value (if needed)
456 if nbmissing:
457 fval = mrec._check_fill_value(fill_value, a.dtype)
458 if isinstance(fval, (np.ndarray, np.void)):
459 if len(fval.dtype) == 1:
460 fval = fval.item()[0]
461 fmsk = True
462 else:
463 fval = np.array(fval, dtype=a.dtype, ndmin=1)
464 fmsk = np.ones((1,), dtype=mask.dtype)
465 else:
466 fval = None
467 fmsk = True
468 # Store an iterator padding the input to the expected length
469 seqdata.append(itertools.chain(data, [fval] * nbmissing))
470 seqmask.append(itertools.chain(mask, [fmsk] * nbmissing))
471 # Create an iterator for the data
472 data = tuple(_izip_records(seqdata, flatten=flatten))
473 output = ma.array(np.fromiter(data, dtype=newdtype, count=maxlength),
474 mask=list(_izip_records(seqmask, flatten=flatten)))
475 if asrecarray:
476 output = output.view(mrec.MaskedRecords)
477 else:
478 # Same as before, without the mask we don't need...
479 for (a, n) in zip(seqarrays, sizes):
480 nbmissing = (maxlength - n)
481 data = a.ravel().__array__()
482 if nbmissing:
483 fval = mrec._check_fill_value(fill_value, a.dtype)
484 if isinstance(fval, (np.ndarray, np.void)):
485 if len(fval.dtype) == 1:
486 fval = fval.item()[0]
487 else:
488 fval = np.array(fval, dtype=a.dtype, ndmin=1)
489 else:
490 fval = None
491 seqdata.append(itertools.chain(data, [fval] * nbmissing))
492 output = np.fromiter(tuple(_izip_records(seqdata, flatten=flatten)),
493 dtype=newdtype, count=maxlength)
494 if asrecarray:
495 output = output.view(np.recarray)
496 # And we're done...
497 return output
498
499
500def _drop_fields_dispatcher(base, drop_names, usemask=None, asrecarray=None):
501 return (base,)
502
503
504@array_function_dispatch(_drop_fields_dispatcher)
505def drop_fields(base, drop_names, usemask=True, asrecarray=False):
506 """
507 Return a new array with fields in `drop_names` dropped.
508
509 Nested fields are supported.
510
511 Parameters
512 ----------
513 base : array
514 Input array
515 drop_names : string or sequence
516 String or sequence of strings corresponding to the names of the
517 fields to drop.
518 usemask : {False, True}, optional
519 Whether to return a masked array or not.
520 asrecarray : string or sequence, optional
521 Whether to return a recarray or a mrecarray (`asrecarray=True`) or
522 a plain ndarray or masked array with flexible dtype. The default
523 is False.
524
525 Examples
526 --------
527 >>> import numpy as np
528 >>> from numpy.lib import recfunctions as rfn
529 >>> a = np.array([(1, (2, 3.0)), (4, (5, 6.0))],
530 ... dtype=[('a', np.int64), ('b', [('ba', np.double), ('bb', np.int64)])])
531 >>> rfn.drop_fields(a, 'a')
532 array([((2., 3),), ((5., 6),)],
533 dtype=[('b', [('ba', '<f8'), ('bb', '<i8')])])
534 >>> rfn.drop_fields(a, 'ba')
535 array([(1, (3,)), (4, (6,))], dtype=[('a', '<i8'), ('b', [('bb', '<i8')])])
536 >>> rfn.drop_fields(a, ['ba', 'bb'])
537 array([(1,), (4,)], dtype=[('a', '<i8')])
538 """
539 if _is_string_like(drop_names):
540 drop_names = [drop_names]
541 else:
542 drop_names = set(drop_names)
543
544 def _drop_descr(ndtype, drop_names):
545 names = ndtype.names
546 newdtype = []
547 for name in names:
548 current = ndtype[name]
549 if name in drop_names:
550 continue
551 if current.names is not None:
552 descr = _drop_descr(current, drop_names)
553 if descr:
554 newdtype.append((name, descr))
555 else:
556 newdtype.append((name, current))
557 return newdtype
558
559 newdtype = _drop_descr(base.dtype, drop_names)
560
561 output = np.empty(base.shape, dtype=newdtype)
562 output = recursive_fill_fields(base, output)
563 return _fix_output(output, usemask=usemask, asrecarray=asrecarray)
564
565
566def _keep_fields(base, keep_names, usemask=True, asrecarray=False):
567 """
568 Return a new array keeping only the fields in `keep_names`,
569 and preserving the order of those fields.
570
571 Parameters
572 ----------
573 base : array
574 Input array
575 keep_names : string or sequence
576 String or sequence of strings corresponding to the names of the
577 fields to keep. Order of the names will be preserved.
578 usemask : {False, True}, optional
579 Whether to return a masked array or not.
580 asrecarray : string or sequence, optional
581 Whether to return a recarray or a mrecarray (`asrecarray=True`) or
582 a plain ndarray or masked array with flexible dtype. The default
583 is False.
584 """
585 newdtype = [(n, base.dtype[n]) for n in keep_names]
586 output = np.empty(base.shape, dtype=newdtype)
587 output = recursive_fill_fields(base, output)
588 return _fix_output(output, usemask=usemask, asrecarray=asrecarray)
589
590
591def _rec_drop_fields_dispatcher(base, drop_names):
592 return (base,)
593
594
595@array_function_dispatch(_rec_drop_fields_dispatcher)
596def rec_drop_fields(base, drop_names):
597 """
598 Returns a new numpy.recarray with fields in `drop_names` dropped.
599 """
600 return drop_fields(base, drop_names, usemask=False, asrecarray=True)
601
602
603def _rename_fields_dispatcher(base, namemapper):
604 return (base,)
605
606
607@array_function_dispatch(_rename_fields_dispatcher)
608def rename_fields(base, namemapper):
609 """
610 Rename the fields from a flexible-datatype ndarray or recarray.
611
612 Nested fields are supported.
613
614 Parameters
615 ----------
616 base : ndarray
617 Input array whose fields must be modified.
618 namemapper : dictionary
619 Dictionary mapping old field names to their new version.
620
621 Examples
622 --------
623 >>> import numpy as np
624 >>> from numpy.lib import recfunctions as rfn
625 >>> a = np.array([(1, (2, [3.0, 30.])), (4, (5, [6.0, 60.]))],
626 ... dtype=[('a', int),('b', [('ba', float), ('bb', (float, 2))])])
627 >>> rfn.rename_fields(a, {'a':'A', 'bb':'BB'})
628 array([(1, (2., [ 3., 30.])), (4, (5., [ 6., 60.]))],
629 dtype=[('A', '<i8'), ('b', [('ba', '<f8'), ('BB', '<f8', (2,))])])
630
631 """
632 def _recursive_rename_fields(ndtype, namemapper):
633 newdtype = []
634 for name in ndtype.names:
635 newname = namemapper.get(name, name)
636 current = ndtype[name]
637 if current.names is not None:
638 newdtype.append(
639 (newname, _recursive_rename_fields(current, namemapper))
640 )
641 else:
642 newdtype.append((newname, current))
643 return newdtype
644 newdtype = _recursive_rename_fields(base.dtype, namemapper)
645 return base.view(newdtype)
646
647
648def _append_fields_dispatcher(base, names, data, dtypes=None,
649 fill_value=None, usemask=None, asrecarray=None):
650 yield base
651 yield from data
652
653
654@array_function_dispatch(_append_fields_dispatcher)
655def append_fields(base, names, data, dtypes=None,
656 fill_value=-1, usemask=True, asrecarray=False):
657 """
658 Add new fields to an existing array.
659
660 The names of the fields are given with the `names` arguments,
661 the corresponding values with the `data` arguments.
662 If a single field is appended, `names`, `data` and `dtypes` do not have
663 to be lists but just values.
664
665 Parameters
666 ----------
667 base : array
668 Input array to extend.
669 names : string, sequence
670 String or sequence of strings corresponding to the names
671 of the new fields.
672 data : array or sequence of arrays
673 Array or sequence of arrays storing the fields to add to the base.
674 dtypes : sequence of datatypes, optional
675 Datatype or sequence of datatypes.
676 If None, the datatypes are estimated from the `data`.
677 fill_value : {float}, optional
678 Filling value used to pad missing data on the shorter arrays.
679 usemask : {False, True}, optional
680 Whether to return a masked array or not.
681 asrecarray : {False, True}, optional
682 Whether to return a recarray (MaskedRecords) or not.
683
684 """
685 # Check the names
686 if isinstance(names, (tuple, list)):
687 if len(names) != len(data):
688 msg = "The number of arrays does not match the number of names"
689 raise ValueError(msg)
690 elif isinstance(names, str):
691 names = [names, ]
692 data = [data, ]
693 #
694 if dtypes is None:
695 data = [np.array(a, copy=None, subok=True) for a in data]
696 data = [a.view([(name, a.dtype)]) for (name, a) in zip(names, data)]
697 else:
698 if not isinstance(dtypes, (tuple, list)):
699 dtypes = [dtypes, ]
700 if len(data) != len(dtypes):
701 if len(dtypes) == 1:
702 dtypes = dtypes * len(data)
703 else:
704 msg = "The dtypes argument must be None, a dtype, or a list."
705 raise ValueError(msg)
706 data = [np.array(a, copy=None, subok=True, dtype=d).view([(n, d)])
707 for (a, n, d) in zip(data, names, dtypes)]
708 #
709 base = merge_arrays(base, usemask=usemask, fill_value=fill_value)
710 if len(data) > 1:
711 data = merge_arrays(data, flatten=True, usemask=usemask,
712 fill_value=fill_value)
713 else:
714 data = data.pop()
715 #
716 output = ma.masked_all(
717 max(len(base), len(data)),
718 dtype=_get_fieldspec(base.dtype) + _get_fieldspec(data.dtype))
719 output = recursive_fill_fields(base, output)
720 output = recursive_fill_fields(data, output)
721 #
722 return _fix_output(output, usemask=usemask, asrecarray=asrecarray)
723
724
725def _rec_append_fields_dispatcher(base, names, data, dtypes=None):
726 yield base
727 yield from data
728
729
730@array_function_dispatch(_rec_append_fields_dispatcher)
731def rec_append_fields(base, names, data, dtypes=None):
732 """
733 Add new fields to an existing array.
734
735 The names of the fields are given with the `names` arguments,
736 the corresponding values with the `data` arguments.
737 If a single field is appended, `names`, `data` and `dtypes` do not have
738 to be lists but just values.
739
740 Parameters
741 ----------
742 base : array
743 Input array to extend.
744 names : string, sequence
745 String or sequence of strings corresponding to the names
746 of the new fields.
747 data : array or sequence of arrays
748 Array or sequence of arrays storing the fields to add to the base.
749 dtypes : sequence of datatypes, optional
750 Datatype or sequence of datatypes.
751 If None, the datatypes are estimated from the `data`.
752
753 See Also
754 --------
755 append_fields
756
757 Returns
758 -------
759 appended_array : np.recarray
760 """
761 return append_fields(base, names, data=data, dtypes=dtypes,
762 asrecarray=True, usemask=False)
763
764
765def _repack_fields_dispatcher(a, align=None, recurse=None):
766 return (a,)
767
768
769@array_function_dispatch(_repack_fields_dispatcher)
770def repack_fields(a, align=False, recurse=False):
771 """
772 Re-pack the fields of a structured array or dtype in memory.
773
774 The memory layout of structured datatypes allows fields at arbitrary
775 byte offsets. This means the fields can be separated by padding bytes,
776 their offsets can be non-monotonically increasing, and they can overlap.
777
778 This method removes any overlaps and reorders the fields in memory so they
779 have increasing byte offsets, and adds or removes padding bytes depending
780 on the `align` option, which behaves like the `align` option to
781 `numpy.dtype`.
782
783 If `align=False`, this method produces a "packed" memory layout in which
784 each field starts at the byte the previous field ended, and any padding
785 bytes are removed.
786
787 If `align=True`, this methods produces an "aligned" memory layout in which
788 each field's offset is a multiple of its alignment, and the total itemsize
789 is a multiple of the largest alignment, by adding padding bytes as needed.
790
791 Parameters
792 ----------
793 a : ndarray or dtype
794 array or dtype for which to repack the fields.
795 align : boolean
796 If true, use an "aligned" memory layout, otherwise use a "packed" layout.
797 recurse : boolean
798 If True, also repack nested structures.
799
800 Returns
801 -------
802 repacked : ndarray or dtype
803 Copy of `a` with fields repacked, or `a` itself if no repacking was
804 needed.
805
806 Examples
807 --------
808 >>> import numpy as np
809
810 >>> from numpy.lib import recfunctions as rfn
811 >>> def print_offsets(d):
812 ... print("offsets:", [d.fields[name][1] for name in d.names])
813 ... print("itemsize:", d.itemsize)
814 ...
815 >>> dt = np.dtype('u1, <i8, <f8', align=True)
816 >>> dt
817 dtype({'names': ['f0', 'f1', 'f2'], 'formats': ['u1', '<i8', '<f8'], \
818'offsets': [0, 8, 16], 'itemsize': 24}, align=True)
819 >>> print_offsets(dt)
820 offsets: [0, 8, 16]
821 itemsize: 24
822 >>> packed_dt = rfn.repack_fields(dt)
823 >>> packed_dt
824 dtype([('f0', 'u1'), ('f1', '<i8'), ('f2', '<f8')])
825 >>> print_offsets(packed_dt)
826 offsets: [0, 1, 9]
827 itemsize: 17
828
829 """
830 if not isinstance(a, np.dtype):
831 dt = repack_fields(a.dtype, align=align, recurse=recurse)
832 return a.astype(dt, copy=False)
833
834 if a.names is None:
835 return a
836
837 fieldinfo = []
838 for name in a.names:
839 tup = a.fields[name]
840 if recurse:
841 fmt = repack_fields(tup[0], align=align, recurse=True)
842 else:
843 fmt = tup[0]
844
845 if len(tup) == 3:
846 name = (tup[2], name)
847
848 fieldinfo.append((name, fmt))
849
850 dt = np.dtype(fieldinfo, align=align)
851 return np.dtype((a.type, dt))
852
853def _get_fields_and_offsets(dt, offset=0):
854 """
855 Returns a flat list of (dtype, count, offset) tuples of all the
856 scalar fields in the dtype "dt", including nested fields, in left
857 to right order.
858 """
859
860 # counts up elements in subarrays, including nested subarrays, and returns
861 # base dtype and count
862 def count_elem(dt):
863 count = 1
864 while dt.shape != ():
865 for size in dt.shape:
866 count *= size
867 dt = dt.base
868 return dt, count
869
870 fields = []
871 for name in dt.names:
872 field = dt.fields[name]
873 f_dt, f_offset = field[0], field[1]
874 f_dt, n = count_elem(f_dt)
875
876 if f_dt.names is None:
877 fields.append((np.dtype((f_dt, (n,))), n, f_offset + offset))
878 else:
879 subfields = _get_fields_and_offsets(f_dt, f_offset + offset)
880 size = f_dt.itemsize
881
882 for i in range(n):
883 if i == 0:
884 # optimization: avoid list comprehension if no subarray
885 fields.extend(subfields)
886 else:
887 fields.extend([(d, c, o + i * size) for d, c, o in subfields])
888 return fields
889
890def _common_stride(offsets, counts, itemsize):
891 """
892 Returns the stride between the fields, or None if the stride is not
893 constant. The values in "counts" designate the lengths of
894 subarrays. Subarrays are treated as many contiguous fields, with
895 always positive stride.
896 """
897 if len(offsets) <= 1:
898 return itemsize
899
900 negative = offsets[1] < offsets[0] # negative stride
901 if negative:
902 # reverse, so offsets will be ascending
903 it = zip(reversed(offsets), reversed(counts))
904 else:
905 it = zip(offsets, counts)
906
907 prev_offset = None
908 stride = None
909 for offset, count in it:
910 if count != 1: # subarray: always c-contiguous
911 if negative:
912 return None # subarrays can never have a negative stride
913 if stride is None:
914 stride = itemsize
915 if stride != itemsize:
916 return None
917 end_offset = offset + (count - 1) * itemsize
918 else:
919 end_offset = offset
920
921 if prev_offset is not None:
922 new_stride = offset - prev_offset
923 if stride is None:
924 stride = new_stride
925 if stride != new_stride:
926 return None
927
928 prev_offset = end_offset
929
930 if negative:
931 return -stride
932 return stride
933
934
935def _structured_to_unstructured_dispatcher(arr, dtype=None, copy=None,
936 casting=None):
937 return (arr,)
938
939@array_function_dispatch(_structured_to_unstructured_dispatcher)
940def structured_to_unstructured(arr, dtype=None, copy=False, casting='unsafe'):
941 """
942 Converts an n-D structured array into an (n+1)-D unstructured array.
943
944 The new array will have a new last dimension equal in size to the
945 number of field-elements of the input array. If not supplied, the output
946 datatype is determined from the numpy type promotion rules applied to all
947 the field datatypes.
948
949 Nested fields, as well as each element of any subarray fields, all count
950 as a single field-elements.
951
952 Parameters
953 ----------
954 arr : ndarray
955 Structured array or dtype to convert. Cannot contain object datatype.
956 dtype : dtype, optional
957 The dtype of the output unstructured array.
958 copy : bool, optional
959 If true, always return a copy. If false, a view is returned if
960 possible, such as when the `dtype` and strides of the fields are
961 suitable and the array subtype is one of `numpy.ndarray`,
962 `numpy.recarray` or `numpy.memmap`.
963
964 .. versionchanged:: 1.25.0
965 A view can now be returned if the fields are separated by a
966 uniform stride.
967
968 casting : {'no', 'equiv', 'safe', 'same_kind', 'unsafe'}, optional
969 See casting argument of `numpy.ndarray.astype`. Controls what kind of
970 data casting may occur.
971
972 Returns
973 -------
974 unstructured : ndarray
975 Unstructured array with one more dimension.
976
977 Examples
978 --------
979 >>> import numpy as np
980
981 >>> from numpy.lib import recfunctions as rfn
982 >>> a = np.zeros(4, dtype=[('a', 'i4'), ('b', 'f4,u2'), ('c', 'f4', 2)])
983 >>> a
984 array([(0, (0., 0), [0., 0.]), (0, (0., 0), [0., 0.]),
985 (0, (0., 0), [0., 0.]), (0, (0., 0), [0., 0.])],
986 dtype=[('a', '<i4'), ('b', [('f0', '<f4'), ('f1', '<u2')]), ('c', '<f4', (2,))])
987 >>> rfn.structured_to_unstructured(a)
988 array([[0., 0., 0., 0., 0.],
989 [0., 0., 0., 0., 0.],
990 [0., 0., 0., 0., 0.],
991 [0., 0., 0., 0., 0.]])
992
993 >>> b = np.array([(1, 2, 5), (4, 5, 7), (7, 8 ,11), (10, 11, 12)],
994 ... dtype=[('x', 'i4'), ('y', 'f4'), ('z', 'f8')])
995 >>> np.mean(rfn.structured_to_unstructured(b[['x', 'z']]), axis=-1)
996 array([ 3. , 5.5, 9. , 11. ])
997
998 """ # noqa: E501
999 if arr.dtype.names is None:
1000 raise ValueError('arr must be a structured array')
1001
1002 fields = _get_fields_and_offsets(arr.dtype)
1003 n_fields = len(fields)
1004 if n_fields == 0 and dtype is None:
1005 raise ValueError("arr has no fields. Unable to guess dtype")
1006 elif n_fields == 0:
1007 # too many bugs elsewhere for this to work now
1008 raise NotImplementedError("arr with no fields is not supported")
1009
1010 dts, counts, offsets = zip(*fields)
1011 names = [f'f{n}' for n in range(n_fields)]
1012
1013 if dtype is None:
1014 out_dtype = np.result_type(*[dt.base for dt in dts])
1015 else:
1016 out_dtype = np.dtype(dtype)
1017
1018 # Use a series of views and casts to convert to an unstructured array:
1019
1020 # first view using flattened fields (doesn't work for object arrays)
1021 # Note: dts may include a shape for subarrays
1022 flattened_fields = np.dtype({'names': names,
1023 'formats': dts,
1024 'offsets': offsets,
1025 'itemsize': arr.dtype.itemsize})
1026 arr = arr.view(flattened_fields)
1027
1028 # we only allow a few types to be unstructured by manipulating the
1029 # strides, because we know it won't work with, for example, np.matrix nor
1030 # np.ma.MaskedArray.
1031 can_view = type(arr) in (np.ndarray, np.recarray, np.memmap)
1032 if (not copy) and can_view and all(dt.base == out_dtype for dt in dts):
1033 # all elements have the right dtype already; if they have a common
1034 # stride, we can just return a view
1035 common_stride = _common_stride(offsets, counts, out_dtype.itemsize)
1036 if common_stride is not None:
1037 wrap = arr.__array_wrap__
1038
1039 new_shape = arr.shape + (sum(counts), out_dtype.itemsize)
1040 new_strides = arr.strides + (abs(common_stride), 1)
1041
1042 arr = arr[..., np.newaxis].view(np.uint8) # view as bytes
1043 arr = arr[..., min(offsets):] # remove the leading unused data
1044 arr = np.lib.stride_tricks.as_strided(arr,
1045 new_shape,
1046 new_strides,
1047 subok=True)
1048
1049 # cast and drop the last dimension again
1050 arr = arr.view(out_dtype)[..., 0]
1051
1052 if common_stride < 0:
1053 arr = arr[..., ::-1] # reverse, if the stride was negative
1054 if type(arr) is not type(wrap.__self__):
1055 # Some types (e.g. recarray) turn into an ndarray along the
1056 # way, so we have to wrap it again in order to match the
1057 # behavior with copy=True.
1058 arr = wrap(arr)
1059 return arr
1060
1061 # next cast to a packed format with all fields converted to new dtype
1062 packed_fields = np.dtype({'names': names,
1063 'formats': [(out_dtype, dt.shape) for dt in dts]})
1064 arr = arr.astype(packed_fields, copy=copy, casting=casting)
1065
1066 # finally is it safe to view the packed fields as the unstructured type
1067 return arr.view((out_dtype, (sum(counts),)))
1068
1069
1070def _unstructured_to_structured_dispatcher(arr, dtype=None, names=None,
1071 align=None, copy=None, casting=None):
1072 return (arr,)
1073
1074@array_function_dispatch(_unstructured_to_structured_dispatcher)
1075def unstructured_to_structured(arr, dtype=None, names=None, align=False,
1076 copy=False, casting='unsafe'):
1077 """
1078 Converts an n-D unstructured array into an (n-1)-D structured array.
1079
1080 The last dimension of the input array is converted into a structure, with
1081 number of field-elements equal to the size of the last dimension of the
1082 input array. By default all output fields have the input array's dtype, but
1083 an output structured dtype with an equal number of fields-elements can be
1084 supplied instead.
1085
1086 Nested fields, as well as each element of any subarray fields, all count
1087 towards the number of field-elements.
1088
1089 Parameters
1090 ----------
1091 arr : ndarray
1092 Unstructured array or dtype to convert.
1093 dtype : dtype, optional
1094 The structured dtype of the output array
1095 names : list of strings, optional
1096 If dtype is not supplied, this specifies the field names for the output
1097 dtype, in order. The field dtypes will be the same as the input array.
1098 align : boolean, optional
1099 Whether to create an aligned memory layout.
1100 copy : bool, optional
1101 See copy argument to `numpy.ndarray.astype`. If true, always return a
1102 copy. If false, and `dtype` requirements are satisfied, a view is
1103 returned.
1104 casting : {'no', 'equiv', 'safe', 'same_kind', 'unsafe'}, optional
1105 See casting argument of `numpy.ndarray.astype`. Controls what kind of
1106 data casting may occur.
1107
1108 Returns
1109 -------
1110 structured : ndarray
1111 Structured array with fewer dimensions.
1112
1113 Examples
1114 --------
1115 >>> import numpy as np
1116
1117 >>> from numpy.lib import recfunctions as rfn
1118 >>> dt = np.dtype([('a', 'i4'), ('b', 'f4,u2'), ('c', 'f4', 2)])
1119 >>> a = np.arange(20).reshape((4,5))
1120 >>> a
1121 array([[ 0, 1, 2, 3, 4],
1122 [ 5, 6, 7, 8, 9],
1123 [10, 11, 12, 13, 14],
1124 [15, 16, 17, 18, 19]])
1125 >>> rfn.unstructured_to_structured(a, dt)
1126 array([( 0, ( 1., 2), [ 3., 4.]), ( 5, ( 6., 7), [ 8., 9.]),
1127 (10, (11., 12), [13., 14.]), (15, (16., 17), [18., 19.])],
1128 dtype=[('a', '<i4'), ('b', [('f0', '<f4'), ('f1', '<u2')]), ('c', '<f4', (2,))])
1129
1130 """ # noqa: E501
1131 if arr.shape == ():
1132 raise ValueError('arr must have at least one dimension')
1133 n_elem = arr.shape[-1]
1134 if n_elem == 0:
1135 # too many bugs elsewhere for this to work now
1136 raise NotImplementedError("last axis with size 0 is not supported")
1137
1138 if dtype is None:
1139 if names is None:
1140 names = [f'f{n}' for n in range(n_elem)]
1141 out_dtype = np.dtype([(n, arr.dtype) for n in names], align=align)
1142 fields = _get_fields_and_offsets(out_dtype)
1143 dts, counts, offsets = zip(*fields)
1144 else:
1145 if names is not None:
1146 raise ValueError("don't supply both dtype and names")
1147 # if dtype is the args of np.dtype, construct it
1148 dtype = np.dtype(dtype)
1149 # sanity check of the input dtype
1150 fields = _get_fields_and_offsets(dtype)
1151 if len(fields) == 0:
1152 dts, counts, offsets = [], [], []
1153 else:
1154 dts, counts, offsets = zip(*fields)
1155
1156 if n_elem != sum(counts):
1157 raise ValueError('The length of the last dimension of arr must '
1158 'be equal to the number of fields in dtype')
1159 out_dtype = dtype
1160 if align and not out_dtype.isalignedstruct:
1161 raise ValueError("align was True but dtype is not aligned")
1162
1163 names = [f'f{n}' for n in range(len(fields))]
1164
1165 # Use a series of views and casts to convert to a structured array:
1166
1167 # first view as a packed structured array of one dtype
1168 packed_fields = np.dtype({'names': names,
1169 'formats': [(arr.dtype, dt.shape) for dt in dts]})
1170 arr = np.ascontiguousarray(arr).view(packed_fields)
1171
1172 # next cast to an unpacked but flattened format with varied dtypes
1173 flattened_fields = np.dtype({'names': names,
1174 'formats': dts,
1175 'offsets': offsets,
1176 'itemsize': out_dtype.itemsize})
1177 arr = arr.astype(flattened_fields, copy=copy, casting=casting)
1178
1179 # finally view as the final nested dtype and remove the last axis
1180 return arr.view(out_dtype)[..., 0]
1181
1182def _apply_along_fields_dispatcher(func, arr):
1183 return (arr,)
1184
1185@array_function_dispatch(_apply_along_fields_dispatcher)
1186def apply_along_fields(func, arr):
1187 """
1188 Apply function 'func' as a reduction across fields of a structured array.
1189
1190 This is similar to `numpy.apply_along_axis`, but treats the fields of a
1191 structured array as an extra axis. The fields are all first cast to a
1192 common type following the type-promotion rules from `numpy.result_type`
1193 applied to the field's dtypes.
1194
1195 Parameters
1196 ----------
1197 func : function
1198 Function to apply on the "field" dimension. This function must
1199 support an `axis` argument, like `numpy.mean`, `numpy.sum`, etc.
1200 arr : ndarray
