Coverage for jaxquantum/core/qarray.py: 84%

874 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-27 22:28 +0000

1"""New Qarray implementation with sparse support.""" 

2 

3from __future__ import annotations 

4 

5from abc import ABC, abstractmethod 

6from copy import deepcopy 

7from enum import Enum 

8from math import prod 

9from numbers import Number 

10from typing import TYPE_CHECKING, Generic, List, Literal, TypeVar, Union, overload 

11 

12import jax.numpy as jnp 

13import jax.scipy as jsp 

14from flax import struct 

15from jax import Array, config, default_backend 

16from jax.experimental import sparse 

17from numpy import ndarray 

18 

19from jaxquantum.core.dims import ( 

20 Qdims, 

21 Qtypes, 

22 check_dims, 

23 ket_from_op_dims, 

24) 

25from jaxquantum.core.settings import SETTINGS, _maybe_shard 

26from jaxquantum.utils.utils import robust_isscalar 

27 

28if TYPE_CHECKING: 

29 from jaxquantum.core.sparse_bcoo import SparseBCOOImpl 

30 

31config.update("jax_enable_x64", True) 

32 

33# Type variable for implementation types 

34ImplT = TypeVar("ImplT", bound="QarrayImpl") 

35 

36# Module-level registry mapping impl_class -> QarrayImplType member 

37_IMPL_REGISTRY: dict = {} 

38 

39 

40class QarrayImplType(Enum): 

41 """Enumeration of available Qarray storage backends. 

42 

43 Each member maps one-to-one with a concrete ``QarrayImpl`` subclass. 

44 New backends should call ``QarrayImplType.register(MyImpl, QarrayImplType.MY_TYPE)`` 

45 immediately after defining their impl class. 

46 

47 Members: 

48 DENSE: Standard JAX dense array (``jnp.ndarray``). 

49 SPARSE_BCOO: JAX experimental BCOO sparse array. 

50 SPARSE_DIA: Diagonal sparse array. 

51 """ 

52 

53 DENSE = "dense" 

54 SPARSE_BCOO = "sparse_bcoo" 

55 SPARSE_DIA = "sparse_dia" 

56 

57 @classmethod 

58 def register(cls, impl_class, member): 

59 """Register an implementation class with a QarrayImplType member. 

60 

61 Args: 

62 impl_class: The concrete ``QarrayImpl`` subclass to register. 

63 member: The ``QarrayImplType`` enum member to associate with it. 

64 """ 

65 _IMPL_REGISTRY[impl_class] = member 

66 

67 @classmethod 

68 def has(cls, x) -> bool: 

69 """Return True if x corresponds to a member of QarrayImplType. 

70 

71 Accepts an existing ``QarrayImplType`` member, a string equal to the 

72 member name or value (case-insensitive), or an implementation class 

73 (e.g. ``DenseImpl``, ``SparseBCOOImpl``) that has been registered. 

74 

75 Args: 

76 x: Value to test — a ``QarrayImplType``, ``str``, or impl class. 

77 

78 Returns: 

79 True if ``x`` maps to a known ``QarrayImplType`` member. 

80 """ 

81 if isinstance(x, cls): 

82 return True 

83 

84 if isinstance(x, str): 

85 xl = x.lower() 

86 return any(xl == member.value or xl == member.name.lower() for member in cls) 

87 

88 # Try mapping from an implementation class to an enum member 

89 try: 

90 cls.from_impl_class(x) 

91 return True 

92 except Exception: # noqa: BLE001 

93 return False 

94 

95 @classmethod 

96 def from_impl_class(cls, impl_class) -> "QarrayImplType": 

97 """Return the ``QarrayImplType`` member associated with *impl_class*. 

98 

99 Args: 

100 impl_class: A concrete ``QarrayImpl`` subclass that has been 

101 registered via :meth:`register`. 

102 

103 Returns: 

104 The corresponding ``QarrayImplType`` member. 

105 

106 Raises: 

107 ValueError: If *impl_class* is not in the registry. 

108 """ 

109 if impl_class in _IMPL_REGISTRY: 

110 return _IMPL_REGISTRY[impl_class] 

111 raise ValueError(f"Unknown implementation class: {impl_class}") 

112 

113 def get_impl_class(self): 

114 """Return the implementation class registered for this member. 

115 

116 Returns: 

117 The concrete ``QarrayImpl`` subclass associated with this member. 

118 

119 Raises: 

120 ValueError: If no class has been registered for this member. 

121 """ 

122 for cls_key, member in _IMPL_REGISTRY.items(): 

123 if member is self: 

124 return cls_key 

125 raise ValueError(f"No impl class registered for {self}") 

126 

127 

128def robust_asarray(data) -> Union[Array, sparse.BCOO]: 

129 """Convert *data* to a JAX array, leaving sparse BCOO and SparseDiaData untouched. 

130 

131 Args: 

132 data: Input data — any array-like, ``sparse.BCOO``, or ``SparseDiaData``. 

133 

134 Returns: 

135 A ``jax.Array``, ``sparse.BCOO``, or ``SparseDiaData``. 

136 """ 

137 if isinstance(data, sparse.BCOO): 

138 return data 

139 # SparseDiaData has a ``_is_sparse_dia`` marker; pass it through unchanged 

140 if getattr(data, "_is_sparse_dia", False): 

141 return data 

142 return jnp.asarray(data) 

143 

144 

145class QarrayImpl(ABC): 

146 """Abstract base class defining the interface every storage backend must implement. 

147 

148 A ``QarrayImpl`` wraps a raw data array (dense ``jnp.ndarray`` or sparse 

149 ``BCOO``) and provides the mathematical primitives used by ``Qarray``. 

150 Concrete subclasses must implement every ``@abstractmethod``. 

151 

152 Attributes: 

153 PROMOTION_ORDER: Integer priority used by ``_coerce`` to decide which 

154 side to promote when operands have different types. Higher means 

155 "more general" (``DenseImpl = 1``, ``SparseBCOOImpl = 0``). 

156 """ 

157 

158 PROMOTION_ORDER: int = 0 # override in subclasses; higher = more general 

159 # Current hierarchy: SparseDiaImpl=0, SparseBCOOImpl=1, DenseImpl=2 

160 

161 @abstractmethod 

162 def get_data(self) -> Array: 

163 """Return the underlying raw data array.""" 

164 pass 

165 

166 @property 

167 def data(self) -> Array: 

168 """The underlying raw data array.""" 

169 return self.get_data() 

170 

171 @property 

172 def impl_type(self) -> QarrayImplType: 

173 """The ``QarrayImplType`` member corresponding to this instance.""" 

174 return QarrayImplType.from_impl_class(type(self)) 

175 

176 @classmethod 

177 @abstractmethod 

178 def from_data(cls, data) -> "QarrayImpl": 

179 """Wrap raw data in this impl type. 

180 

181 Args: 

182 data: Raw array data (dense ``jnp.ndarray`` or ``sparse.BCOO``). 

183 

184 Returns: 

185 A new instance of this implementation wrapping *data*. 

186 """ 

187 pass 

188 

189 @abstractmethod 

190 def matmul(self, other: "QarrayImpl") -> "QarrayImpl": 

191 """Matrix multiplication with *other*. 

192 

193 Args: 

194 other: Right-hand operand. 

195 

196 Returns: 

197 Result of ``self @ other`` as a ``QarrayImpl``. 

198 """ 

199 pass 

200 

201 @abstractmethod 

202 def add(self, other: "QarrayImpl") -> "QarrayImpl": 

203 """Element-wise addition with *other*. 

204 

205 Args: 

206 other: Right-hand operand. 

207 

208 Returns: 

209 Result of ``self + other`` as a ``QarrayImpl``. 

210 """ 

211 pass 

212 

213 @abstractmethod 

214 def sub(self, other: "QarrayImpl") -> "QarrayImpl": 

215 """Element-wise subtraction of *other*. 

216 

217 Args: 

218 other: Right-hand operand. 

219 

220 Returns: 

221 Result of ``self - other`` as a ``QarrayImpl``. 

222 """ 

223 pass 

224 

225 @abstractmethod 

226 def mul(self, scalar) -> "QarrayImpl": 

227 """Scalar multiplication. 

228 

229 Args: 

230 scalar: Scalar value to multiply by. 

231 

232 Returns: 

233 Result of ``scalar * self`` as a ``QarrayImpl``. 

234 """ 

235 pass 

236 

237 @abstractmethod 

238 def dag(self) -> "QarrayImpl": 

239 """Conjugate transpose. 

240 

241 Returns: 

242 The conjugate transpose of this array as a ``QarrayImpl``. 

243 """ 

244 pass 

245 

246 @abstractmethod 

247 def to_dense(self) -> "DenseImpl": 

248 """Convert to a ``DenseImpl``. 

249 

250 Returns: 

251 A ``DenseImpl`` wrapping the same data. 

252 """ 

253 pass 

254 

255 @abstractmethod 

256 def to_sparse_bcoo(self) -> "SparseBCOOImpl": 

257 """Convert to a ``SparseBCOOImpl`` (BCOO). 

258 

259 Returns: 

260 A ``SparseBCOOImpl`` wrapping the same data. 

261 """ 

262 pass 

263 

264 def to_sparse_dia(self) -> "QarrayImpl": 

265 """Convert to a ``SparseDiaImpl``. 

266 

267 Default implementation goes through dense and auto-detects diagonals. 

268 Subclasses may override for a more direct path. 

269 

270 Returns: 

271 A ``SparseDiaImpl`` wrapping the same data. 

272 """ 

273 # Import here to avoid circular imports at module load time 

274 from jaxquantum.core.sparse_dia import SparseDiaImpl 

275 return SparseDiaImpl.from_data(self.to_dense()._data) 

276 

277 @abstractmethod 

278 def shape(self) -> tuple: 

279 """Shape of the underlying data array. 

280 

281 Returns: 

282 Tuple of dimension sizes. 

283 """ 

284 pass 

285 

286 @abstractmethod 

287 def dtype(self): 

288 """Data type of the underlying array. 

289 

290 Returns: 

291 A numpy/JAX dtype object. 

292 """ 

293 pass 

294 

295 @abstractmethod 

296 def __deepcopy__(self, memo=None): 

297 pass 

298 

299 @abstractmethod 

300 def tidy_up(self, atol): 

301 """Zero out values whose magnitude is below *atol*. 

302 

303 Args: 

304 atol: Absolute tolerance threshold. 

305 

306 Returns: 

307 A new ``QarrayImpl`` with small values zeroed. 

308 """ 

309 pass 

310 

311 @abstractmethod 

312 def kron(self, other: "QarrayImpl") -> "QarrayImpl": 

313 """Kronecker (tensor) product with another implementation. 

314 

315 Args: 

316 other: Right-hand operand. Mixed-type pairs are handled by 

317 ``_coerce`` — the result has the higher ``PROMOTION_ORDER`` 

318 type (dense wins over sparse). 

319 

320 Returns: 

321 A new ``QarrayImpl`` containing the Kronecker product. 

322 """ 

323 pass 

324 

325 @classmethod 

326 @abstractmethod 

327 def _eye_data(cls, n: int, dtype=None): 

328 """Create identity matrix data of size n. 

329 

330 Args: 

331 n: Matrix size. 

332 dtype: Optional data type for the identity entries. 

333 

334 Returns: 

335 Raw identity matrix data in the format appropriate for this impl. 

336 """ 

337 pass 

338 

339 @classmethod 

340 def _scaled_identity(cls, n: int, scalar, dtype=None) -> QarrayImpl: 

341 """Create an identity scaled over any leading scalar batch axes.""" 

342 scalar = jnp.asarray(scalar) + 0.0j 

343 if scalar.ndim: 

344 scalar = scalar.reshape(scalar.shape + (1, 1)) 

345 return cls.from_data(cls._eye_data(n, dtype=dtype) * scalar) 

346 

347 @classmethod 

348 @abstractmethod 

349 def can_handle_data(cls, arr) -> bool: 

350 """Return True if *arr* is a raw data type natively handled by this impl. 

351 

352 Used by the module-level :func:`dag_data` dispatcher to route raw 

353 arrays to the correct backend without any isinstance chain outside the 

354 impl classes. 

355 

356 Args: 

357 arr: Raw array — e.g. ``jnp.ndarray`` for ``DenseImpl`` or 

358 ``sparse.BCOO`` for ``SparseBCOOImpl``. 

359 

360 Returns: 

361 True if this impl can operate on *arr* without conversion. 

362 """ 

363 pass 

364 

365 @classmethod 

366 @abstractmethod 

367 def dag_data(cls, arr): 

368 """Conjugate transpose of raw data in this impl's native format. 

369 

370 Implementations must handle batched arrays (last two axes are 

371 swapped) and must not densify sparse arrays. 

372 

373 Args: 

374 arr: Raw array in this impl's native format. 

375 

376 Returns: 

377 Conjugate transpose with the last two axes swapped. 

378 """ 

379 pass 

380 

381 def _promote_to(self, target_cls: type) -> "QarrayImpl": 

382 """Convert this impl to *target_cls* by passing through dense. 

383 

384 Args: 

385 target_cls: The destination ``QarrayImpl`` subclass. 

386 

387 Returns: 

388 An instance of *target_cls* holding equivalent data. 

389 """ 

390 if isinstance(self, target_cls): 

391 return self 

392 return target_cls.from_data(self.to_dense()._data) 

393 

394 def _coerce(self, other: "QarrayImpl") -> "tuple[QarrayImpl, QarrayImpl]": 

395 """Coerce *self* and *other* to the same implementation type. 

396 

397 The impl type with the higher ``PROMOTION_ORDER`` wins; the other side 

398 is promoted via :meth:`_promote_to`. 

399 

400 Args: 

401 other: The other operand. 

402 

403 Returns: 

404 A pair ``(a, b)`` of the same ``QarrayImpl`` subclass, suitable 

405 for a binary operation. 

406 """ 

407 if type(self) is type(other): 

408 return self, other 

409 if self.PROMOTION_ORDER >= other.PROMOTION_ORDER: 

410 return self, other._promote_to(type(self)) 

411 return self._promote_to(type(other)), other 

412 

413 

414@struct.dataclass 

415class DenseImpl(QarrayImpl): 

416 """Dense implementation using JAX dense arrays. 

417 

418 Attributes: 

419 _data: The underlying ``jnp.ndarray``. 

420 """ 

421 

422 _data: Array 

423 

424 PROMOTION_ORDER = 2 # noqa: RUF012 — not a struct field; no annotation intentional 

425 

426 @classmethod 

427 def _make(cls, data) -> "DenseImpl": 

428 """Construct a ``DenseImpl``, applying the configured default sharding. 

429 

430 All internal construction sites route through this so that every 

431 Qarray (including intermediates produced by ``matmul``, ``kron``, 

432 etc.) satisfies the user's sharding invariant. 

433 """ 

434 return cls(_data=_maybe_shard(data)) 

435 

436 @classmethod 

437 def from_data(cls, data) -> "DenseImpl": 

438 """Wrap *data* in a new ``DenseImpl``. 

439 

440 Args: 

441 data: Array-like input data. 

442 

443 Returns: 

444 A ``DenseImpl`` wrapping ``robust_asarray(data)``. 

445 """ 

446 return cls._make(robust_asarray(data)) 

447 

448 def get_data(self) -> Array: 

449 """Return the underlying dense array.""" 

450 return self._data 

451 

452 def matmul(self, other: QarrayImpl) -> QarrayImpl: 

453 """Matrix multiply ``self @ other``, coercing types as needed. 

454 

455 Args: 

456 other: Right-hand operand. 

457 

458 Returns: 

459 A ``DenseImpl`` containing the matrix product. 

460 """ 

461 a, b = self._coerce(other) 

462 if a is not self: 

463 return a.matmul(b) 

464 return DenseImpl._make(self._data @ b._data) 

465 

466 def add(self, other: QarrayImpl) -> QarrayImpl: 

467 """Element-wise addition ``self + other``, coercing types as needed. 

468 

469 Args: 

470 other: Right-hand operand. 

471 

472 Returns: 

473 A ``DenseImpl`` containing the sum. 

474 """ 

475 a, b = self._coerce(other) 

476 if a is not self: 

477 return a.add(b) 

478 return DenseImpl._make(self._data + b._data) 

479 

480 def sub(self, other: QarrayImpl) -> QarrayImpl: 

481 """Element-wise subtraction ``self - other``, coercing types as needed. 

482 

483 Args: 

484 other: Right-hand operand. 

485 

486 Returns: 

487 A ``DenseImpl`` containing the difference. 

488 """ 

489 a, b = self._coerce(other) 

490 if a is not self: 

491 return a.sub(b) 

492 return DenseImpl._make(self._data - b._data) 

493 

494 def mul(self, scalar) -> QarrayImpl: 

495 """Scalar multiplication. 

496 

497 Args: 

498 scalar: Scalar value. 

499 

500 Returns: 

501 A ``DenseImpl`` with each element multiplied by *scalar*. 

502 """ 

503 return DenseImpl._make(scalar * self._data) 

504 

505 def dag(self) -> QarrayImpl: 

506 """Conjugate transpose. 

507 

508 Returns: 

509 A ``DenseImpl`` containing the conjugate transpose. 

510 """ 

511 return DenseImpl._make(jnp.moveaxis(jnp.conj(self._data), -1, -2)) 

512 

513 def to_dense(self) -> "DenseImpl": 

514 """Return self (already dense). 

515 

516 Returns: 

517 This ``DenseImpl`` instance unchanged. 

518 """ 

519 return self 

520 

521 def to_sparse_bcoo(self) -> "SparseBCOOImpl": 

522 """Convert to a ``SparseBCOOImpl`` via ``BCOO.fromdense``. 

523 

524 Returns: 

525 A ``SparseBCOOImpl`` wrapping a BCOO conversion of this array. 

526 """ 

527 from jaxquantum.core.sparse_bcoo import SparseBCOOImpl 

528 return SparseBCOOImpl(sparse.BCOO.fromdense(self._data)) 

529 

530 def shape(self) -> tuple: 

531 """Shape of the underlying dense array. 

532 

533 Returns: 

534 Tuple of dimension sizes. 

535 """ 

536 return self._data.shape 

537 

538 def dtype(self): 

539 """Data type of the underlying dense array. 

540 

541 Returns: 

542 The dtype of ``_data``. 

543 """ 

544 return self._data.dtype 

545 

546 def frobenius_norm(self) -> float: 

547 """Compute the Frobenius norm. 

548 

549 Returns: 

550 The Frobenius norm as a scalar. 

551 """ 

552 return jnp.sqrt(jnp.sum(jnp.abs(self._data) ** 2)) 

553 

554 def real(self) -> QarrayImpl: 

555 """Element-wise real part. 

556 

557 Returns: 

558 A ``DenseImpl`` containing the real parts. 

559 """ 

560 return DenseImpl._make(jnp.real(self._data)) 

561 

562 def imag(self) -> QarrayImpl: 

563 """Element-wise imaginary part. 

564 

565 Returns: 

566 A ``DenseImpl`` containing the imaginary parts. 

567 """ 

568 return DenseImpl._make(jnp.imag(self._data)) 

569 

570 def conj(self) -> QarrayImpl: 

571 """Element-wise complex conjugate. 

572 

573 Returns: 

574 A ``DenseImpl`` containing the complex-conjugated values. 

575 """ 

576 return DenseImpl._make(jnp.conj(self._data)) 

577 

578 def __deepcopy__(self, memo=None): 

579 return DenseImpl._make(deepcopy(self._data, memo)) 

580 

581 def tidy_up(self, atol): 

582 """Zero out real/imaginary parts whose magnitude is below *atol*. 

583 

584 Args: 

585 atol: Absolute tolerance threshold. 

586 

587 Returns: 

588 A new ``DenseImpl`` with small values zeroed. 

589 """ 

590 data = self._data 

591 data_re = jnp.real(data) 

592 data_im = jnp.imag(data) 

593 data_re_mask = jnp.abs(data_re) > atol 

594 data_im_mask = jnp.abs(data_im) > atol 

595 data_new = data_re * data_re_mask + 1j * data_im * data_im_mask 

596 

597 return DenseImpl._make(data_new) 

598 

599 def kron(self, other: "QarrayImpl") -> "QarrayImpl": 

600 """Kronecker product using ``jnp.kron``. 

601 

602 Args: 

603 other: Right-hand operand. 

604 

605 Returns: 

606 A ``DenseImpl`` containing the Kronecker product. 

607 """ 

608 a, b = self._coerce(other) 

609 if a is not self: 

610 return a.kron(b) 

611 return DenseImpl._make(jnp.kron(self._data, b._data)) 

612 

613 @classmethod 

614 def _eye_data(cls, n: int, dtype=None): 

615 """Create an ``n x n`` identity matrix as a dense JAX array. 

616 

617 Args: 

618 n: Matrix size. 

619 dtype: Optional data type. 

620 

621 Returns: 

622 A ``jnp.ndarray`` identity matrix of shape ``(n, n)``. 

623 """ 

624 return jnp.eye(n, dtype=dtype) 

625 

626 @classmethod 

627 def can_handle_data(cls, arr) -> bool: 

628 """Return True for any non-BCOO, non-SparseDIA array. 

629 

630 ``SparseDiaData`` objects carry a ``_is_sparse_dia`` marker so we can 

631 exclude them without a direct type import (which would be circular). 

632 

633 Args: 

634 arr: Raw array. 

635 

636 Returns: 

637 True when *arr* is a plain dense array (not BCOO, not SparseDiaData). 

638 """ 

639 return not isinstance(arr, sparse.BCOO) and not getattr(arr, "_is_sparse_dia", False) 

640 

641 @classmethod 

642 def dag_data(cls, arr) -> Array: 

643 """Conjugate transpose for dense arrays. 

644 

645 Swaps the last two axes via :func:`jnp.moveaxis` and conjugates all 

646 elements. For 1-D inputs only conjugation is applied. 

647 

648 Args: 

649 arr: Dense array. 

650 

651 Returns: 

652 Conjugate transpose with the last two axes swapped. 

653 """ 

654 if len(arr.shape) == 1: 

655 return jnp.conj(arr) 

656 return jnp.moveaxis(jnp.conj(arr), -1, -2) 

657 

658 

659# Register implementation classes with the enum registry 

660# SparseBCOOImpl is registered in sparse_bcoo.py after import 

661QarrayImplType.register(DenseImpl, QarrayImplType.DENSE) 

662 

663 

664@struct.dataclass 

665class Qarray(Generic[ImplT]): 

666 """Quantum array with a pluggable storage backend. 

667 

668 ``Qarray`` wraps a ``QarrayImpl`` together with quantum-mechanical 

669 dimension metadata (``_qdims``) and optional batch dimensions 

670 (``_bdims``). The default backend is dense (``DenseImpl``); pass 

671 ``implementation="sparse_bcoo"`` (or ``QarrayImplType.SPARSE_BCOO``) to 

672 store data as a JAX BCOO sparse array. 

673 

674 Attributes: 

675 _impl: The storage backend holding the raw data. 

676 _qdims: Quantum dimension metadata (bra/ket structure, Hilbert space 

677 sizes). 

678 _bdims: Tuple of batch dimension sizes (empty tuple = non-batched). 

679 

680 Example: 

681 >>> import jaxquantum as jqt 

682 >>> a = jqt.destroy(10, implementation="sparse_bcoo") 

683 >>> a.is_sparse_bcoo 

684 True 

685 """ 

686 

687 _impl: ImplT 

688 _qdims: Qdims = struct.field(pytree_node=False) 

689 _bdims: tuple[int] = struct.field(pytree_node=False) 

690 

691 # Initialization ---- 

692 @classmethod 

693 def _from_impl(cls, impl, qdims, bdims=None): 

694 """Wrap a canonical internal result without repeating public validation.""" 

695 if bdims is None: 

696 shape = impl.shape() 

697 bdims = shape[:-2] if qdims.qtype == Qtypes.oper else shape[:-1] 

698 return cls(impl, qdims, tuple(bdims)) 

699 

700 @classmethod 

701 @overload 

702 def create(cls, data, dims=None, bdims=None, qtype=None, implementation: Literal[QarrayImplType.DENSE] = QarrayImplType.DENSE) -> "Qarray[DenseImpl]": 

703 ... 

704 

705 @classmethod 

706 @overload 

707 def create(cls, data, dims=None, bdims=None, qtype=None, implementation: Literal[QarrayImplType.SPARSE_BCOO] = ...) -> "Qarray[SparseBCOOImpl]": 

708 ... 

709 

710 @classmethod 

711 @overload 

712 def create(cls, data, dims=None, bdims=None, qtype=None, implementation=...) -> "Qarray[DenseImpl]": 

713 ... 

714 

715 @classmethod 

716 def create(cls, data, dims=None, bdims=None, qtype=None, implementation=QarrayImplType.DENSE): 

717 """Create a ``Qarray`` from raw data. 

718 

719 Handles shape normalisation, dimension inference, and tidying of small 

720 values. 

721 

722 State vectors are stored with their Hilbert space on a **single trailing 

723 axis** — a ket/bra of dimension ``N`` has data shape ``bdims + (N,)`` 

724 (never ``(N,1)`` / ``(1,N)``). Operators keep the last two axes: 

725 ``bdims + (M, N)``. The ket/bra/oper distinction lives in ``_qdims``, 

726 not in the data shape. 

727 

728 Legacy ``(N,1)`` / ``(1,N)`` inputs are still accepted and are squeezed 

729 to ``(N,)`` on the way in. Because a 1‑D ``(N,)`` array (or a square 

730 ``(N,N)`` batch of vectors) is shape-ambiguous, pass ``qtype`` to be 

731 explicit. 

732 

733 Args: 

734 data: Input data array (dense array-like or ``sparse.BCOO``). 

735 dims: Quantum dimensions as ``((row_dims...), (col_dims...))`` or, as 

736 a convenience, a flat list of Hilbert space sizes. Inferred from 

737 *data* shape when ``None``. 

738 bdims: Tuple of batch dimension sizes. Inferred from the leading 

739 dimensions of *data* when ``None``. 

740 qtype: Optional quantum type — ``"ket"`` / ``"bra"`` / ``"oper"`` (or 

741 a :class:`Qtypes` member). When given it is authoritative and 

742 resolves shape ambiguity (e.g. a bare ``(N,)`` array with 

743 ``qtype="ket"`` behaves like a legacy ``(N,1)`` input). 

744 implementation: Storage backend — ``QarrayImplType.DENSE`` 

745 (default) or ``QarrayImplType.SPARSE_BCOO``, or the equivalent 

746 string ``"dense"`` / ``"sparse_bcoo"``. 

747 

748 Returns: 

749 A new ``Qarray`` backed by the requested implementation. 

750 """ 

751 data = robust_asarray(data) 

752 

753 # Whether the caller explicitly requested a qtype (vs. it being derived 

754 # from a full ``dims`` tuple). An explicit qtype is authoritative and 

755 # mismatches with the data raise a clear error. 

756 qtype_explicit = qtype is not None 

757 if qtype is not None and not isinstance(qtype, Qtypes): 

758 # Accepts "ket" / "bra" / "oper" (case-sensitive enum value). 

759 qtype = Qtypes.from_str(qtype) 

760 

761 # Empty array (e.g. an empty list of operators) — keep legacy behaviour. 

762 if len(data.shape) == 1 and data.shape[0] == 0: 

763 dims = ((), ()) if dims is None else (tuple(dims[0]), tuple(dims[1])) 

764 bdims = (0,) if bdims is None else tuple(bdims) 

765 check_dims(dims, bdims, data.shape) 

766 qdims = Qdims(dims) 

767 impl_class = QarrayImplType(implementation).get_impl_class() 

768 impl = impl_class.from_data(data) 

769 impl = impl.tidy_up(SETTINGS["auto_tidyup_atol"]) 

770 return cls(impl, qdims, bdims) 

771 

772 # Resolve full quantum dims + batch dims. 

773 dims_is_full = dims is not None and isinstance(dims[0], (list, tuple)) 

774 if qtype is None and dims_is_full: 

775 qtype = Qtypes.from_dims((tuple(dims[0]), tuple(dims[1]))) 

776 

777 if qtype is not None: 

778 # Explicit (or dims-derived) qtype: unambiguous layout. 

779 data, dims, bdims = cls._qtype_layout(data, dims, bdims, qtype) 

780 else: 

781 # Auto: legacy shape heuristics (1‑D → ket, square → oper, etc.). 

782 data, dims, bdims = cls._legacy_layout(data, dims, bdims) 

783 

784 qdims = Qdims(dims) 

785 

786 # Store vectors as bdims + (N,); operators as bdims + (M, N). 

787 if qdims.qtype == Qtypes.oper: 

788 space = (prod(dims[0]), prod(dims[1])) 

789 else: 

790 space = (prod(dims[0]) * prod(dims[1]),) 

791 

792 target_shape = tuple(bdims) + space 

793 

794 # An explicitly requested qtype is authoritative: error out if the data 

795 # cannot be interpreted as that type rather than silently coercing it. 

796 if qtype_explicit: 

797 if qdims.qtype != qtype: 

798 raise ValueError( 

799 f"Requested qtype={qtype} is incompatible with the resolved " 

800 f"dims {dims} (which is a {qdims.qtype})." 

801 ) 

802 if prod(data.shape) != prod(target_shape): 

803 raise ValueError( 

804 f"Data of shape {tuple(data.shape)} is incompatible with " 

805 f"qtype={qtype} and dims={dims}: cannot reshape to " 

806 f"{target_shape}." 

807 ) 

808 

809 # Reshape to the canonical stored shape only when needed. Use varargs so 

810 # this works for dense arrays, BCOO, and SparseDiaData alike. 

811 if tuple(data.shape) != target_shape: 

812 data = data.reshape(*target_shape) 

813 

814 check_dims(dims, bdims, data.shape) 

815 

816 # NOTE: Constantly tidying up on Qarray creation might be a bit overkill. 

817 # It increases the compilation time, but only very slightly 

818 # increased the runtime of the jit compiled function. 

819 # We could instead use this tidy up where we think we need it. 

820 

821 impl_class = QarrayImplType(implementation).get_impl_class() 

822 impl = impl_class.from_data(data) 

823 impl = impl.tidy_up(SETTINGS["auto_tidyup_atol"]) 

824 

825 return cls(impl, qdims, bdims) 

826 

827 @staticmethod 

828 def _legacy_layout(data, dims, bdims): 

829 """Auto-resolve dims/bdims from data shape (no explicit ``qtype``). 

830 

831 Mirrors the historical shape heuristics: a 1‑D array is a ket, a square 

832 2‑D array is an operator, a non-square 2‑D array is a batch of kets, and 

833 legacy ``(N,1)`` / ``(1,N)`` carry the orientation. Returns 

834 ``(data, dims, bdims)`` where *data* may still carry a trailing singleton 

835 for vectors — the caller collapses it to ``(N,)``. 

836 """ 

837 if len(data.shape) == 1 and data.shape[0] > 0: 

838 data = data.reshape(data.shape[0], 1) 

839 

840 if ( 

841 len(data.shape) >= 2 

842 and data.shape[-2] != data.shape[-1] 

843 and not (data.shape[-2] == 1 or data.shape[-1] == 1) 

844 ): 

845 data = data.reshape(*data.shape[:-1], data.shape[-1], 1) 

846 

847 if bdims is not None and len(data.shape) - len(bdims) == 1: 

848 data = data.reshape(*data.shape[:-1], data.shape[-1], 1) 

849 

850 if bdims is None: 

851 bdims = tuple(data.shape[:-2]) 

852 

853 if dims is None: 

854 dims = ((data.shape[-2],), (data.shape[-1],)) 

855 

856 if not isinstance(dims[0], (list, tuple)): 

857 # Only the Hilbert space dimensions were sent in. 

858 if data.shape[-1] == 1: 

859 dims = (tuple(dims), tuple([1 for _ in dims])) 

860 elif data.shape[-2] == 1: 

861 dims = (tuple([1 for _ in dims]), tuple(dims)) 

862 else: 

863 dims = (tuple(dims), tuple(dims)) 

864 else: 

865 dims = (tuple(dims[0]), tuple(dims[1])) 

866 

867 return data, dims, tuple(bdims) 

868 

869 @staticmethod 

870 def _qtype_layout(data, dims, bdims, qtype): 

871 """Resolve dims/bdims given an explicit ``qtype`` (no shape guessing). 

872 

873 Accepts modern ``(...,N)`` vectors as well as legacy ``(N,1)`` / ``(1,N)`` 

874 (and batched variants) and returns ``(data, dims_full, bdims)``. The 

875 caller reshapes *data* to the canonical stored shape. 

876 """ 

877 shape = tuple(data.shape) 

878 dims_is_full = dims is not None and isinstance(dims[0], (list, tuple)) 

879 

880 if dims_is_full: 

881 dims_full = (tuple(dims[0]), tuple(dims[1])) 

882 elif dims is not None: 

883 sl = tuple(dims) 

884 if qtype == Qtypes.ket: 

885 dims_full = (sl, tuple(1 for _ in sl)) 

886 elif qtype == Qtypes.bra: 

887 dims_full = (tuple(1 for _ in sl), sl) 

888 else: 

889 dims_full = (sl, sl) 

890 else: 

891 dims_full = None # inferred from shape below 

892 

893 if qtype == Qtypes.oper: 

894 if dims_full is None: 

895 if len(shape) < 2: 

896 raise ValueError( 

897 f"qtype='oper' needs 2-D data or explicit dims; " 

898 f"got shape {shape}." 

899 ) 

900 dims_full = ((shape[-2],), (shape[-1],)) 

901 if bdims is None: 

902 bdims = shape[:-2] 

903 return data, dims_full, tuple(bdims) 

904 

905 # Vector (ket / bra). 

906 if bdims is not None: 

907 bdims = tuple(bdims) 

908 if dims_full is None: 

909 n = 1 

910 for d in shape[len(bdims):]: 

911 n *= d 

912 dims_full = ((n,), (1,)) if qtype == Qtypes.ket else ((1,), (n,)) 

913 return data, dims_full, bdims 

914 

915 # bdims unknown: peel a legacy orientation singleton, then the last axis 

916 # is the space axis and the leading axes are batch dims. 

917 s = list(shape) 

918 if qtype == Qtypes.ket and len(s) >= 2 and s[-1] == 1: 

919 s = s[:-1] 

920 elif qtype == Qtypes.bra and len(s) >= 2 and s[-2] == 1: 

921 s = s[:-2] + s[-1:] 

922 inferred_n = s[-1] if s else 1 

923 bdims = tuple(s[:-1]) 

924 if dims_full is None: 

925 dims_full = ((inferred_n,), (1,)) if qtype == Qtypes.ket else ((1,), (inferred_n,)) 

926 return data, dims_full, bdims 

927 

928 @classmethod 

929 @overload 

930 def from_sparse_bcoo(cls, data, dims=None, bdims=None) -> "Qarray[SparseBCOOImpl]": 

931 ... 

932 

933 @classmethod 

934 def from_sparse_bcoo(cls, data, dims=None, bdims=None): 

935 """Create a ``Qarray`` directly from a sparse BCOO array without densifying. 

936 

937 Args: 

938 data: A ``sparse.BCOO`` or array-like to store as sparse BCOO. 

939 dims: Quantum dimensions. Inferred when ``None``. 

940 bdims: Batch dimensions. Inferred when ``None``. 

941 

942 Returns: 

943 A ``Qarray[SparseBCOOImpl]``. 

944 """ 

945 return cls.create(data, dims=dims, bdims=bdims, implementation=QarrayImplType.SPARSE_BCOO) 

946 

947 @classmethod 

948 def from_sparse_dia(cls, data, dims=None, bdims=None) -> "Qarray": 

949 """Create a SparseDIA-backed ``Qarray``. 

950 

951 Accepts either a dense array-like (diagonals are auto-detected) or a 

952 :class:`~jaxquantum.core.sparse_dia.SparseDiaData` container. 

953 

954 Args: 

955 data: Dense array of shape (*batch, n, n) or a ``SparseDiaData``. 

956 dims: Quantum dimensions ``((row_dims,), (col_dims,))``. 

957 bdims: Batch dimension sizes. 

958 

959 Returns: 

960 A ``Qarray`` backed by ``SparseDiaImpl``. 

961 """ 

962 return cls.create(data, dims=dims, bdims=bdims, implementation=QarrayImplType.SPARSE_DIA) 

963 

964 @classmethod 

965 @overload 

966 def from_list(cls, qarr_list: List["Qarray[DenseImpl]"], qtype=None) -> "Qarray[DenseImpl]": 

967 ... 

968 

969 @classmethod 

970 @overload 

971 def from_list(cls, qarr_list: List["Qarray[SparseBCOOImpl]"], qtype=None) -> "Qarray[SparseBCOOImpl]": 

972 ... 

973 

974 @classmethod 

975 def from_list(cls, qarr_list: List[Qarray], qtype=None) -> Qarray: 

976 """Create a batched ``Qarray`` from a list of same-shaped ``Qarray`` objects. 

977 

978 The output implementation is determined by the element with the highest 

979 ``PROMOTION_ORDER``: if all inputs are sparse the result is sparse; if 

980 any input is dense (or types are mixed) all inputs are promoted to dense 

981 and the result is dense. 

982 

983 Works for kets/bras (stacked into ``(len, *bdims, N)``) as well as 

984 operators, regardless of whether the elements were originally created 

985 from ``(N,)`` or legacy ``(N,1)`` / ``(1,N)`` arrays — they are all 

986 stored as ``(N,)`` vectors by the time they reach here. 

987 

988 Args: 

989 qarr_list: List of ``Qarray`` objects with identical ``dims`` and 

990 ``bdims``. May be empty. 

991 qtype: Optional quantum type ("ket"/"bra"/"oper" or a ``Qtypes``) 

992 forwarded to :meth:`create`. Defaults to the qtype of the 

993 first element, which is the correct choice in all normal cases; 

994 pass it explicitly to override or to be defensive. 

995 

996 Returns: 

997 A ``Qarray`` with an extra leading batch dimension of size 

998 ``len(qarr_list)``. 

999 

1000 Raises: 

1001 ValueError: If the elements have mismatched ``dims`` or ``bdims``. 

1002 """ 

1003 if len(qarr_list) == 0: 

1004 dims = ((), ()) 

1005 bdims = (0,) 

1006 return cls.create(jnp.array([]), dims=dims, bdims=bdims) 

1007 

1008 dims = qarr_list[0].dims 

1009 bdims = qarr_list[0].bdims 

1010 

1011 if not all(qarr.dims == dims and qarr.bdims == bdims for qarr in qarr_list): 

1012 raise ValueError("All Qarrays in the list must have the same dimensions.") 

1013 

1014 if qtype is None: 

1015 qtype = qarr_list[0].qtype 

1016 

1017 new_bdims = (len(qarr_list),) + bdims 

1018 

1019 # Pick the target type: highest PROMOTION_ORDER wins (dense beats sparse). 

1020 target_impl_type = max( 

1021 (q.impl_type for q in qarr_list), 

1022 key=lambda t: t.get_impl_class().PROMOTION_ORDER, 

1023 ) 

1024 

1025 if target_impl_type == QarrayImplType.SPARSE_DIA: 

1026 # All inputs are SparseDIA — batch without densifying. 

1027 # Compute union of offsets across all operators, then remap each 

1028 # operator's _diags rows into the union shape and stack. 

1029 from jaxquantum.core.sparse_dia import SparseDiaData # lazy to avoid circular 

1030 union_offsets = tuple(sorted( 

1031 set().union(*[set(q._impl._offsets) for q in qarr_list]) 

1032 )) 

1033 union_idx = {k: i for i, k in enumerate(union_offsets)} 

1034 n = qarr_list[0]._impl._diags.shape[-1] 

1035 dtype = jnp.result_type(*[q._impl._diags.dtype for q in qarr_list]) 

1036 remapped = [] 

1037 for q in qarr_list: 

1038 row = jnp.zeros((len(union_offsets), n), dtype=dtype) 

1039 for i_src, k in enumerate(q._impl._offsets): 

1040 row = row.at[union_idx[k], :].set(q._impl._diags[i_src, :]) 

1041 remapped.append(row) 

1042 stacked = jnp.stack(remapped, axis=0) # (n_ops, n_union_diags, N) 

1043 raw = SparseDiaData(offsets=union_offsets, diags=stacked) 

1044 return cls.create(raw, dims=dims, bdims=new_bdims, qtype=qtype, implementation=QarrayImplType.SPARSE_DIA) 

1045 

1046 if target_impl_type == QarrayImplType.SPARSE_BCOO: 

1047 # All inputs are sparse BCOO — stack via dense intermediates then re-sparsify. 

1048 data = jnp.array([q.data.todense() for q in qarr_list]) 

1049 return cls.create(data, dims=dims, bdims=new_bdims, qtype=qtype, implementation=QarrayImplType.SPARSE_BCOO) 

1050 

1051 # Target is dense: promote any sparse inputs before stacking. 

1052 data = jnp.array([q.to_dense().data for q in qarr_list]) 

1053 return cls.create(data, dims=dims, bdims=new_bdims, qtype=qtype, implementation=QarrayImplType.DENSE) 

1054 

1055 @classmethod 

1056 @overload 

1057 def from_array(cls, qarr_arr: "Qarray[DenseImpl]") -> "Qarray[DenseImpl]": 

1058 ... 

1059 

1060 @classmethod 

1061 @overload 

1062 def from_array(cls, qarr_arr: "Qarray[SparseBCOOImpl]") -> "Qarray[SparseBCOOImpl]": 

1063 ... 

1064 

1065 @classmethod 

1066 def from_array(cls, qarr_arr) -> Qarray: 

1067 """Create a ``Qarray`` from a (possibly nested) list of ``Qarray`` objects. 

1068 

1069 Args: 

1070 qarr_arr: A ``Qarray`` (returned as-is) or a nested list of 

1071 ``Qarray`` objects. 

1072 

1073 Returns: 

1074 A ``Qarray`` with batch dimensions matching the nesting structure 

1075 of *qarr_arr*. 

1076 """ 

1077 if isinstance(qarr_arr, Qarray): 

1078 return qarr_arr 

1079 

1080 bdims = () 

1081 lvl = qarr_arr 

1082 while not isinstance(lvl, Qarray): 

1083 bdims = bdims + (len(lvl),) 

1084 if len(lvl) > 0: 

1085 lvl = lvl[0] 

1086 else: 

1087 break 

1088 

1089 def flat(lis): 

1090 flatList = [] 

1091 for element in lis: 

1092 if type(element) is list: 

1093 flatList += flat(element) 

1094 else: 

1095 flatList.append(element) 

1096 return flatList 

1097 

1098 qarr_list = flat(qarr_arr) 

1099 qarr = cls.from_list(qarr_list) 

1100 qarr = qarr.reshape_bdims(*bdims) 

1101 return qarr 

1102 

1103 # Properties ---- 

1104 @property 

1105 def qtype(self): 

1106 """Quantum type of this array (ket, bra, or operator).""" 

1107 return self._qdims.qtype 

1108 

1109 @property 

1110 def dtype(self): 

1111 """Data type of the underlying storage array.""" 

1112 return self._impl.dtype() 

1113 

1114 @property 

1115 def dims(self): 

1116 """Quantum dimensions as ``((row_dims...), (col_dims...))``.""" 

1117 return self._qdims.dims 

1118 

1119 @property 

1120 def bdims(self): 

1121 """Tuple of batch dimension sizes (empty tuple = non-batched).""" 

1122 return self._bdims 

1123 

1124 @property 

1125 def qdims(self): 

1126 """The ``Qdims`` metadata object for this array.""" 

1127 return self._qdims 

1128 

1129 @property 

1130 def space_dims(self): 

1131 """Hilbert space dimensions for the relevant side (ket row / bra col).""" 

1132 if self.qtype in [Qtypes.oper, Qtypes.ket]: 

1133 return self.dims[0] 

1134 elif self.qtype == Qtypes.bra: 

1135 return self.dims[1] 

1136 else: 

1137 # TODO: not reached for some reason 

1138 raise ValueError("Unsupported qtype.") 

1139 

1140 @property 

1141 def data(self): 

1142 """The raw underlying data (dense ``jnp.ndarray`` or ``sparse.BCOO``).""" 

1143 return self._impl.data 

1144 

1145 @property 

1146 def shaped_data(self): 

1147 """Data reshaped to ``bdims + dims[0] + dims[1]``.""" 

1148 return self.data.reshape(self.bdims + self.dims[0] + self.dims[1]) 

1149 

1150 @property 

1151 def shape(self): 

1152 """Shape of the underlying data array.""" 

1153 return self.data.shape 

1154 

1155 @property 

1156 def is_batched(self): 

1157 """True if this array has one or more batch dimensions.""" 

1158 return len(self.bdims) > 0 

1159 

1160 @property 

1161 def is_sparse_bcoo(self): 

1162 """True if the storage backend is ``SparseBCOOImpl`` (BCOO).""" 

1163 return self._impl.impl_type == QarrayImplType.SPARSE_BCOO 

1164 

1165 @property 

1166 def is_dense(self): 

1167 """True if the storage backend is ``DenseImpl``.""" 

1168 return self._impl.impl_type == QarrayImplType.DENSE 

1169 

1170 @property 

1171 def is_sparse_dia(self): 

1172 """True if the storage backend is ``SparseDiaImpl``.""" 

1173 return self._impl.impl_type == QarrayImplType.SPARSE_DIA 

1174 

1175 @property 

1176 def impl_type(self): 

1177 """The ``QarrayImplType`` member of the current storage backend.""" 

1178 return self._impl.impl_type 

1179 

1180 def to_sparse_bcoo(self) -> "Qarray[SparseBCOOImpl]": 

1181 """Return a BCOO-sparse-backed copy of this array. 

1182 

1183 If the array is already sparse BCOO, returns self unchanged. 

1184 

1185 Returns: 

1186 A ``Qarray[SparseBCOOImpl]``. 

1187 """ 

1188 if self.is_sparse_bcoo: 

1189 return self 

1190 new_impl = self._impl.to_sparse_bcoo() 

1191 return Qarray(new_impl, self._qdims, self._bdims) 

1192 

1193 def to_sparse_dia(self) -> "Qarray": 

1194 """Return a SparseDIA-backed copy of this array. 

1195 

1196 If the array is already SparseDIA, returns self unchanged. 

1197 

1198 Returns: 

1199 A ``Qarray[SparseDiaImpl]``. 

1200 """ 

1201 if self.is_sparse_dia: 

1202 return self 

1203 new_impl = self._impl.to_sparse_dia() 

1204 return Qarray(new_impl, self._qdims, self._bdims) 

1205 

1206 def to_dense(self) -> "Qarray[DenseImpl]": 

1207 """Return a dense-backed copy of this array. 

1208 

1209 If the array is already dense, returns self unchanged. 

1210 

1211 Returns: 

1212 A ``Qarray[DenseImpl]``. 

1213 """ 

1214 if self.is_dense: 

1215 return self 

1216 new_impl = self._impl.to_dense() 

1217 return Qarray(new_impl, self._qdims, self._bdims) 

1218 

1219 def __getitem__(self, index): 

1220 if len(self.bdims) > 0: 

1221 impl = type(self._impl).from_data(self.data[index]) 

1222 return Qarray._from_impl(impl, self._qdims) 

1223 else: 

1224 raise ValueError("Cannot index a non-batched Qarray.") 

1225 

1226 def reshape_bdims(self, *args): 

1227 """Reshape the batch dimensions of this ``Qarray``. 

1228 

1229 Args: 

1230 *args: New batch dimension sizes. 

1231 

1232 Returns: 

1233 A new ``Qarray`` with the requested batch shape. 

1234 """ 

1235 new_bdims = tuple(args) 

1236 

1237 if prod(new_bdims) == 0: 

1238 new_shape = new_bdims 

1239 elif self.qtype == Qtypes.oper: 

1240 new_shape = new_bdims + (prod(self.dims[0]), -1) 

1241 else: 

1242 # Vectors keep a single trailing space axis (no (N,1)). 

1243 new_shape = new_bdims + (prod(self.dims[0]) * prod(self.dims[1]),) 

1244 

1245 impl = type(self._impl).from_data(self.data.reshape(new_shape)) 

1246 return Qarray._from_impl( 

1247 impl, 

1248 self._qdims, 

1249 new_bdims, 

1250 ) 

1251 

1252 def space_to_qdims(self, space_dims: List[int]): 

1253 """Convert Hilbert space dimensions to full quantum dims tuple. 

1254 

1255 Args: 

1256 space_dims: Sequence of per-subsystem Hilbert space sizes, or a 

1257 full ``((row_dims), (col_dims))`` tuple (returned unchanged). 

1258 

1259 Returns: 

1260 A ``((row_dims...), (col_dims...))`` tuple. 

1261 

1262 Raises: 

1263 ValueError: If ``self.qtype`` is not ket, bra, or oper. 

1264 """ 

1265 if isinstance(space_dims[0], (list, tuple)): 

1266 return space_dims 

1267 

1268 if self.qtype == Qtypes.oper: 

1269 return (tuple(space_dims), tuple(space_dims)) 

1270 elif self.qtype == Qtypes.ket: 

1271 return (tuple(space_dims), tuple([1 for _ in range(len(space_dims))])) 

1272 elif self.qtype == Qtypes.bra: 

1273 return (tuple([1 for _ in range(len(space_dims))]), tuple(space_dims)) 

1274 else: 

1275 raise ValueError("Unsupported qtype for space_to_qdims conversion.") 

1276 

1277 def reshape_qdims(self, *args): 

1278 """Reshape the quantum dimensions of the Qarray. 

1279 

1280 Note that this does not take in qdims but rather the new Hilbert space 

1281 dimensions. 

1282 

1283 Args: 

1284 *args: New Hilbert dimensions for the Qarray. 

1285 

1286 Returns: 

1287 Qarray: reshaped Qarray. 

1288 """ 

1289 

1290 new_space_dims = tuple(args) 

1291 current_space_dims = self.space_dims 

1292 assert prod(new_space_dims) == prod(current_space_dims) 

1293 

1294 new_qdims = self.space_to_qdims(new_space_dims) 

1295 return Qarray._from_impl(self._impl, Qdims(new_qdims), self._bdims) 

1296 

1297 def resize(self, new_shape): 

1298 """Resize the Qarray to a new shape. 

1299 

1300 TODO: review and maybe deprecate this method. 

1301 

1302 Args: 

1303 new_shape: Target shape tuple. 

1304 

1305 Returns: 

1306 A new ``Qarray`` with data resized via ``jnp.resize``. 

1307 """ 

1308 dims = self.dims 

1309 data = jnp.resize(self.data, new_shape) 

1310 # Preserve implementation type 

1311 implementation = self.impl_type 

1312 return Qarray.create( 

1313 data, 

1314 dims=dims, 

1315 implementation=implementation, 

1316 ) 

1317 

1318 def __len__(self): 

1319 """Length along the first batch dimension. 

1320 

1321 Returns: 

1322 Size of the leading batch dimension. 

1323 

1324 Raises: 

1325 ValueError: If the array is not batched. 

1326 """ 

1327 if len(self.bdims) > 0: 

1328 return self.data.shape[0] 

1329 else: 

1330 raise ValueError("Cannot get length of a non-batched Qarray.") 

1331 

1332 def __eq__(self, other): 

1333 if not isinstance(other, Qarray): 

1334 raise ValueError( # noqa: TRY004 

1335 "Cannot calculate equality of a Qarray with a non-Qarray." 

1336 ) 

1337 

1338 if self.dims != other.dims: 

1339 return False 

1340 

1341 if self.bdims != other.bdims: 

1342 return False 

1343 

1344 if self.is_sparse_bcoo and other.is_sparse_bcoo: 

1345 # Fast structural path: same sparsity pattern → compare values only (no todense) 

1346 if (self.data.indices.shape == other.data.indices.shape 

1347 and bool(jnp.all(self.data.indices == other.data.indices))): 

1348 return bool(jnp.allclose(self.data.data, other.data.data)) 

1349 # Different patterns: fall back to dense comparison (unavoidable) 

1350 return bool(jnp.all(self.data.todense() == other.data.todense())) 

1351 

1352 # At least one dense: convert sparse side to dense for comparison 

1353 self_data = self.data.todense() if hasattr(self.data, 'todense') else self.data 

1354 other_data = other.data.todense() if hasattr(other.data, 'todense') else other.data 

1355 return bool(jnp.all(self_data == other_data)) 

1356 

1357 def __ne__(self, other): 

1358 return not self.__eq__(other) 

1359 

1360 # Elementary Math ---- 

1361 def __matmul__(self, other): 

1362 if not isinstance(other, Qarray): 

1363 return NotImplemented 

1364 

1365 _qdims_new = self._qdims @ other._qdims 

1366 st, ot = self.qtype, other.qtype 

1367 

1368 # Operator @ operator: keep the backend-native path (dense or sparse), 

1369 # which preserves the storage implementation. Operators are 2-D-tailed, 

1370 # so create() infers bdims/qtype from the result shape unambiguously. 

1371 if st == Qtypes.oper and ot == Qtypes.oper: 

1372 new_impl = self._impl.matmul(other._impl) 

1373 return Qarray._from_impl(new_impl, _qdims_new) 

1374 

1375 # bra @ oper == dag( oper^dag @ bra^dag ). Reusing the oper @ ket path 

1376 # keeps a sparse operator sparse (no densification of the operator). 

1377 if st == Qtypes.bra and ot == Qtypes.oper: 

1378 return (other.dag() @ self.dag()).dag() 

1379 

1380 # oper @ ket: matrix-vector product. Vectors are dense (see migration 

1381 # scope); the operator may be sparse and acts natively on the dense 

1382 # vector. The vector's single space axis is contracted in place — it is 

1383 # never reshaped to (N,1) for storage. 

1384 if st == Qtypes.oper and ot == Qtypes.ket: 

1385 vec = other.to_dense().data # (..., N) 

1386 if self.is_dense: 

1387 out = jnp.einsum("...ij,...j->...i", self.data, vec) 

1388 else: 

1389 # Transient column only inside the sparse kernel; never stored. 

1390 rhs = vec[..., None] # (..., N, 1) 

1391 out = self._impl.matmul(DenseImpl._make(rhs)).data[..., 0] 

1392 return Qarray._from_impl(DenseImpl._make(out), _qdims_new) 

1393 

1394 # Vector ⊗/· vector — both operands are dense. 

1395 a = self.to_dense().data 

1396 b = other.to_dense().data 

1397 if st == Qtypes.ket and ot == Qtypes.bra: 

1398 # Outer product |a><b| → operator. 

1399 out = jnp.einsum("...i,...j->...ij", a, b) 

1400 return Qarray._from_impl(DenseImpl._make(out), _qdims_new) 

1401 if st == Qtypes.bra and ot == Qtypes.ket: 

1402 # Inner product <a|b> → 1x1 "ket" (qdims ((1,),(1,))). 

1403 out = jnp.einsum("...i,...i->...", a, b) 

1404 out = out.reshape(out.shape + (1,)) 

1405 return Qarray._from_impl(DenseImpl._make(out), _qdims_new) 

1406 

1407 return NotImplemented 

1408 

1409 def __mul__(self, other): 

1410 if isinstance(other, Qarray): 

1411 return self.__matmul__(other) 

1412 

1413 other = other + 0.0j 

1414 if not robust_isscalar(other) and len(other.shape) > 0: # not a scalar 

1415 # Broadcast per-batch scalars against the stored data: vectors carry 

1416 # a single trailing space axis, operators carry two. 

1417 extra = (1,) if self.qtype in (Qtypes.ket, Qtypes.bra) else (1, 1) 

1418 other = other.reshape(other.shape + extra) 

1419 

1420 new_impl = self._impl.mul(other) 

1421 return Qarray._from_impl(new_impl, self._qdims) 

1422 

1423 def __rmul__(self, other): 

1424 return self.__mul__(other) 

1425 

1426 def __neg__(self): 

1427 return self.__mul__(-1) 

1428 

1429 def __truediv__(self, other): 

1430 """Divide by a scalar. 

1431 

1432 Args: 

1433 other: Scalar divisor. 

1434 

1435 Returns: 

1436 A new ``Qarray`` with all elements divided by *other*. 

1437 

1438 Raises: 

1439 ValueError: If *other* is a ``Qarray``. 

1440 """ 

1441 if isinstance(other, Qarray): 

1442 raise ValueError("Cannot divide a Qarray by another Qarray.") # noqa: TRY004 

1443 

1444 return self.__mul__(1 / other) 

1445 

1446 def _scaled_identity(self, scalar): 

1447 return type(self._impl)._scaled_identity( 

1448 self.data.shape[-2], 

1449 scalar, 

1450 dtype=self.data.dtype, 

1451 ) 

1452 

1453 def __add__(self, other): 

1454 if isinstance(other, Qarray): 

1455 if self.dims != other.dims: 

1456 msg = ( 

1457 "Dimensions are incompatible: " 

1458 + repr(self.dims) 

1459 + " and " 

1460 + repr(other.dims) 

1461 ) 

1462 raise ValueError(msg) 

1463 new_impl = self._impl.add(other._impl) 

1464 return Qarray._from_impl(new_impl, self._qdims) 

1465 

1466 if isinstance(other, Number) and other == 0: 

1467 return self.copy() 

1468 

1469 if self.qtype == Qtypes.oper: 

1470 return Qarray._from_impl( 

1471 self._impl.add(self._scaled_identity(other)), 

1472 self._qdims, 

1473 ) 

1474 

1475 return NotImplemented 

1476 

1477 def __radd__(self, other): 

1478 return self.__add__(other) 

1479 

1480 def __sub__(self, other): 

1481 if isinstance(other, Qarray): 

1482 if self.dims != other.dims: 

1483 msg = ( 

1484 "Dimensions are incompatible: " 

1485 + repr(self.dims) 

1486 + " and " 

1487 + repr(other.dims) 

1488 ) 

1489 raise ValueError(msg) 

1490 new_impl = self._impl.sub(other._impl) 

1491 return Qarray._from_impl(new_impl, self._qdims) 

1492 

1493 if isinstance(other, Number) and other == 0: 

1494 return self.copy() 

1495 

1496 if self.qtype == Qtypes.oper: 

1497 return Qarray._from_impl( 

1498 self._impl.sub(self._scaled_identity(other)), 

1499 self._qdims, 

1500 ) 

1501 

1502 return NotImplemented 

1503 

1504 def __rsub__(self, other): 

1505 return self.__neg__().__add__(other) 

1506 

1507 def __xor__(self, other): 

1508 if not isinstance(other, Qarray): 

1509 return NotImplemented 

1510 return tensor(self, other) 

1511 

1512 def __rxor__(self, other): 

1513 if not isinstance(other, Qarray): 

1514 return NotImplemented 

1515 return tensor(other, self) 

1516 

1517 def __pow__(self, other): 

1518 if not isinstance(other, int): 

1519 return NotImplemented 

1520 

1521 return powm(self, other) 

1522 

1523 # String Representation ---- 

1524 def _str_header(self): 

1525 """Build the one-line header string for ``__str__`` and ``__repr__``.""" 

1526 impl_type = self.impl_type.value 

1527 out = ", ".join( 

1528 [ 

1529 "Quantum array: dims = " + str(self.dims), 

1530 "bdims = " + str(self.bdims), 

1531 "shape = " + str(self.data.shape), 

1532 "type = " + str(self.qtype), 

1533 "impl = " + impl_type, 

1534 ] 

1535 ) 

1536 return out 

1537 

1538 def __str__(self): 

1539 return self._str_header() + "\nQarray data =\n" + str(self.data) 

1540 

1541 @property 

1542 def header(self): 

1543 """One-line header string describing dimensions, shape, and backend.""" 

1544 return self._str_header() 

1545 

1546 def __repr__(self): 

1547 return self.__str__() 

1548 

1549 # Utilities ---- 

1550 def copy(self, memo=None): 

1551 """Return a deep copy of this ``Qarray``. 

1552 

1553 Args: 

1554 memo: Optional memo dict forwarded to ``deepcopy``. 

1555 

1556 Returns: 

1557 A new ``Qarray`` with independent copies of all data. 

1558 """ 

1559 return self.__deepcopy__(memo) 

1560 

1561 def __deepcopy__(self, memo): 

1562 """Need to override this when defining __getattr__.""" 

1563 

1564 return Qarray( 

1565 _impl=deepcopy(self._impl, memo=memo), 

1566 _qdims=deepcopy(self._qdims, memo=memo), 

1567 _bdims=deepcopy(self._bdims, memo=memo), 

1568 ) 

1569 

1570 def __getattr__(self, method_name): 

1571 if "__" == method_name[:2]: 

1572 # NOTE: we return NotImplemented for binary special methods logic in python, plus things like __jax_array__ 

1573 return lambda *args, **kwargs: NotImplemented 

1574 

1575 modules = [jnp, jnp.linalg, jsp, jsp.linalg] 

1576 

1577 method_f = None 

1578 for mod in modules: 

1579 method_f = getattr(mod, method_name, None) 

1580 if method_f is not None: 

1581 break 

1582 

1583 if method_f is None: 

1584 raise NotImplementedError( 

1585 f"Method {method_name} does not exist. No backup method found in {modules}." 

1586 ) 

1587 

1588 def func(*args, **kwargs): 

1589 # For operations that might not be supported in sparse, convert to dense 

1590 if self.is_sparse_bcoo: 

1591 dense_self = self.to_dense() 

1592 res = method_f(dense_self.data, *args, **kwargs) 

1593 else: 

1594 res = method_f(self.data, *args, **kwargs) 

1595 

1596 if getattr(res, "shape", None) is None or res.shape != self.data.shape: 

1597 return res 

1598 else: 

1599 # Preserve implementation type 

1600 return Qarray.create(res, dims=self._qdims.dims, implementation=self.impl_type) 

1601 

1602 return func 

1603 

1604 # Conversions / Reshaping ---- 

1605 def dag(self): 

1606 """Conjugate transpose of this array.""" 

1607 return dag(self) 

1608 

1609 def to_dm(self): 

1610 """Convert a ket to a density matrix via outer product.""" 

1611 return ket2dm(self) 

1612 

1613 def is_dm(self): 

1614 """Return True if this array is an operator (density-matrix type).""" 

1615 return self.qtype == Qtypes.oper 

1616 

1617 def is_vec(self): 

1618 """Return True if this array is a ket or bra.""" 

1619 return self.qtype == Qtypes.ket or self.qtype == Qtypes.bra 

1620 

1621 def to_ket(self): 

1622 """Convert a bra to a ket (no-op for kets).""" 

1623 return to_ket(self) 

1624 

1625 def transpose(self, *args): 

1626 """Transpose subsystem indices.""" 

1627 return transpose(self, *args) 

1628 

1629 def keep_only_diag_elements(self): 

1630 """Zero out all off-diagonal elements.""" 

1631 return keep_only_diag_elements(self) 

1632 

1633 # Math Functions ---- 

1634 def unit(self): 

1635 """Return the normalised (unit-norm) version of this array.""" 

1636 return unit(self) 

1637 

1638 def norm(self): 

1639 """Compute the norm of this array.""" 

1640 return norm(self) 

1641 

1642 def frobenius_norm(self): 

1643 """Compute the Frobenius norm directly from the implementation. 

1644 

1645 Returns: 

1646 The Frobenius norm as a scalar. 

1647 """ 

1648 return self._impl.frobenius_norm() 

1649 

1650 def real(self): 

1651 """Element-wise real part. 

1652 

1653 Returns: 

1654 A new ``Qarray`` containing the real parts of each element. 

1655 """ 

1656 new_impl = self._impl.real() 

1657 return Qarray._from_impl(new_impl, self._qdims) 

1658 

1659 def imag(self): 

1660 """Element-wise imaginary part. 

1661 

1662 Returns: 

1663 A new ``Qarray`` containing the imaginary parts of each element. 

1664 """ 

1665 new_impl = self._impl.imag() 

1666 return Qarray._from_impl(new_impl, self._qdims) 

1667 

1668 def conj(self): 

1669 """Element-wise complex conjugate. 

1670 

1671 Returns: 

1672 A new ``Qarray`` containing the complex-conjugated elements. 

1673 """ 

1674 new_impl = self._impl.conj() 

1675 return Qarray._from_impl(new_impl, self._qdims) 

1676 

1677 def expm(self): 

1678 """Matrix exponential.""" 

1679 return expm(self) 

1680 

1681 def powm(self, n): 

1682 """Matrix power. 

1683 

1684 Args: 

1685 n: Exponent (integer or float). 

1686 

1687 Returns: 

1688 This array raised to the *n*-th matrix power. 

1689 """ 

1690 return powm(self, n) 

1691 

1692 def cosm(self): 

1693 """Matrix cosine.""" 

1694 return cosm(self) 

1695 

1696 def sinm(self): 

1697 """Matrix sine.""" 

1698 return sinm(self) 

1699 

1700 def tr(self, **kwargs): 

1701 """Full trace.""" 

1702 return tr(self, **kwargs) 

1703 

1704 def trace(self, **kwargs): 

1705 """Full trace (alias for :meth:`tr`).""" 

1706 return tr(self, **kwargs) 

1707 

1708 def ptrace(self, indx): 

1709 """Partial trace over subsystem *indx*. 

1710 

1711 Args: 

1712 indx: Index of the subsystem to trace out. 

1713 

1714 Returns: 

1715 Reduced density matrix. 

1716 """ 

1717 return ptrace(self, indx) 

1718 

1719 def eigenstates(self): 

1720 """Eigenvalues and eigenstates of this operator.""" 

1721 return eigenstates(self) 

1722 

1723 def eigenenergies(self): 

1724 """Eigenvalues of this operator.""" 

1725 return eigenenergies(self) 

1726 

1727 def eigenvalues(self): 

1728 """Eigenvalues of this operator (alias for :meth:`eigenenergies`).""" 

1729 return eigenenergies(self) 

1730 

1731 def collapse(self, mode="sum"): 

1732 """Collapse batch dimensions. 

1733 

1734 Args: 

1735 mode: Collapse strategy — currently only ``"sum"`` is supported. 

1736 

1737 Returns: 

1738 A non-batched ``Qarray``. 

1739 """ 

1740 return collapse(self, mode=mode) 

1741 

1742 

1743# Qarray operations --------------------------------------------------------------------- 

1744 

1745def concatenate(qarr_list: List[Qarray], axis: int = 0) -> Qarray: 

1746 """Concatenate a list of Qarrays along a specified axis. 

1747 

1748 Args: 

1749 qarr_list: List of Qarrays to concatenate. 

1750 axis: Axis along which to concatenate. Default is 0. 

1751 

1752 Returns: 

1753 Concatenated Qarray. 

1754 """ 

1755 

1756 non_empty_qarr_list = [qarr for qarr in qarr_list if len(qarr.data) != 0] 

1757 

1758 if len(non_empty_qarr_list) == 0: 

1759 return Qarray.from_list([]) 

1760 

1761 concatenated_data = jnp.concatenate( 

1762 [qarr.data for qarr in non_empty_qarr_list], axis=axis 

1763 ) 

1764 

1765 dims = non_empty_qarr_list[0].dims 

1766 return Qarray.create(concatenated_data, dims=dims) 

1767 

1768 

1769def collapse(qarr: Qarray, mode="sum") -> Qarray: 

1770 """Collapse the batch dimensions of *qarr*. 

1771 

1772 Args: 

1773 qarr: Quantum array with optional batch dimensions. 

1774 mode: Collapse strategy. Only ``"sum"`` is currently supported. 

1775 

1776 Returns: 

1777 A non-batched ``Qarray`` obtained by summing over all batch axes. 

1778 """ 

1779 

1780 if mode == "sum": 

1781 if len(qarr.bdims) == 0: 

1782 return qarr 

1783 

1784 batch_axes = list(range(len(qarr.bdims))) 

1785 

1786 # Preserve implementation type 

1787 implementation = qarr.impl_type 

1788 return Qarray.create(jnp.sum(qarr.data, axis=batch_axes), dims=qarr.dims, implementation=implementation) 

1789 

1790 

1791def transpose(qarr: Qarray, indices: List[int]) -> Qarray: 

1792 """Transpose subsystem indices of the quantum array. 

1793 

1794 Args: 

1795 qarr: Input quantum array. 

1796 indices: New ordering of subsystem indices. 

1797 

1798 Returns: 

1799 Transposed ``Qarray`` (converted to dense first). 

1800 """ 

1801 

1802 qarr = qarr.to_dense() 

1803 

1804 indices = list(indices) 

1805 

1806 shaped_data = qarr.shaped_data 

1807 dims = qarr.dims 

1808 bdims_indxs = list(range(len(qarr.bdims))) 

1809 

1810 reshape_indices = indices + [j + len(dims[0]) for j in indices] 

1811 reshape_indices = bdims_indxs + [j + len(bdims_indxs) for j in reshape_indices] 

1812 

1813 shaped_data = shaped_data.transpose(reshape_indices) 

1814 new_dims = ( 

1815 tuple([dims[0][j] for j in indices]), 

1816 tuple([dims[1][j] for j in indices]), 

1817 ) 

1818 

1819 full_dims = prod(dims[0]) 

1820 full_data = shaped_data.reshape(*qarr.bdims, full_dims, -1) 

1821 

1822 # Preserve implementation type 

1823 implementation = qarr.impl_type 

1824 return Qarray.create(full_data, dims=new_dims, implementation=implementation) 

1825 

1826 

1827def unit(qarr: Qarray) -> Qarray: 

1828 """Normalize *qarr* to unit norm. 

1829 

1830 Args: 

1831 qarr: Input quantum array. 

1832 

1833 Returns: 

1834 Normalized quantum array. 

1835 """ 

1836 return qarr / qarr.norm() 

1837 

1838 

1839def norm(qarr: Qarray) -> float: 

1840 """Compute the norm of a quantum array. 

1841 

1842 Sparse paths (no densification): 

1843 

1844 * ket / bra — L2 norm via :meth:`SparseBCOOImpl.l2_norm_batched` (handles 

1845 batch dimensions). 

1846 * operator — trace norm assuming PSD (nuclear norm = tr(rho) for density 

1847 matrices). This is exact for density matrices; for general non-PSD 

1848 operators convert to dense first. 

1849 

1850 Args: 

1851 qarr: Input quantum array. 

1852 

1853 Returns: 

1854 The norm as a scalar (or batched array of scalars). 

1855 """ 

1856 if qarr.qtype in [Qtypes.ket, Qtypes.bra] and qarr.is_sparse_bcoo: 

1857 return qarr._impl.l2_norm_batched(qarr.bdims) 

1858 

1859 if qarr.qtype == Qtypes.oper and qarr.is_sparse_bcoo: 

1860 # Nuclear norm = trace for positive-semidefinite (density matrix) operators. 

1861 # jnp.real strips any floating-point imaginary artefact. 

1862 return jnp.real(qarr._impl.trace()) 

1863 

1864 if qarr.qtype == Qtypes.oper and qarr.is_sparse_dia: 

1865 return jnp.real(qarr._impl.trace()) 

1866 

1867 qarr = qarr.to_dense() 

1868 

1869 qdata = qarr.data 

1870 

1871 if qarr.qtype == Qtypes.oper: 

1872 if default_backend() == "cpu": 

1873 return jnp.sum(jnp.linalg.svd(qdata, compute_uv=False), axis=-1) 

1874 gram = qdata @ jnp.swapaxes(jnp.conj(qdata), -1, -2) 

1875 values = jnp.linalg.eigvalsh(gram) 

1876 return jnp.sum(jnp.sqrt(jnp.abs(values)), axis=-1) 

1877 

1878 elif qarr.qtype in [Qtypes.ket, Qtypes.bra]: 

1879 # Vectors store the Hilbert space on the single trailing axis. 

1880 return jnp.sqrt(jnp.sum(jnp.abs(qdata) ** 2, axis=-1)) 

1881 

1882 

1883def tensor(*args, **kwargs) -> Qarray: 

1884 """Tensor (Kronecker) product of two or more ``Qarray`` objects. 

1885 

1886 Args: 

1887 *args: ``Qarray`` objects to tensor together (left to right). 

1888 **kwargs: Optional keyword arguments. Pass ``parallel=True`` to use 

1889 an einsum-based batched outer product instead of ``jnp.kron``. 

1890 

1891 Returns: 

1892 The tensor product as a ``Qarray``. The output implementation is 

1893 determined by the highest ``PROMOTION_ORDER`` among the inputs: all-sparse 

1894 inputs → sparse output; any dense input → dense output. This holds for 

1895 both ``parallel=True`` and ``parallel=False``. 

1896 

1897 Note: 

1898 ``parallel=True`` uses an einsum-based batched outer product. The 

1899 einsum is always computed on dense data for efficiency, but the result 

1900 is then wrapped in the appropriate backend (sparse when all inputs are 

1901 sparse, dense otherwise). For the default (``parallel=False``) path 

1902 each backend's ``kron`` method is used directly. 

1903 """ 

1904 parallel = kwargs.pop("parallel", False) 

1905 

1906 if parallel: 

1907 # Determine target implementation: highest PROMOTION_ORDER wins. 

1908 # All-sparse → sparse; any dense input → dense (same rule as non-parallel). 

1909 target_impl_type = max( 

1910 (arg.impl_type for arg in args), 

1911 key=lambda t: t.get_impl_class().PROMOTION_ORDER, 

1912 ) 

1913 # Einsum-based batched outer product (computed on dense data). 

1914 dense_args = [arg.to_dense() for arg in args] 

1915 # Vectors (kets/bras) carry one trailing space axis; operators carry two. 

1916 # Tensoring requires a consistent qtype across args (mixed is invalid). 

1917 is_vec = dense_args[0].qtype in (Qtypes.ket, Qtypes.bra) 

1918 n_space = 1 if is_vec else 2 

1919 data = dense_args[0].data 

1920 dims_0 = dense_args[0].dims[0] 

1921 dims_1 = dense_args[0].dims[1] 

1922 for arg in dense_args[1:]: 

1923 a, b = data, arg.data 

1924 ba, bb = a.shape[:-n_space], b.shape[:-n_space] 

1925 if len(ba) > len(bb): 

1926 batch_dim = ba 

1927 elif len(ba) == len(bb): 

1928 batch_dim = ba if prod(ba) >= prod(bb) else bb 

1929 else: 

1930 batch_dim = bb 

1931 

1932 if is_vec: 

1933 # (..., N) ⊗ (..., M) -> (..., N*M) 

1934 data = jnp.einsum("...i,...j->...ij", a, b).reshape( 

1935 *batch_dim, a.shape[-1] * b.shape[-1] 

1936 ) 

1937 else: 

1938 # (..., M, N) ⊗ (..., K, L) -> (..., M*K, N*L) 

1939 data = jnp.einsum("...ij,...kl->...ikjl", a, b).reshape( 

1940 *batch_dim, a.shape[-2] * b.shape[-2], -1 

1941 ) 

1942 dims_0 = dims_0 + arg.dims[0] 

1943 dims_1 = dims_1 + arg.dims[1] 

1944 impl = target_impl_type.get_impl_class().from_data(data) 

1945 return Qarray._from_impl(impl, Qdims((dims_0, dims_1))) 

1946 

1947 # Non-parallel: delegate to each impl's kron method. 

1948 # All-sparse inputs stay sparse; mixed inputs promote to dense via _coerce. 

1949 current_impl = args[0]._impl 

1950 dims_0 = args[0].dims[0] 

1951 dims_1 = args[0].dims[1] 

1952 for arg in args[1:]: 

1953 current_impl = current_impl.kron(arg._impl) 

1954 dims_0 = dims_0 + arg.dims[0] 

1955 dims_1 = dims_1 + arg.dims[1] 

1956 return Qarray._from_impl(current_impl, Qdims((dims_0, dims_1))) 

1957 

1958 

1959def tr(qarr: Qarray, **kwargs) -> Array: 

1960 """Full trace of *qarr*. 

1961 

1962 For sparse ``Qarray`` objects the trace is computed natively on the BCOO 

1963 data using a masked scatter — no densification. Custom axis arguments 

1964 are ignored for sparse (the last two dimensions are always the matrix 

1965 dimensions in jaxquantum's convention). 

1966 

1967 Args: 

1968 qarr: Input quantum array. 

1969 **kwargs: Forwarded to ``jnp.trace`` for dense arrays (e.g. 

1970 ``axis1``, ``axis2``). 

1971 

1972 Returns: 

1973 The trace as a scalar (or batched array of scalars). 

1974 """ 

1975 if qarr.is_sparse_bcoo: 

1976 return qarr._impl.trace() 

1977 if qarr.is_sparse_dia: 

1978 return qarr._impl.trace() 

1979 axis1 = kwargs.get("axis1", -2) 

1980 axis2 = kwargs.get("axis2", -1) 

1981 return jnp.trace(qarr.data, axis1=axis1, axis2=axis2, **kwargs) 

1982 

1983 

1984def trace(qarr: Qarray, **kwargs) -> Array: 

1985 """Full trace (alias for :func:`tr`). 

1986 

1987 Args: 

1988 qarr: Input quantum array. 

1989 **kwargs: Forwarded to :func:`tr`. 

1990 

1991 Returns: 

1992 The trace as a scalar (or batched array of scalars). 

1993 """ 

1994 return tr(qarr, **kwargs) 

1995 

1996 

1997def expm_data(data: Array, **kwargs) -> Array: 

1998 """Matrix exponential of a raw array. 

1999 

2000 Args: 

2001 data: Dense matrix array. 

2002 **kwargs: Forwarded to ``jsp.linalg.expm``. 

2003 

2004 Returns: 

2005 The matrix exponential. 

2006 """ 

2007 return jsp.linalg.expm(data, **kwargs) 

2008 

2009 

2010def expm(qarr: Qarray, **kwargs) -> Qarray: 

2011 """Matrix exponential of a ``Qarray``. 

2012 

2013 Args: 

2014 qarr: Input quantum array (converted to dense internally). 

2015 **kwargs: Forwarded to ``jsp.linalg.expm``. 

2016 

2017 Returns: 

2018 A dense ``Qarray`` containing the matrix exponential. 

2019 """ 

2020 dims = qarr.dims 

2021 # Convert to dense for expm 

2022 dense_data = qarr.to_dense().data 

2023 data = expm_data(dense_data, **kwargs) 

2024 return Qarray.create(data, dims=dims) 

2025 

2026 

2027def powm(qarr: Qarray, n: Union[int, float], clip_eigvals=False) -> Qarray: 

2028 """Matrix power of a ``Qarray``. 

2029 

2030 Args: 

2031 qarr: Input quantum array. 

2032 n: Exponent. Integer powers use ``jnp.linalg.matrix_power``; float 

2033 powers diagonalise the matrix. 

2034 clip_eigvals: When ``True``, clip negative eigenvalues to zero before 

2035 applying the float power (useful for nearly-PSD matrices). 

2036 

2037 Returns: 

2038 The *n*-th matrix power as a ``Qarray`` (stays SparseDIA for integer 

2039 non-negative exponents when the input is SparseDIA). 

2040 

2041 Raises: 

2042 ValueError: If *n* is a float and the matrix has negative eigenvalues 

2043 (and *clip_eigvals* is ``False``). 

2044 """ 

2045 # SparseDIA fast path: binary exponentiation stays in SparseDIA format. 

2046 if qarr.is_sparse_dia and isinstance(n, int) and n >= 0: 

2047 new_impl = qarr._impl.powm(n) 

2048 return Qarray.create(new_impl.data, dims=qarr.dims, implementation=new_impl.impl_type) 

2049 

2050 # Convert to dense for powm 

2051 dense_qarr = qarr.to_dense() 

2052 

2053 if isinstance(n, int): 

2054 data_res = jnp.linalg.matrix_power(dense_qarr.data, n) 

2055 else: 

2056 evalues, evectors = jnp.linalg.eig(dense_qarr.data) 

2057 if clip_eigvals: 

2058 evalues = jnp.maximum(evalues, 0) 

2059 else: 

2060 if not (evalues >= 0).all(): 

2061 raise ValueError( 

2062 "Non-integer power of a matrix can only be " 

2063 "computed if the matrix is positive semi-definite." 

2064 "Got a matrix with a negative eigenvalue." 

2065 ) 

2066 data_res = evectors * jnp.pow(evalues, n) @ jnp.linalg.inv(evectors) 

2067 

2068 return Qarray.create(data_res, dims=qarr.dims) 

2069 

2070 

2071def cosm_data(data: Array, **kwargs) -> Array: 

2072 """Matrix cosine of a raw array. 

2073 

2074 Args: 

2075 data: Dense matrix array. 

2076 **kwargs: Unused; kept for API consistency. 

2077 

2078 Returns: 

2079 The matrix cosine computed as ``(expm(i*A) + expm(-i*A)) / 2``. 

2080 """ 

2081 return (expm_data(1j * data) + expm_data(-1j * data)) / 2 

2082 

2083 

2084def cosm(qarr: Qarray) -> Qarray: 

2085 """Matrix cosine of a ``Qarray``. 

2086 

2087 Args: 

2088 qarr: Input quantum array (converted to dense internally). 

2089 

2090 Returns: 

2091 A dense ``Qarray`` containing the matrix cosine. 

2092 """ 

2093 dims = qarr.dims 

2094 # Convert to dense for cosm 

2095 dense_data = qarr.to_dense().data 

2096 data = cosm_data(dense_data) 

2097 return Qarray.create(data, dims=dims) 

2098 

2099 

2100def sinm_data(data: Array, **kwargs) -> Array: 

2101 """Matrix sine of a raw array. 

2102 

2103 Args: 

2104 data: Dense matrix array. 

2105 **kwargs: Unused; kept for API consistency. 

2106 

2107 Returns: 

2108 The matrix sine computed as ``(expm(i*A) - expm(-i*A)) / (2i)``. 

2109 """ 

2110 return (expm_data(1j * data) - expm_data(-1j * data)) / (2j) 

2111 

2112 

2113def sinm(qarr: Qarray) -> Qarray: 

2114 """Matrix sine of a ``Qarray``. 

2115 

2116 Args: 

2117 qarr: Input quantum array (converted to dense internally). 

2118 

2119 Returns: 

2120 A dense ``Qarray`` containing the matrix sine. 

2121 """ 

2122 dims = qarr.dims 

2123 # Convert to dense for sinm 

2124 dense_data = qarr.to_dense().data 

2125 data = sinm_data(dense_data) 

2126 return Qarray.create(data, dims=dims) 

2127 

2128 

2129def keep_only_diag_elements(qarr: Qarray) -> Qarray: 

2130 """Zero out all off-diagonal elements of *qarr*. 

2131 

2132 For sparse ``Qarray`` objects the off-diagonal stored values are zeroed 

2133 in-place on the BCOO structure — no densification. 

2134 

2135 Args: 

2136 qarr: Non-batched input quantum array. 

2137 

2138 Returns: 

2139 A ``Qarray`` with only diagonal entries non-zero. 

2140 

2141 Raises: 

2142 ValueError: If *qarr* has batch dimensions. 

2143 """ 

2144 if len(qarr.bdims) > 0: 

2145 raise ValueError("Cannot keep only diagonal elements of a batched Qarray.") 

2146 

2147 dims = qarr.dims 

2148 if qarr.is_sparse_bcoo: 

2149 new_impl = qarr._impl.keep_only_diag() 

2150 return Qarray.create(new_impl.data, dims=dims, implementation=QarrayImplType.SPARSE_BCOO) 

2151 if qarr.is_sparse_dia: 

2152 from jaxquantum.core.sparse_dia import SparseDiaImpl 

2153 impl = qarr._impl 

2154 n = impl._diags.shape[-1] 

2155 if 0 in impl._offsets: 

2156 i = impl._offsets.index(0) 

2157 main_diag = impl._diags[..., i:i + 1, :] 

2158 else: 

2159 main_diag = jnp.zeros((*impl._diags.shape[:-2], 1, n), dtype=impl._diags.dtype) 

2160 new_impl = SparseDiaImpl(_offsets=(0,), _diags=main_diag) 

2161 return Qarray.create(new_impl.get_data(), dims=dims, implementation=QarrayImplType.SPARSE_DIA) 

2162 data = jnp.diag(jnp.diag(qarr.data)) 

2163 return Qarray.create(data, dims=dims) 

2164 

2165 

2166def to_ket(qarr: Qarray) -> Qarray: 

2167 """Convert *qarr* to a ket. 

2168 

2169 Args: 

2170 qarr: A ket (returned as-is) or bra (conjugate-transposed). 

2171 

2172 Returns: 

2173 The ket form of *qarr*. 

2174 

2175 Raises: 

2176 ValueError: If *qarr* is an operator. 

2177 """ 

2178 if qarr.qtype == Qtypes.ket: 

2179 return qarr 

2180 elif qarr.qtype == Qtypes.bra: 

2181 return qarr.dag() 

2182 else: 

2183 raise ValueError("Can only get ket from a ket or bra.") 

2184 

2185 

2186def eigenstates(qarr: Qarray) -> Qarray: 

2187 """Eigenstates of a quantum array. 

2188 

2189 Args: 

2190 qarr: Hermitian operator (converted to dense internally). 

2191 

2192 Returns: 

2193 A tuple ``(eigenvalues, eigenstates_qarray)`` where eigenvalues are 

2194 sorted in ascending order. 

2195 """ 

2196 # Convert to dense for eigenstates 

2197 dense_qarr = qarr.to_dense() 

2198 

2199 evals, evecs = jnp.linalg.eigh(dense_qarr.data) 

2200 dims = ket_from_op_dims(qarr.dims) 

2201 

2202 # numpy returns [batch, :, i] as the i-th eigenvector 

2203 # we want [batch, i, :] as the i-th eigenvector 

2204 evecs = jnp.swapaxes(evecs, -2, -1) 

2205 evecs = Qarray._from_impl( 

2206 DenseImpl._make(evecs), 

2207 Qdims(dims), 

2208 ) 

2209 

2210 return evals, evecs 

2211 

2212 

2213def eigenenergies(qarr: Qarray) -> Array: 

2214 """Eigenvalues of a quantum array. 

2215 

2216 Args: 

2217 qarr: Hermitian operator (converted to dense internally). 

2218 

2219 Returns: 

2220 Sorted eigenvalues as a JAX array. 

2221 """ 

2222 # Convert to dense for eigenenergies 

2223 dense_qarr = qarr.to_dense() 

2224 evals = jnp.linalg.eigvalsh(dense_qarr.data) 

2225 return evals 

2226 

2227 

2228def ptrace(qarr: Qarray, indx) -> Qarray: 

2229 """Partial trace over subsystem *indx*. 

2230 

2231 Args: 

2232 qarr: Input quantum array (converted to dense internally). 

2233 indx: Index of the subsystem to trace out. 

2234 

2235 Returns: 

2236 Reduced density matrix as a ``Qarray``. 

2237 """ 

2238 # Convert to dense for ptrace 

2239 dense_qarr = qarr.to_dense() 

2240 dense_qarr = ket2dm(dense_qarr) 

2241 rho = dense_qarr.shaped_data 

2242 dims = dense_qarr.dims 

2243 

2244 Nq = len(dims[0]) 

2245 

2246 indxs = [indx, indx + Nq] 

2247 for j in range(Nq): 

2248 if j == indx: 

2249 continue 

2250 indxs.append(j) 

2251 indxs.append(j + Nq) 

2252 

2253 bdims = dense_qarr.bdims 

2254 len_bdims = len(bdims) 

2255 bdims_indxs = list(range(len_bdims)) 

2256 indxs = bdims_indxs + [j + len_bdims for j in indxs] 

2257 rho = rho.transpose(indxs) 

2258 

2259 for j in range(Nq - 1): 

2260 rho = jnp.trace(rho, axis1=2 + len_bdims, axis2=3 + len_bdims) 

2261 

2262 return Qarray.create(rho) 

2263 

2264 

2265def dag(qarr: Qarray) -> Qarray: 

2266 """Conjugate transpose of *qarr*. 

2267 

2268 For ket/bra vectors (stored on a single axis) this is just a complex 

2269 conjugate with the ``dims`` reversed — no axis swap, so the data stays 

2270 1‑D. For operators it is the usual conjugate transpose. 

2271 

2272 Args: 

2273 qarr: Input quantum array. 

2274 

2275 Returns: 

2276 The conjugate transpose with swapped ``dims``. 

2277 """ 

2278 dims = qarr.dims[::-1] 

2279 if qarr.qtype in (Qtypes.ket, Qtypes.bra): 

2280 new_impl = qarr._impl.conj() 

2281 else: 

2282 new_impl = qarr._impl.dag() 

2283 # Infer batch dimensions from the transformed data because ``_bdims`` is 

2284 # static PyTree metadata and can be stale under ``vmap``. 

2285 return Qarray._from_impl(new_impl, Qdims(dims)) 

2286 

2287 

2288def dag_data(arr) -> Array: 

2289 """Conjugate transpose of a raw array, dispatching to the right backend. 

2290 

2291 Iterates through registered :class:`QarrayImpl` subclasses and delegates 

2292 to the first one whose :meth:`~QarrayImpl.can_handle_data` returns True. 

2293 Adding a new backend automatically extends this function — no changes 

2294 required here. 

2295 

2296 Args: 

2297 arr: Input array (``jnp.ndarray``, ``sparse.BCOO``, or any type 

2298 handled by a registered impl). For 1-D dense arrays only 

2299 conjugation is applied (no transpose). 

2300 

2301 Returns: 

2302 Conjugate transpose with the last two axes swapped. 

2303 

2304 Raises: 

2305 TypeError: If no registered impl can handle *arr*. 

2306 """ 

2307 for impl_class in _IMPL_REGISTRY: 

2308 if impl_class.can_handle_data(arr): 

2309 return impl_class.dag_data(arr) 

2310 raise TypeError(f"dag_data: no registered impl can handle type {type(arr)}") 

2311 

2312 

2313def ket2dm(qarr: Qarray) -> Qarray: 

2314 """Convert a ket to a density matrix via outer product. 

2315 

2316 Args: 

2317 qarr: Ket, bra, or operator. Operators are returned unchanged. 

2318 

2319 Returns: 

2320 Density matrix ``|ψ⟩⟨ψ|``. 

2321 """ 

2322 if qarr.qtype == Qtypes.oper: 

2323 return qarr 

2324 

2325 if qarr.qtype == Qtypes.bra: 

2326 qarr = qarr.dag() 

2327 

2328 return qarr @ qarr.dag() 

2329 

2330 

2331# Data level operations 

2332def is_dm_data(data: Array) -> bool: 

2333 """Check whether *data* has the shape of a density matrix (square matrix). 

2334 

2335 Args: 

2336 data: Array to check. 

2337 

2338 Returns: 

2339 True if the last two dimensions are equal. 

2340 """ 

2341 return data.shape[-2] == data.shape[-1] 

2342 

2343 

2344def powm_data(data: Array, n: int) -> Array: 

2345 """Integer matrix power of a raw array. 

2346 

2347 Args: 

2348 data: Dense square matrix array. 

2349 n: Integer exponent. 

2350 

2351 Returns: 

2352 The *n*-th matrix power. 

2353 """ 

2354 return jnp.linalg.matrix_power(data, n) 

2355 

2356 

2357# Type aliases for readability 

2358DenseQarray = Qarray[DenseImpl] 

2359# SparseBCOOQarray and SparseDIAQarray are defined lazily (impls imported at runtime) 

2360# Use Qarray[SparseBCOOImpl] / Qarray[SparseDiaImpl] once those modules are imported. 

2361 

2362ARRAY_TYPES = (Array, ndarray, Qarray)