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

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/>. 

19 

20import math 1cba

21import textwrap 1cba

22 

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

28 

29 

30@tree_util.register_pytree_node_class 1cba

31class StructuredArray: 1cba

32 """ 1cba

33 JAX-friendly imitation of a numpy structured array. 

34 

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. 

37 

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``. 

43 

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. 

49 

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 """ 

57 

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

67 

68 @classmethod 1cba

69 def _array(cls, s, t, d, *, check=True): 1cba

70 """ 

71 Create a new StructuredArray. 

72 

73 All methods and functions that create a new StructuredArray object 

74 should use this method. 

75 

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. 

89 

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

99 

100 # remove offset info since this is actually a columnar format 

101 t = recfunctions.repack_fields(t, align=False, recurse=True) 1cba

102 

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

109 

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

118 

119 out = super().__new__(cls) 1cba

120 out.shape = s 1cba

121 out.dtype = t 1cba

122 out._dict = d 1cba

123 

124 return out 1cba

125 

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

134 

135 @classmethod 1cba

136 def from_dataframe(cls, df): 1cba

137 """ 

138 Make a StructuredArray from a DataFrame. 

139 

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

147 

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

156 

157 @property 1cba

158 def size(self): 1cba

159 return math.prod(self.shape) 1ba

160 

161 @property 1cba

162 def ndim(self): 1cba

163 return len(self.shape) 1ba

164 

165 @property 1cba

166 def nbytes(self): 1cba

167 return sum(x.nbytes for x in self._dict.values()) 

168 

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

174 

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

179 

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

186 

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

203 

204 @property 1cba

205 def at(self): 1cba

206 return self._Getter(self) 1cba

207 

208 class _Getter: 1cba

209 def __init__(self, array): 1cba

210 self.array = array 1cba

211 

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

216 

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

222 

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

227 

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

240 

241 def reshape(self, *shape): 1cba

242 """ 

243 Reshape the array without changing its contents. 

244 

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

256 

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

266 

267 def astype(self, dtype): 1cba

268 if dtype != self.dtype: 

269 raise NotImplementedError 

270 return self 

271 

272 def broadcast_to(self, shape, **kw): 1cba

273 """ 

274 Return a view of the array broadcasted to another shape. 

275 

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

285 

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

291 

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'], {}) 

298 

299 # convert children to arrays because tree.flatten unpacks 0d 

300 # arrays 

301 children = list(map(asarray, children)) 1cba

302 

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

322 

323 d = dict(zip(oldtype.names, children, strict=True)) 1cba

324 

325 return cls._array(None, dtype, d) 1cba

326 

327 def __repr__(self): 1cba

328 # code from gvar https://github.com/gplepage/gvar 

329 # bufferdict.pyx:BufferDict:__str__ 

330 out = 'StructuredArray({' 1a

331 

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

334 

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

343 

344 if out.endswith(', '): 1a

345 out = out[:-2] 1a

346 elif newlinemode: 1a

347 out += '\n' 1a

348 out += '})' 1a

349 

350 return out 1a

351 

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

364 

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

373 

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

378 

379 _handled_functions = {} # noqa: RUF012, registry shared by design 1cba

380 

381 @classmethod 1cba

382 def _implements(cls, np_function): 1cba

383 """Register an __array_function__ implementation.""" 

384 

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`. 

389 

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

397 

398 return decorator 1cba

399 

400 

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

410 

411 

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 

419 

420 

421class broadcast: 1cba

422 """Version of numpy.broadcast that works with StructuredArray.""" 

423 

424 # not handled by __array_function__ 

425 

426 def __init__(self, *arrays): 1cba

427 self.shape = numpy.broadcast_shapes(*(a.shape for a in arrays)) 1cba

428 

429 

430def asarray(x, dtype=None): 1cba

431 """ 

432 Version of `numpy.asarray` that works with `StructuredArray` and JAX arrays. 

433 

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

446 

447 

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

458 

459 

460@StructuredArray._implements(numpy.squeeze) 1cba

461def _squeeze(a, axis=None): 1cba

462 return a.squeeze(axis) 1a

463 

464 

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 ) 

473 

474 

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`. 

480 

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

497 

498 

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

539 

540 

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

555 

556 

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

566 

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. 

570 

571 

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

610 

611 

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

617 

618 

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

639 

640 

641@StructuredArray._implements(numpy.concatenate) 1cba

642def _concatenate(arrays, axis=0, dtype=None, casting='same_kind'): 1cba

643 

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) 

649 

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

662 

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 ) 

670 

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

675 

676 

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

692 

693 

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

708 

709 

710@StructuredArray._implements(numpy.swapaxes) 1cba

711def _swapaxes(x, i, j): 1cba

712 return x.swapaxes(i, j)