Coverage for src/lsqfitgp/_array.py: 93%
408 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-04 11:43 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-04 11:43 +0000
1# lsqfitgp/_array.py
2#
3# Copyright (c) 2020, 2022, 2023, 2024, 2026, Giacomo Petrillo
4#
5# This file is part of lsqfitgp.
6#
7# lsqfitgp is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# lsqfitgp is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with lsqfitgp. If not, see <http://www.gnu.org/licenses/>.
20import math 1cba
21import textwrap 1cba
23import jax 1cba
24import numpy 1cba
25from jax import numpy as jnp 1cba
26from jax import tree, tree_util 1cba
27from numpy.lib import recfunctions 1cba
30@tree_util.register_pytree_node_class 1cba
31class StructuredArray: 1cba
32 """ 1cba
33 JAX-friendly imitation of a numpy structured array.
35 It behaves like a read-only numpy structured array, and you can create
36 a copy with a modified field with a jax-like syntax.
38 Parameters
39 ----------
40 array : numpy array, StructuredArray
41 A structured array. An array qualifies as structured if
42 ``array.dtype.names is not None``.
44 Notes
45 -----
46 The StructuredArray is a readonly view on the input array. When you
47 change the content of a field of the StructuredArray, however, the
48 reference to the original array for that field is lost.
50 Examples
51 --------
52 >>> a = numpy.empty(3, dtype=[('f', float), ('g', float)])
53 >>> a = StructuredArray(a)
54 >>> a = a.at['f'].set(numpy.arange(3))
55 ... # is equivalent to a['f'] = numpy.arange(3)
56 """
58 @classmethod 1cba
59 def _readonlyview_wrapifstructured(cls, x): 1cba
60 if x.dtype.names is not None: 1cba
61 x = cls(x) 1ba
62 if isinstance(x, numpy.ndarray): 1cba
63 x = x.view() 1cba
64 x.flags.writeable = False 1cba
65 # jax arrays and StructuredArrays are already readonly
66 return x 1cba
68 @classmethod 1cba
69 def _array(cls, s, t, d, *, check=True): 1cba
70 """
71 Create a new StructuredArray.
73 All methods and functions that create a new StructuredArray object
74 should use this method.
76 Parameters
77 ----------
78 s : tuple or None
79 The shape. If None, it is determined automatically from the arrays.
80 t : dtype or None
81 The dtype of the array. If None, it is determined automatically
82 (before the shape).
83 d : dict str -> array
84 The _dict of the array. The arrays, if structured, must be already
85 StructuredArrays. The order of the keys must match the order of the
86 fields.
87 check : bool
88 If True (default), check the passed values are consistent.
90 Return
91 ------
92 out : StructuredArray
93 A new StructuredArray object.
94 """
95 if t is None: 1cba
96 # infer the data type from the arrays in the dictionary
97 ndim = min((x.ndim for x in d.values()), default=None) 1ba
98 t = numpy.dtype([(name, x.dtype, x.shape[ndim:]) for name, x in d.items()]) 1ba
100 # remove offset info since this is actually a columnar format
101 t = recfunctions.repack_fields(t, align=False, recurse=True) 1cba
103 if s is None: 1cba
104 # infer the shape from the arrays in the dictionary
105 assert d, 'can not infer array shape with no fields' 1cba
106 f = t.names[0] 1cba
107 a = d[f] 1cba
108 s = a.shape[: a.ndim - t[0].ndim] 1cba
110 if check: 110 ↛ 119line 110 didn't jump to line 119 because the condition on line 110 was always true1cba
111 assert len(t) == len(d) 1cba
112 assert t.names == tuple(d.keys()) 1cba
113 assert all( 1cba
114 x.dtype == t[f].base and x.ndim >= t[f].ndim for f, x in d.items()
115 )
116 shapes = [x.shape[: x.ndim - t[f].ndim] for f, x in d.items()] 1cba
117 assert all(s == s1 for s1 in shapes) 1cba
119 out = super().__new__(cls) 1cba
120 out.shape = s 1cba
121 out.dtype = t 1cba
122 out._dict = d 1cba
124 return out 1cba
126 def __new__(cls, array): 1cba
127 if isinstance(array, cls): 1cba
128 return array 1cba
129 d = { 1cba
130 name: cls._readonlyview_wrapifstructured(array[name])
131 for name in array.dtype.names
132 }
133 return cls._array(array.shape, array.dtype, d) 1cba
135 @classmethod 1cba
136 def from_dataframe(cls, df): 1cba
137 """
138 Make a StructuredArray from a DataFrame.
140 Data is not copied if not necessary.
141 """
142 d = { 1ba
143 col: cls._readonlyview_wrapifstructured(df[col].to_numpy())
144 for col in df.columns
145 }
146 return cls._array(None, None, d) 1ba
148 @classmethod 1cba
149 def from_dict(cls, mapping): 1cba
150 """Make a StructuredArray from a dictionary of arrays. Data is not copied."""
151 d = { 1ba
152 name: cls._readonlyview_wrapifstructured(value)
153 for name, value in mapping.items()
154 }
155 return cls._array(None, None, d) 1ba
157 @property 1cba
158 def size(self): 1cba
159 return math.prod(self.shape) 1ba
161 @property 1cba
162 def ndim(self): 1cba
163 return len(self.shape) 1ba
165 @property 1cba
166 def nbytes(self): 1cba
167 return sum(x.nbytes for x in self._dict.values())
169 @property 1cba
170 def T(self): 1cba
171 if self.ndim < 2: 1a
172 return self 1a
173 return self.swapaxes(self.ndim - 2, self.ndim - 1) 1a
175 def swapaxes(self, i, j): 1cba
176 shape = jax.eval_shape(lambda: jnp.empty(self.shape).swapaxes(i, j)).shape 1a
177 d = {k: v.swapaxes(i, j) for k, v in self._dict.items()} 1a
178 return self._array(shape, self.dtype, d) 1a
180 def __len__(self): 1cba
181 if self.shape: 1ba
182 return self.shape[0] 1ba
183 else:
184 msg = 'len() of unsized object' 1a
185 raise TypeError(msg) 1a
187 def __getitem__(self, key): 1cba
188 if isinstance(key, str): 1cba
189 return self._dict[key] 1cba
190 elif isinstance(key, list) and key and all(isinstance(k, str) for k in key): 1cba
191 d = {name: self._dict[name] for name in key} 1a
192 return self._array(self.shape, self.dtype[key], d) 1a
193 else:
194 d = { 1cba
195 name: x[
196 (key if isinstance(key, tuple) else (key,))
197 + (slice(None),) * self.dtype[name].ndim
198 ]
199 for name, x in self._dict.items()
200 }
201 shape = jax.eval_shape(lambda: jnp.empty(self.shape)[key]).shape 1cba
202 return self._array(shape, self.dtype, d) 1cba
204 @property 1cba
205 def at(self): 1cba
206 return self._Getter(self) 1cba
208 class _Getter: 1cba
209 def __init__(self, array): 1cba
210 self.array = array 1cba
212 def __getitem__(self, key): 1cba
213 if key not in self.array.dtype.names: 213 ↛ 214line 213 didn't jump to line 214 because the condition on line 213 was never true1cba
214 raise KeyError(key)
215 return self.Setter(self.array, key) 1cba
217 class Setter: 1cba
218 def __init__(self, array, key, parent=None): 1cba
219 self.array = array 1cba
220 self.key = key 1cba
221 self.parent = parent 1cba
223 def __getitem__(self, subkey): 1cba
224 if subkey not in self.array.dtype[self.key].names: 224 ↛ 225line 224 didn't jump to line 225 because the condition on line 224 was never true1a
225 raise KeyError(subkey)
226 return self.__class__(self.array[self.key], subkey, self) 1a
228 def set(self, val): 1cba
229 assert isinstance(val, (numpy.ndarray, jnp.ndarray, StructuredArray)) 1cba
230 prev = self.array._dict[self.key] 1cba
231 assert prev.dtype == val.dtype 1cba
232 assert prev.shape == val.shape 1cba
233 d = dict(self.array._dict) 1cba
234 d[self.key] = self.array._readonlyview_wrapifstructured(val) 1cba
235 out = self.array._array(self.array.shape, self.array.dtype, d) 1cba
236 if self.parent: 1cba
237 return self.parent.set(out) 1a
238 else:
239 return out 1cba
241 def reshape(self, *shape): 1cba
242 """
243 Reshape the array without changing its contents.
245 See numpy.ndarray.reshape.
246 """
247 if len(shape) == 1 and hasattr(shape[0], '__len__'): 1cba
248 shape = shape[0] 1ba
249 shape = tuple(shape) 1cba
250 d = { 1cba
251 name: x.reshape(shape + self.dtype[name].shape)
252 for name, x in self._dict.items()
253 }
254 shape = numpy.empty(self.shape, []).reshape(shape).shape 1cba
255 return self._array(shape, self.dtype, d) 1cba
257 def squeeze(self, axis=None): 1cba
258 """Remove axes of length 1. See numpy.ndarray.squeeze."""
259 if axis is None: 1ba
260 axis = tuple(i for i, size in enumerate(self.shape) if size == 1) 1a
261 if not hasattr(axis, '__len__'): 1ba
262 axis = (axis,) 1ba
263 assert all(self.shape[i] == 1 for i in axis) 1ba
264 newshape = [size for i, size in enumerate(self.shape) if i not in axis] 1ba
265 return self.reshape(newshape) 1ba
267 def astype(self, dtype): 1cba
268 if dtype != self.dtype:
269 raise NotImplementedError
270 return self
272 def broadcast_to(self, shape, **kw): 1cba
273 """
274 Return a view of the array broadcasted to another shape.
276 See numpy.broadcast_to.
277 """
278 # raises if not broadcastable
279 numpy.broadcast_to(numpy.empty(self.shape, []), shape, **kw) 1a
280 d = { 1a
281 name: broadcast_to(x, shape + self.dtype[name].shape, **kw)
282 for name, x in self._dict.items()
283 }
284 return self._array(shape, self.dtype, d) 1a
286 def tree_flatten(self): 1cba
287 """JAX PyTree encoder. See `jax.tree_util.tree_flatten`."""
288 children = tuple(self._dict[key] for key in self.dtype.names) 1cba
289 aux = dict(shape=self.shape, dtype=self.dtype) 1cba
290 return children, aux 1cba
292 @classmethod 1cba
293 def tree_unflatten(cls, aux, children): 1cba
294 """JAX PyTree decoder. See `jax.tree_util.tree_unflatten`."""
295 # if there are no fields, keep original shape
296 if not children: 296 ↛ 297line 296 didn't jump to line 297 because the condition on line 296 was never true1cba
297 return cls._array(aux['shape'], aux['dtype'], {})
299 # convert children to arrays because tree.flatten unpacks 0d
300 # arrays
301 children = list(map(asarray, children)) 1cba
303 # if possible, keep original dtype shapes
304 oldtype = aux['dtype'] 1cba
305 compatible_tail_shapes = all( 1cba
306 x.shape[max(0, x.ndim - oldtype[i].ndim) :] == oldtype[i].shape
307 for i, x in enumerate(children)
308 )
309 head_shapes = [ 1cba
310 x.shape[: max(0, x.ndim - oldtype[i].ndim)] for i, x in enumerate(children)
311 ]
312 compatible_head_shapes = all(head_shapes[0] == s for s in head_shapes) 1cba
313 if compatible_tail_shapes and compatible_head_shapes: 1cba
314 dtype = numpy.dtype( 1cba
315 [
316 (oldtype.names[i], x.dtype, oldtype[i].shape)
317 for i, x in enumerate(children)
318 ]
319 )
320 else:
321 dtype = None 1a
323 d = dict(zip(oldtype.names, children, strict=True)) 1cba
325 return cls._array(None, dtype, d) 1cba
327 def __repr__(self): 1cba
328 # code from gvar https://github.com/gplepage/gvar
329 # bufferdict.pyx:BufferDict:__str__
330 out = 'StructuredArray({' 1a
332 listrepr = [(repr(k), repr(v)) for k, v in self._dict.items()] 1a
333 newlinemode = any('\n' in rv for _, rv in listrepr) 1a
335 for rk, rv in listrepr: 1a
336 if not newlinemode: 1a
337 out += f'{rk}: {rv}, ' 1a
338 elif '\n' in rv: 1a
339 rv = rv.replace('\n', '\n ') 1a
340 out += f'\n {rk}:\n {rv},' 1a
341 else:
342 out += f'\n {rk}: {rv},' 1a
344 if out.endswith(', '): 1a
345 out = out[:-2] 1a
346 elif newlinemode: 1a
347 out += '\n' 1a
348 out += '})' 1a
350 return out 1a
352 def __array__(self, copy=None, dtype=None): 1cba
353 if copy is False: 353 ↛ 354line 353 didn't jump to line 354 because the condition on line 353 was never true1a
354 msg = 'StructuredArray has to be copied when converted to a numpy array'
355 raise ValueError(msg)
356 if dtype is not None: 356 ↛ 357line 356 didn't jump to line 357 because the condition on line 356 was never true1a
357 dtype = numpy.dtype(dtype)
358 if dtype != self.dtype:
359 msg = 'StructuredArray can not be converted to a numpy array with a different dtype'
360 raise ValueError(msg)
361 array = numpy.empty(self.shape, self.dtype) 1a
362 self._copy_into_array(array) 1a
363 return array 1a
365 def _copy_into_array(self, dest): 1cba
366 assert self.dtype == dest.dtype 1a
367 assert self.shape == dest.shape 1a
368 for name, src in self._dict.items(): 1a
369 if isinstance(src, StructuredArray): 1a
370 src._copy_into_array(dest[name]) 1a
371 else:
372 dest[name][...] = src 1a
374 def __array_function__(self, func, types, args, kwargs): 1cba
375 if func not in self._handled_functions: 375 ↛ 376line 375 didn't jump to line 376 because the condition on line 375 was never true1ba
376 return NotImplemented
377 return self._handled_functions[func](*args, **kwargs) 1ba
379 _handled_functions = {} # noqa: RUF012, registry shared by design 1cba
381 @classmethod 1cba
382 def _implements(cls, np_function): 1cba
383 """Register an __array_function__ implementation."""
385 def decorator(func): 1cba
386 cls._handled_functions[np_function] = func 1cba
387 newdoc = f"""\ 1cba
388Implementation of `{np_function.__module__}.{np_function.__name__}` for `StructuredArray`.
390"""
391 if func.__doc__: 1cba
392 newdoc += textwrap.dedent(func.__doc__) + '\n' 1cba
393 newdoc += 'Original docstring below:\n\n' 1cba
394 newdoc += textwrap.dedent(np_function.__doc__) 1cba
395 func.__doc__ = newdoc 1cba
396 return func 1cba
398 return decorator 1cba
401@StructuredArray._implements(numpy.broadcast_to) 1cba
402def broadcast_to(x, shape, **kw): 1cba
403 """Version of numpy.broadcast_to for StructuredArray and JAX arrays."""
404 if isinstance(x, StructuredArray): 1a
405 return x.broadcast_to(shape, **kw) 1a
406 elif isinstance(x, jnp.ndarray): 406 ↛ 407line 406 didn't jump to line 407 because the condition on line 406 was never true1a
407 return jnp.broadcast_to(x, shape, **kw)
408 else:
409 return numpy.broadcast_to(x, shape, **kw) 1a
412@StructuredArray._implements(numpy.broadcast_arrays) 1cba
413def broadcast_arrays(*arrays, **kw): 1cba
414 """Version of numpy.broadcast_arrays for StructuredArray and JAX arrays."""
415 shapes = [a.shape for a in arrays] 1a
416 shape = numpy.broadcast_shapes(*shapes) 1a
417 return [broadcast_to(a, shape, **kw) for a in arrays] 1a
418 # numpy.broadcast_arrays returns a list, not a tuple
421class broadcast: 1cba
422 """Version of numpy.broadcast that works with StructuredArray."""
424 # not handled by __array_function__
426 def __init__(self, *arrays): 1cba
427 self.shape = numpy.broadcast_shapes(*(a.shape for a in arrays)) 1cba
430def asarray(x, dtype=None): 1cba
431 """
432 Version of `numpy.asarray` that works with `StructuredArray` and JAX arrays.
434 If `x` is not an array already, returns a JAX array if possible.
435 """
436 if isinstance(x, (StructuredArray, jnp.ndarray, numpy.ndarray)): 1cba
437 return x if dtype is None else x.astype(dtype) 1cba
438 if x is None: 1cba
439 return numpy.asarray(x, dtype) 1a
440 # partial workaround for jax issue #14506, None would be interpreted as
441 # nan by jax
442 try: 1cba
443 return jnp.asarray(x, dtype) 1cba
444 except (TypeError, ValueError): 1ca
445 return numpy.asarray(x, dtype) 1ca
448def _asarray_jaxifpossible(x): 1cba
449 x = asarray(x) 1cba
450 if x.dtype.names: 1cba
451 return tree.map(_asarray_jaxifpossible, StructuredArray(x)) 1cba
452 if isinstance(x, numpy.ndarray): 1cba
453 try: 1cba
454 return jnp.asarray(x) 1cba
455 except (TypeError, ValueError): 1cba
456 pass 1cba
457 return x 1cba
460@StructuredArray._implements(numpy.squeeze) 1cba
461def _squeeze(a, axis=None): 1cba
462 return a.squeeze(axis) 1a
465@StructuredArray._implements(numpy.ix_) 1cba
466def _ix(*args): 1cba
467 args = tuple(map(asarray, args)) 1a
468 assert all(x.ndim == 1 for x in args) 1a
469 n = len(args) 1a
470 return tuple( 1a
471 x.reshape((1,) * i + (-1,) + (1,) * (n - i - 1)) for i, x in enumerate(args)
472 )
475def unstructured_to_structured( 1cba
476 arr, dtype=None, names=None, align=False, copy=False, casting='unsafe'
477):
478 """
479 Convert an unstructured array to a `StructuredArray`.
481 Like `numpy.lib.recfunctions.unstructured_to_structured`, but outputs a
482 `StructuredArray`.
483 """
484 arr = asarray(arr) 1ba
485 if not arr.ndim: 1ba
486 msg = 'arr must have at least one dimension' 1a
487 raise ValueError(msg) 1a
488 mockup = numpy.empty((0, *arr.shape[-1:]), arr.dtype) 1ba
489 dummy = recfunctions.unstructured_to_structured( 1ba
490 mockup, dtype=dtype, names=names, align=align, copy=copy, casting=casting
491 )
492 out, length = _unstructured_to_structured_recursive( 1ba
493 0, (), arr, dummy.dtype, copy, casting
494 )
495 assert length == arr.shape[-1] 1ba
496 return out 1ba
499def _unstructured_to_structured_recursive( 1cba
500 idx, shape, arr, dtype, copy, casting, *strides
501):
502 arrays = {} 1ba
503 for i, name in enumerate(dtype.names): 1ba
504 base = dtype[i].base 1ba
505 subshape = shape + dtype[i].shape 1ba
506 size = math.prod(dtype[i].shape) 1ba
507 stride = _nd(base) 1ba
508 substrides = (*strides, (size, stride)) 1ba
509 if base.names is not None: 1ba
510 y, newidx = _unstructured_to_structured_recursive( 1ba
511 idx, subshape, arr, base, copy, casting, *substrides
512 )
513 shift = newidx - idx 1ba
514 assert shift == stride 1ba
515 idx += size * stride 1ba
516 else:
517 assert stride == 1 1ba
518 if all(size == 1 for size, _ in strides): 1ba
519 indices = numpy.s_[idx : idx + size] 1ba
520 else:
521 indices = sum( 1a
522 (
523 stride * numpy.arange(size)[numpy.s_[:,] + (None,) * i]
524 for i, (size, stride) in enumerate(reversed(substrides))
525 ),
526 start=idx,
527 )
528 indices = indices.reshape(-1) 1a
529 key = numpy.s_[..., indices] 1ba
530 x = arr[key] 1ba
531 x = x.reshape(arr.shape[:-1] + subshape) 1ba
532 if isinstance(x, jnp.ndarray): 1ba
533 y = x.astype(base) 1ba
534 else:
535 y = x.astype(base, copy=copy, casting=casting) 1a
536 idx += size 1ba
537 arrays[name] = y 1ba
538 return StructuredArray._array(arr.shape[:-1] + shape, dtype, arrays), idx 1ba
541@StructuredArray._implements(recfunctions.structured_to_unstructured) 1cba
542def _structured_to_unstructured(arr, dtype=None, casting='unsafe'): 1cba
543 mockup = numpy.empty(0, arr.dtype) 1ba
544 dummy = recfunctions.structured_to_unstructured( 1ba
545 mockup, dtype=dtype, casting=casting
546 )
547 args = (arr.shape + dummy.shape[-1:], dummy.dtype) 1ba
548 try: 1ba
549 out = jnp.empty(*args) 1ba
550 except TypeError: 1a
551 out = numpy.empty(*args) 1a
552 out, length = _structured_to_unstructured_recursive(0, arr, out) 1ba
553 assert length == dummy.shape[-1] 1ba
554 return out 1ba
557def _nd(dtype): 1cba
558 """Count the number of scalars in a dtype."""
559 base = dtype.base 1cba
560 shape = dtype.shape 1cba
561 size = math.prod(shape) 1cba
562 if base.names is None: 1cba
563 return size 1cba
564 else:
565 return size * sum(_nd(base[name]) for name in base.names) 1cba
567 # I use this function in many parts of the package so it should not have an
568 # underscore, even if I don't export it in the main namespace. And move it
569 # to utils, it's not specific to StructuredArray.
572def _structured_to_unstructured_recursive(idx, arr, out, *strides): 1cba
573 dtype = arr.dtype 1ba
574 for i, name in enumerate(dtype.names): 1ba
575 subarr = arr[name] 1ba
576 base = dtype[i].base 1ba
577 size = math.prod(dtype[i].shape) 1ba
578 stride = _nd(base) 1ba
579 substrides = (*strides, (size, stride)) 1ba
580 if base.names is not None: 1ba
581 out, newidx = _structured_to_unstructured_recursive( 1ba
582 idx, subarr, out, *substrides
583 )
584 shift = newidx - idx 1ba
585 assert shift == stride 1ba
586 idx += size * stride 1ba
587 else:
588 assert stride == 1 1ba
589 if all(size == 1 for size, _ in strides): 1ba
590 indices = numpy.s_[idx : idx + size] 1ba
591 srcsize = size 1ba
592 else:
593 indices = sum( 1a
594 (
595 stride * numpy.arange(size)[numpy.s_[:,] + (None,) * i]
596 for i, (size, stride) in enumerate(reversed(substrides))
597 ),
598 start=idx,
599 )
600 indices = indices.reshape(-1) 1a
601 srcsize = indices.size 1a
602 key = numpy.s_[..., indices] 1ba
603 src = subarr.reshape((*out.shape[:-1], srcsize)) 1ba
604 if hasattr(out, 'at'): 1ba
605 out = out.at[key].set(src) 1ba
606 else:
607 out[key] = src 1a
608 idx += size 1ba
609 return out, idx 1ba
612@StructuredArray._implements(numpy.empty_like) 1cba
613def _empty_like(prototype, dtype=None, *, shape=None): 1cba
614 shape = prototype.shape if shape is None else shape 1a
615 dtype = prototype.dtype if dtype is None else dtype 1a
616 return _empty(shape, dtype) 1a
619@StructuredArray._implements(numpy.empty) 1cba
620def _empty(shape, dtype=float): 1cba
621 if hasattr(shape, '__len__'): 1a
622 shape = tuple(shape) 1a
623 else:
624 shape = (int(shape),) 1a
625 dtype = numpy.dtype(dtype) 1a
626 arrays = {} 1a
627 for i, name in enumerate(dtype.names): 1a
628 dtbase = dtype[i].base 1a
629 dtshape = shape + dtype[i].shape 1a
630 if dtbase.names is not None: 1a
631 y = _empty(dtshape, dtbase) 1a
632 else:
633 try: 1a
634 y = jnp.empty(dtshape, dtbase) 1a
635 except TypeError: 1a
636 y = numpy.empty(dtshape, dtbase) 1a
637 arrays[name] = y 1a
638 return StructuredArray._array(shape, dtype, arrays) 1a
641@StructuredArray._implements(numpy.concatenate) 1cba
642def _concatenate(arrays, axis=0, dtype=None, casting='same_kind'): 1cba
644 # checks arrays is a non-empty sequence
645 arrays = list(arrays) 1a
646 if not arrays: 646 ↛ 647line 646 didn't jump to line 647 because the condition on line 646 was never true1a
647 msg = 'need at least one array to concatenate'
648 raise ValueError(msg)
650 # parse axis argument
651 if axis is None: 651 ↛ 652line 651 didn't jump to line 652 because the condition on line 651 was never true1a
652 axis = 0
653 arrays = [a.reshape(-1) for a in arrays]
654 else:
655 ndim = arrays[0].ndim 1a
656 assert all(a.ndim == ndim for a in arrays) 1a
657 assert -ndim <= axis < ndim 1a
658 axis %= ndim 1a
659 shape = arrays[0].shape 1a
660 assert all(a.shape[:axis] == shape[:axis] for a in arrays) 1a
661 assert all(a.shape[axis + 1 :] == shape[axis + 1 :] for a in arrays) 1a
663 dtype = numpy.result_type(*(a.dtype for a in arrays)) 1a
664 assert all(numpy.can_cast(a.dtype, dtype, casting) for a in arrays) 1a
665 shape = ( 1a
666 *arrays[0].shape[:axis],
667 sum(a.shape[axis] for a in arrays),
668 *arrays[0].shape[axis + 1 :],
669 )
671 out = _concatenate_recursive(arrays, axis, dtype, shape, casting) 1a
672 assert out.shape == shape 1a
673 assert out.dtype == dtype 1a
674 return out 1a
677def _concatenate_recursive(arrays, axis, dtype, shape, casting): 1cba
678 cat = {} 1a
679 for name in dtype.names: 1a
680 subarrays = [a[name] for a in arrays] 1a
681 base = dtype[name].base 1a
682 if base.names is not None: 1a
683 subshape = shape + dtype[name].shape 1a
684 y = _concatenate_recursive(subarrays, axis, base, subshape, casting) 1a
685 else:
686 try: 1a
687 y = jnp.concatenate(subarrays, axis=axis, dtype=base) 1a
688 except TypeError: 1a
689 y = numpy.concatenate(subarrays, axis=axis, dtype=base, casting=casting) 1a
690 cat[name] = y 1a
691 return StructuredArray._array(shape, dtype, cat) 1a
694@StructuredArray._implements(recfunctions.append_fields) 1cba
695def _append_fields(base, names, data, usemask=True): 1cba
696 assert not usemask, 'masked arrays not supported, set usemask=False' 1a
697 if isinstance(names, str): 1a
698 names = [names] 1a
699 data = [data] 1a
700 assert len(names) == len(data) 1a
701 arrays = base._dict.copy() 1a
702 arrays.update(zip(names, data, strict=True)) 1a
703 dtype = numpy.dtype( 1a
704 base.dtype.descr
705 + [(name, array.dtype) for name, array in zip(names, data, strict=True)]
706 )
707 return StructuredArray._array(base.shape, dtype, arrays) 1a
710@StructuredArray._implements(numpy.swapaxes) 1cba
711def _swapaxes(x, i, j): 1cba
712 return x.swapaxes(i, j)