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

772 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-01 06:26 +0000

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

2 

3from __future__ import annotations 

4 

5from abc import ABC, abstractmethod 

6from flax import struct 

7from jax import Array, config, vmap 

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

9 

10if TYPE_CHECKING: 

11 from jaxquantum.core.sparse_bcoo import SparseBCOOImpl 

12import jax.numpy as jnp 

13import jax.scipy as jsp 

14from jax.experimental import sparse 

15from numpy import ndarray 

16from copy import deepcopy 

17from math import prod 

18from enum import Enum 

19 

20from jaxquantum.core.settings import SETTINGS 

21from jaxquantum.utils.utils import robust_isscalar 

22from jaxquantum.core.dims import Qtypes, Qdims, check_dims, ket_from_op_dims 

23 

24config.update("jax_enable_x64", True) 

25 

26# Type variable for implementation types 

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

28 

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

30_IMPL_REGISTRY: dict = {} 

31 

32 

33class QarrayImplType(Enum): 

34 """Enumeration of available Qarray storage backends. 

35 

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

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

38 immediately after defining their impl class. 

39 

40 Members: 

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

42 SPARSE_BCOO: JAX experimental BCOO sparse array. 

43 SPARSE_DIA: Diagonal sparse array. 

44 """ 

45 

46 DENSE = "dense" 

47 SPARSE_BCOO = "sparse_bcoo" 

48 SPARSE_DIA = "sparse_dia" 

49 

50 @classmethod 

51 def register(cls, impl_class, member): 

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

53 

54 Args: 

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

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

57 """ 

58 _IMPL_REGISTRY[impl_class] = member 

59 

60 @classmethod 

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

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

63 

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

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

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

67 

68 Args: 

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

70 

71 Returns: 

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

73 """ 

74 if isinstance(x, cls): 

75 return True 

76 

77 if isinstance(x, str): 

78 xl = x.lower() 

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

80 

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

82 try: 

83 cls.from_impl_class(x) 

84 return True 

85 except Exception: # noqa: BLE001 

86 return False 

87 

88 @classmethod 

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

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

91 

92 Args: 

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

94 registered via :meth:`register`. 

95 

96 Returns: 

97 The corresponding ``QarrayImplType`` member. 

98 

99 Raises: 

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

101 """ 

102 if impl_class in _IMPL_REGISTRY: 

103 return _IMPL_REGISTRY[impl_class] 

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

105 

106 def get_impl_class(self): 

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

108 

109 Returns: 

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

111 

112 Raises: 

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

114 """ 

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

116 if member is self: 

117 return cls_key 

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

119 

120 

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

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

123 

124 Args: 

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

126 

127 Returns: 

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

129 """ 

130 if isinstance(data, sparse.BCOO): 

131 return data 

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

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

134 return data 

135 return jnp.asarray(data) 

136 

137 

138class QarrayImpl(ABC): 

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

140 

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

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

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

144 

145 Attributes: 

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

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

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

149 """ 

150 

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

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

153 

154 @abstractmethod 

155 def get_data(self) -> Array: 

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

157 pass 

158 

159 @property 

160 def data(self) -> Array: 

161 """The underlying raw data array.""" 

162 return self.get_data() 

163 

164 @property 

165 def impl_type(self) -> QarrayImplType: 

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

167 return QarrayImplType.from_impl_class(type(self)) 

168 

169 @classmethod 

170 @abstractmethod 

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

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

173 

174 Args: 

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

176 

177 Returns: 

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

179 """ 

180 pass 

181 

182 @abstractmethod 

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

184 """Matrix multiplication with *other*. 

185 

186 Args: 

187 other: Right-hand operand. 

188 

189 Returns: 

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

191 """ 

192 pass 

193 

194 @abstractmethod 

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

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

197 

198 Args: 

199 other: Right-hand operand. 

200 

201 Returns: 

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

203 """ 

204 pass 

205 

206 @abstractmethod 

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

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

209 

210 Args: 

211 other: Right-hand operand. 

212 

213 Returns: 

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

215 """ 

216 pass 

217 

218 @abstractmethod 

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

220 """Scalar multiplication. 

221 

222 Args: 

223 scalar: Scalar value to multiply by. 

224 

225 Returns: 

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

227 """ 

228 pass 

229 

230 @abstractmethod 

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

232 """Conjugate transpose. 

233 

234 Returns: 

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

236 """ 

237 pass 

238 

239 @abstractmethod 

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

241 """Convert to a ``DenseImpl``. 

242 

243 Returns: 

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

245 """ 

246 pass 

247 

248 @abstractmethod 

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

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

251 

252 Returns: 

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

254 """ 

255 pass 

256 

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

258 """Convert to a ``SparseDiaImpl``. 

259 

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

261 Subclasses may override for a more direct path. 

262 

263 Returns: 

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

265 """ 

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

267 from jaxquantum.core.sparse_dia import SparseDiaImpl 

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

269 

270 @abstractmethod 

271 def shape(self) -> tuple: 

272 """Shape of the underlying data array. 

273 

274 Returns: 

275 Tuple of dimension sizes. 

276 """ 

277 pass 

278 

279 @abstractmethod 

280 def dtype(self): 

281 """Data type of the underlying array. 

282 

283 Returns: 

284 A numpy/JAX dtype object. 

285 """ 

286 pass 

287 

288 @abstractmethod 

289 def __deepcopy__(self, memo=None): 

290 pass 

291 

292 @abstractmethod 

293 def tidy_up(self, atol): 

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

295 

296 Args: 

297 atol: Absolute tolerance threshold. 

298 

299 Returns: 

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

301 """ 

302 pass 

303 

304 @abstractmethod 

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

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

307 

308 Args: 

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

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

311 type (dense wins over sparse). 

312 

313 Returns: 

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

315 """ 

316 pass 

317 

318 @classmethod 

319 @abstractmethod 

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

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

322 

323 Args: 

324 n: Matrix size. 

325 dtype: Optional data type for the identity entries. 

326 

327 Returns: 

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

329 """ 

330 pass 

331 

332 @classmethod 

333 @abstractmethod 

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

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

336 

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

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

339 impl classes. 

340 

341 Args: 

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

343 ``sparse.BCOO`` for ``SparseBCOOImpl``. 

344 

345 Returns: 

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

347 """ 

348 pass 

349 

350 @classmethod 

351 @abstractmethod 

352 def dag_data(cls, arr): 

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

354 

355 Implementations must handle batched arrays (last two axes are 

356 swapped) and must not densify sparse arrays. 

357 

358 Args: 

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

360 

361 Returns: 

362 Conjugate transpose with the last two axes swapped. 

363 """ 

364 pass 

365 

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

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

368 

369 Args: 

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

371 

372 Returns: 

373 An instance of *target_cls* holding equivalent data. 

374 """ 

375 if isinstance(self, target_cls): 

376 return self 

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

378 

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

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

381 

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

383 is promoted via :meth:`_promote_to`. 

384 

385 Args: 

386 other: The other operand. 

387 

388 Returns: 

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

390 for a binary operation. 

391 """ 

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

393 return self, other 

394 if self.PROMOTION_ORDER >= other.PROMOTION_ORDER: 

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

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

397 

398 

399@struct.dataclass 

400class DenseImpl(QarrayImpl): 

401 """Dense implementation using JAX dense arrays. 

402 

403 Attributes: 

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

405 """ 

406 

407 _data: Array 

408 

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

410 

411 @classmethod 

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

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

414 

415 Args: 

416 data: Array-like input data. 

417 

418 Returns: 

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

420 """ 

421 return cls(_data=robust_asarray(data)) 

422 

423 def get_data(self) -> Array: 

424 """Return the underlying dense array.""" 

425 return self._data 

426 

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

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

429 

430 Args: 

431 other: Right-hand operand. 

432 

433 Returns: 

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

435 """ 

436 a, b = self._coerce(other) 

437 if a is not self: 

438 return a.matmul(b) 

439 return DenseImpl(self._data @ b._data) 

440 

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

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

443 

444 Args: 

445 other: Right-hand operand. 

446 

447 Returns: 

448 A ``DenseImpl`` containing the sum. 

449 """ 

450 a, b = self._coerce(other) 

451 if a is not self: 

452 return a.add(b) 

453 return DenseImpl(self._data + b._data) 

454 

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

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

457 

458 Args: 

459 other: Right-hand operand. 

460 

461 Returns: 

462 A ``DenseImpl`` containing the difference. 

463 """ 

464 a, b = self._coerce(other) 

465 if a is not self: 

466 return a.sub(b) 

467 return DenseImpl(self._data - b._data) 

468 

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

470 """Scalar multiplication. 

471 

472 Args: 

473 scalar: Scalar value. 

474 

475 Returns: 

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

477 """ 

478 return DenseImpl(scalar * self._data) 

479 

480 def dag(self) -> QarrayImpl: 

481 """Conjugate transpose. 

482 

483 Returns: 

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

485 """ 

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

487 

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

489 """Return self (already dense). 

490 

491 Returns: 

492 This ``DenseImpl`` instance unchanged. 

493 """ 

494 return self 

495 

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

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

498 

499 Returns: 

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

501 """ 

502 from jaxquantum.core.sparse_bcoo import SparseBCOOImpl 

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

504 

505 def shape(self) -> tuple: 

506 """Shape of the underlying dense array. 

507 

508 Returns: 

509 Tuple of dimension sizes. 

510 """ 

511 return self._data.shape 

512 

513 def dtype(self): 

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

515 

516 Returns: 

517 The dtype of ``_data``. 

518 """ 

519 return self._data.dtype 

520 

521 def frobenius_norm(self) -> float: 

522 """Compute the Frobenius norm. 

523 

524 Returns: 

525 The Frobenius norm as a scalar. 

526 """ 

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

528 

529 def real(self) -> QarrayImpl: 

530 """Element-wise real part. 

531 

532 Returns: 

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

534 """ 

535 return DenseImpl(jnp.real(self._data)) 

536 

537 def imag(self) -> QarrayImpl: 

538 """Element-wise imaginary part. 

539 

540 Returns: 

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

542 """ 

543 return DenseImpl(jnp.imag(self._data)) 

544 

545 def conj(self) -> QarrayImpl: 

546 """Element-wise complex conjugate. 

547 

548 Returns: 

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

550 """ 

551 return DenseImpl(jnp.conj(self._data)) 

552 

553 def __deepcopy__(self, memo=None): 

554 return DenseImpl( 

555 _data=deepcopy(self._data, memo) 

556 ) 

557 

558 def tidy_up(self, atol): 

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

560 

561 Args: 

562 atol: Absolute tolerance threshold. 

563 

564 Returns: 

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

566 """ 

567 data = self._data 

568 data_re = jnp.real(data) 

569 data_im = jnp.imag(data) 

570 data_re_mask = jnp.abs(data_re) > atol 

571 data_im_mask = jnp.abs(data_im) > atol 

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

573 

574 return DenseImpl( 

575 _data=data_new 

576 ) 

577 

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

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

580 

581 Args: 

582 other: Right-hand operand. 

583 

584 Returns: 

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

586 """ 

587 a, b = self._coerce(other) 

588 if a is not self: 

589 return a.kron(b) 

590 return DenseImpl(jnp.kron(self._data, b._data)) 

591 

592 @classmethod 

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

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

595 

596 Args: 

597 n: Matrix size. 

598 dtype: Optional data type. 

599 

600 Returns: 

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

602 """ 

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

604 

605 @classmethod 

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

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

608 

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

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

611 

612 Args: 

613 arr: Raw array. 

614 

615 Returns: 

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

617 """ 

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

619 

620 @classmethod 

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

622 """Conjugate transpose for dense arrays. 

623 

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

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

626 

627 Args: 

628 arr: Dense array. 

629 

630 Returns: 

631 Conjugate transpose with the last two axes swapped. 

632 """ 

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

634 return jnp.conj(arr) 

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

636 

637 

638# Register implementation classes with the enum registry 

639# SparseBCOOImpl is registered in sparse_bcoo.py after import 

640QarrayImplType.register(DenseImpl, QarrayImplType.DENSE) 

641 

642 

643@struct.dataclass 

644class Qarray(Generic[ImplT]): 

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

646 

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

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

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

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

651 store data as a JAX BCOO sparse array. 

652 

653 Attributes: 

654 _impl: The storage backend holding the raw data. 

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

656 sizes). 

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

658 

659 Example: 

660 >>> import jaxquantum as jqt 

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

662 >>> a.is_sparse_bcoo 

663 True 

664 """ 

665 

666 _impl: ImplT 

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

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

669 

670 # Initialization ---- 

671 @classmethod 

672 @overload 

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

674 ... 

675 

676 @classmethod 

677 @overload 

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

679 ... 

680 

681 @classmethod 

682 @overload 

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

684 ... 

685 

686 @classmethod 

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

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

689 

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

691 values. 

692 

693 Args: 

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

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

696 Inferred from *data* shape when ``None``. 

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

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

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

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

701 string ``"dense"`` / ``"sparse_bcoo"``. 

702 

703 Returns: 

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

705 """ 

706 # Step 1: Prepare data ---- 

707 data = robust_asarray(data) 

708 

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

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

711 

712 if ( 

713 len(data.shape) >= 2 

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

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

716 ): 

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

718 

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

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

721 # ---- 

722 

723 # Step 2: Prepare dimensions ---- 

724 if bdims is None: 

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

726 

727 if dims is None: 

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

729 

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

731 # This handles the case where only the hilbert space dimensions are sent in. 

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

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

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

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

736 else: 

737 dims = (tuple(dims), tuple(dims)) 

738 else: 

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

740 

741 check_dims(dims, bdims, data.shape) 

742 

743 qdims = Qdims(dims) 

744 

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

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

747 # increased the runtime of the jit compiled function. 

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

749 

750 impl_class = QarrayImplType(implementation).get_impl_class() 

751 impl = impl_class.from_data(data) 

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

753 

754 return cls(impl, qdims, bdims) 

755 

756 @classmethod 

757 @overload 

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

759 ... 

760 

761 @classmethod 

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

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

764 

765 Args: 

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

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

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

769 

770 Returns: 

771 A ``Qarray[SparseBCOOImpl]``. 

772 """ 

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

774 

775 @classmethod 

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

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

778 

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

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

781 

782 Args: 

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

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

785 bdims: Batch dimension sizes. 

786 

787 Returns: 

788 A ``Qarray`` backed by ``SparseDiaImpl``. 

789 """ 

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

791 

792 @classmethod 

793 @overload 

794 def from_list(cls, qarr_list: List["Qarray[DenseImpl]"]) -> "Qarray[DenseImpl]": 

795 ... 

796 

797 @classmethod 

798 @overload 

799 def from_list(cls, qarr_list: List["Qarray[SparseBCOOImpl]"]) -> "Qarray[SparseBCOOImpl]": 

800 ... 

801 

802 @classmethod 

803 def from_list(cls, qarr_list: List[Qarray]) -> Qarray: 

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

805 

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

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

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

809 and the result is dense. 

810 

811 Args: 

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

813 ``bdims``. May be empty. 

814 

815 Returns: 

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

817 ``len(qarr_list)``. 

818 

819 Raises: 

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

821 """ 

822 if len(qarr_list) == 0: 

823 dims = ((), ()) 

824 bdims = (0,) 

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

826 

827 dims = qarr_list[0].dims 

828 bdims = qarr_list[0].bdims 

829 

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

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

832 

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

834 

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

836 target_impl_type = max( 

837 (q.impl_type for q in qarr_list), 

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

839 ) 

840 

841 if target_impl_type == QarrayImplType.SPARSE_DIA: 

842 # All inputs are SparseDIA — batch without densifying. 

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

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

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

846 union_offsets = tuple(sorted( 

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

848 )) 

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

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

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

852 remapped = [] 

853 for q in qarr_list: 

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

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

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

857 remapped.append(row) 

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

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

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

861 

862 if target_impl_type == QarrayImplType.SPARSE_BCOO: 

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

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

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

866 

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

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

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

870 

871 @classmethod 

872 @overload 

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

874 ... 

875 

876 @classmethod 

877 @overload 

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

879 ... 

880 

881 @classmethod 

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

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

884 

885 Args: 

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

887 ``Qarray`` objects. 

888 

889 Returns: 

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

891 of *qarr_arr*. 

892 """ 

893 if isinstance(qarr_arr, Qarray): 

894 return qarr_arr 

895 

896 bdims = () 

897 lvl = qarr_arr 

898 while not isinstance(lvl, Qarray): 

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

900 if len(lvl) > 0: 

901 lvl = lvl[0] 

902 else: 

903 break 

904 

905 def flat(lis): 

906 flatList = [] 

907 for element in lis: 

908 if type(element) is list: 

909 flatList += flat(element) 

910 else: 

911 flatList.append(element) 

912 return flatList 

913 

914 qarr_list = flat(qarr_arr) 

915 qarr = cls.from_list(qarr_list) 

916 qarr = qarr.reshape_bdims(*bdims) 

917 return qarr 

918 

919 # Properties ---- 

920 @property 

921 def qtype(self): 

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

923 return self._qdims.qtype 

924 

925 @property 

926 def dtype(self): 

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

928 return self._impl.dtype() 

929 

930 @property 

931 def dims(self): 

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

933 return self._qdims.dims 

934 

935 @property 

936 def bdims(self): 

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

938 return self._bdims 

939 

940 @property 

941 def qdims(self): 

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

943 return self._qdims 

944 

945 @property 

946 def space_dims(self): 

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

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

949 return self.dims[0] 

950 elif self.qtype == Qtypes.bra: 

951 return self.dims[1] 

952 else: 

953 # TODO: not reached for some reason 

954 raise ValueError("Unsupported qtype.") 

955 

956 @property 

957 def data(self): 

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

959 return self._impl.data 

960 

961 @property 

962 def shaped_data(self): 

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

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

965 

966 @property 

967 def shape(self): 

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

969 return self.data.shape 

970 

971 @property 

972 def is_batched(self): 

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

974 return len(self.bdims) > 0 

975 

976 @property 

977 def is_sparse_bcoo(self): 

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

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

980 

981 @property 

982 def is_dense(self): 

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

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

985 

986 @property 

987 def is_sparse_dia(self): 

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

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

990 

991 @property 

992 def impl_type(self): 

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

994 return self._impl.impl_type 

995 

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

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

998 

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

1000 

1001 Returns: 

1002 A ``Qarray[SparseBCOOImpl]``. 

1003 """ 

1004 if self.is_sparse_bcoo: 

1005 return self 

1006 new_impl = self._impl.to_sparse_bcoo() 

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

1008 

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

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

1011 

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

1013 

1014 Returns: 

1015 A ``Qarray[SparseDiaImpl]``. 

1016 """ 

1017 if self.is_sparse_dia: 

1018 return self 

1019 new_impl = self._impl.to_sparse_dia() 

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

1021 

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

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

1024 

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

1026 

1027 Returns: 

1028 A ``Qarray[DenseImpl]``. 

1029 """ 

1030 if self.is_dense: 

1031 return self 

1032 new_impl = self._impl.to_dense() 

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

1034 

1035 def __getitem__(self, index): 

1036 if len(self.bdims) > 0: 

1037 return Qarray.create( 

1038 self.data[index], 

1039 dims=self.dims, 

1040 implementation=self.impl_type, 

1041 ) 

1042 else: 

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

1044 

1045 def reshape_bdims(self, *args): 

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

1047 

1048 Args: 

1049 *args: New batch dimension sizes. 

1050 

1051 Returns: 

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

1053 """ 

1054 new_bdims = tuple(args) 

1055 

1056 if prod(new_bdims) == 0: 

1057 new_shape = new_bdims 

1058 else: 

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

1060 

1061 # Preserve implementation type 

1062 implementation = self.impl_type 

1063 return Qarray.create( 

1064 self.data.reshape(new_shape), 

1065 dims=self.dims, 

1066 bdims=new_bdims, 

1067 implementation=implementation, 

1068 ) 

1069 

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

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

1072 

1073 Args: 

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

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

1076 

1077 Returns: 

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

1079 

1080 Raises: 

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

1082 """ 

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

1084 return space_dims 

1085 

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

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

1088 elif self.qtype == Qtypes.bra: 

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

1090 else: 

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

1092 

1093 def reshape_qdims(self, *args): 

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

1095 

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

1097 dimensions. 

1098 

1099 Args: 

1100 *args: New Hilbert dimensions for the Qarray. 

1101 

1102 Returns: 

1103 Qarray: reshaped Qarray. 

1104 """ 

1105 

1106 new_space_dims = tuple(args) 

1107 current_space_dims = self.space_dims 

1108 assert prod(new_space_dims) == prod(current_space_dims) 

1109 

1110 new_qdims = self.space_to_qdims(new_space_dims) 

1111 new_bdims = self.bdims 

1112 

1113 # Preserve implementation type 

1114 implementation = self.impl_type 

1115 return Qarray.create(self.data, dims=new_qdims, bdims=new_bdims, implementation=implementation) 

1116 

1117 def resize(self, new_shape): 

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

1119 

1120 TODO: review and maybe deprecate this method. 

1121 

1122 Args: 

1123 new_shape: Target shape tuple. 

1124 

1125 Returns: 

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

1127 """ 

1128 dims = self.dims 

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

1130 # Preserve implementation type 

1131 implementation = self.impl_type 

1132 return Qarray.create( 

1133 data, 

1134 dims=dims, 

1135 implementation=implementation, 

1136 ) 

1137 

1138 def __len__(self): 

1139 """Length along the first batch dimension. 

1140 

1141 Returns: 

1142 Size of the leading batch dimension. 

1143 

1144 Raises: 

1145 ValueError: If the array is not batched. 

1146 """ 

1147 if len(self.bdims) > 0: 

1148 return self.data.shape[0] 

1149 else: 

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

1151 

1152 def __eq__(self, other): 

1153 if not isinstance(other, Qarray): 

1154 raise ValueError( # noqa: TRY004 

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

1156 ) 

1157 

1158 if self.dims != other.dims: 

1159 return False 

1160 

1161 if self.bdims != other.bdims: 

1162 return False 

1163 

1164 if self.is_sparse_bcoo and other.is_sparse_bcoo: 

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

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

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

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

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

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

1171 

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

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

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

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

1176 

1177 def __ne__(self, other): 

1178 return not self.__eq__(other) 

1179 

1180 # Elementary Math ---- 

1181 def __matmul__(self, other): 

1182 if not isinstance(other, Qarray): 

1183 return NotImplemented 

1184 

1185 _qdims_new = self._qdims @ other._qdims 

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

1187 

1188 return Qarray.create( 

1189 new_impl.data, 

1190 dims=_qdims_new.dims, 

1191 implementation=new_impl.impl_type, 

1192 ) 

1193 

1194 def __mul__(self, other): 

1195 if isinstance(other, Qarray): 

1196 return self.__matmul__(other) 

1197 

1198 other = other + 0.0j 

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

1200 other = other.reshape(other.shape + (1, 1)) 

1201 

1202 new_impl = self._impl.mul(other) 

1203 return Qarray.create( 

1204 new_impl.data, 

1205 dims=self._qdims.dims, 

1206 implementation=new_impl.impl_type, 

1207 ) 

1208 

1209 def __rmul__(self, other): 

1210 return self.__mul__(other) 

1211 

1212 def __neg__(self): 

1213 return self.__mul__(-1) 

1214 

1215 def __truediv__(self, other): 

1216 """Divide by a scalar. 

1217 

1218 Args: 

1219 other: Scalar divisor. 

1220 

1221 Returns: 

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

1223 

1224 Raises: 

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

1226 """ 

1227 if isinstance(other, Qarray): 

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

1229 

1230 return self.__mul__(1 / other) 

1231 

1232 def __add__(self, other): 

1233 if isinstance(other, Qarray): 

1234 if self.dims != other.dims: 

1235 msg = ( 

1236 "Dimensions are incompatible: " 

1237 + repr(self.dims) 

1238 + " and " 

1239 + repr(other.dims) 

1240 ) 

1241 raise ValueError(msg) 

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

1243 return Qarray.create( 

1244 new_impl.data, 

1245 dims=self.dims, 

1246 implementation=new_impl.impl_type, 

1247 ) 

1248 

1249 if robust_isscalar(other) and other == 0: 

1250 return self.copy() 

1251 

1252 if self.data.shape[-2] == self.data.shape[-1]: 

1253 other = other + 0.0j 

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

1255 other = other.reshape(other.shape + (1, 1)) 

1256 eye_data = self._impl._eye_data(self.data.shape[-2], dtype=self.data.dtype) 

1257 other = Qarray.create( 

1258 other * eye_data, 

1259 dims=self.dims, 

1260 implementation=self.impl_type 

1261 ) 

1262 return self.__add__(other) 

1263 

1264 return NotImplemented 

1265 

1266 def __radd__(self, other): 

1267 return self.__add__(other) 

1268 

1269 def __sub__(self, other): 

1270 if isinstance(other, Qarray): 

1271 if self.dims != other.dims: 

1272 msg = ( 

1273 "Dimensions are incompatible: " 

1274 + repr(self.dims) 

1275 + " and " 

1276 + repr(other.dims) 

1277 ) 

1278 raise ValueError(msg) 

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

1280 return Qarray.create( 

1281 new_impl.data, 

1282 dims=self.dims, 

1283 implementation=new_impl.impl_type, 

1284 ) 

1285 

1286 if robust_isscalar(other) and other == 0: 

1287 return self.copy() 

1288 

1289 if self.data.shape[-2] == self.data.shape[-1]: 

1290 other = other + 0.0j 

1291 

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

1293 other = other.reshape(other.shape + (1, 1)) 

1294 eye_data = self._impl._eye_data(self.data.shape[-2], dtype=self.data.dtype) 

1295 other = Qarray.create( 

1296 other * eye_data, 

1297 dims=self.dims, 

1298 implementation=self.impl_type 

1299 ) 

1300 return self.__sub__(other) 

1301 

1302 return NotImplemented 

1303 

1304 def __rsub__(self, other): 

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

1306 

1307 def __xor__(self, other): 

1308 if not isinstance(other, Qarray): 

1309 return NotImplemented 

1310 return tensor(self, other) 

1311 

1312 def __rxor__(self, other): 

1313 if not isinstance(other, Qarray): 

1314 return NotImplemented 

1315 return tensor(other, self) 

1316 

1317 def __pow__(self, other): 

1318 if not isinstance(other, int): 

1319 return NotImplemented 

1320 

1321 return powm(self, other) 

1322 

1323 # String Representation ---- 

1324 def _str_header(self): 

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

1326 impl_type = self.impl_type.value 

1327 out = ", ".join( 

1328 [ 

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

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

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

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

1333 "impl = " + impl_type, 

1334 ] 

1335 ) 

1336 return out 

1337 

1338 def __str__(self): 

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

1340 

1341 @property 

1342 def header(self): 

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

1344 return self._str_header() 

1345 

1346 def __repr__(self): 

1347 return self.__str__() 

1348 

1349 # Utilities ---- 

1350 def copy(self, memo=None): 

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

1352 

1353 Args: 

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

1355 

1356 Returns: 

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

1358 """ 

1359 return self.__deepcopy__(memo) 

1360 

1361 def __deepcopy__(self, memo): 

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

1363 

1364 return Qarray( 

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

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

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

1368 ) 

1369 

1370 def __getattr__(self, method_name): 

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

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

1373 return lambda *args, **kwargs: NotImplemented 

1374 

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

1376 

1377 method_f = None 

1378 for mod in modules: 

1379 method_f = getattr(mod, method_name, None) 

1380 if method_f is not None: 

1381 break 

1382 

1383 if method_f is None: 

1384 raise NotImplementedError( 

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

1386 ) 

1387 

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

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

1390 if self.is_sparse_bcoo: 

1391 dense_self = self.to_dense() 

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

1393 else: 

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

1395 

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

1397 return res 

1398 else: 

1399 # Preserve implementation type 

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

1401 

1402 return func 

1403 

1404 # Conversions / Reshaping ---- 

1405 def dag(self): 

1406 """Conjugate transpose of this array.""" 

1407 return dag(self) 

1408 

1409 def to_dm(self): 

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

1411 return ket2dm(self) 

1412 

1413 def is_dm(self): 

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

1415 return self.qtype == Qtypes.oper 

1416 

1417 def is_vec(self): 

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

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

1420 

1421 def to_ket(self): 

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

1423 return to_ket(self) 

1424 

1425 def transpose(self, *args): 

1426 """Transpose subsystem indices.""" 

1427 return transpose(self, *args) 

1428 

1429 def keep_only_diag_elements(self): 

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

1431 return keep_only_diag_elements(self) 

1432 

1433 # Math Functions ---- 

1434 def unit(self): 

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

1436 return unit(self) 

1437 

1438 def norm(self): 

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

1440 return norm(self) 

1441 

1442 def frobenius_norm(self): 

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

1444 

1445 Returns: 

1446 The Frobenius norm as a scalar. 

1447 """ 

1448 return self._impl.frobenius_norm() 

1449 

1450 def real(self): 

1451 """Element-wise real part. 

1452 

1453 Returns: 

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

1455 """ 

1456 new_impl = self._impl.real() 

1457 return Qarray.create( 

1458 new_impl.data, 

1459 dims=self.dims, 

1460 implementation=new_impl.impl_type, 

1461 ) 

1462 

1463 def imag(self): 

1464 """Element-wise imaginary part. 

1465 

1466 Returns: 

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

1468 """ 

1469 new_impl = self._impl.imag() 

1470 

1471 return Qarray.create( 

1472 new_impl.data, 

1473 dims=self.dims, 

1474 implementation=new_impl.impl_type, 

1475 ) 

1476 

1477 def conj(self): 

1478 """Element-wise complex conjugate. 

1479 

1480 Returns: 

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

1482 """ 

1483 new_impl = self._impl.conj() 

1484 return Qarray.create( 

1485 new_impl.data, 

1486 dims=self.dims, 

1487 implementation=new_impl.impl_type, 

1488 ) 

1489 

1490 def expm(self): 

1491 """Matrix exponential.""" 

1492 return expm(self) 

1493 

1494 def powm(self, n): 

1495 """Matrix power. 

1496 

1497 Args: 

1498 n: Exponent (integer or float). 

1499 

1500 Returns: 

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

1502 """ 

1503 return powm(self, n) 

1504 

1505 def cosm(self): 

1506 """Matrix cosine.""" 

1507 return cosm(self) 

1508 

1509 def sinm(self): 

1510 """Matrix sine.""" 

1511 return sinm(self) 

1512 

1513 def tr(self, **kwargs): 

1514 """Full trace.""" 

1515 return tr(self, **kwargs) 

1516 

1517 def trace(self, **kwargs): 

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

1519 return tr(self, **kwargs) 

1520 

1521 def ptrace(self, indx): 

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

1523 

1524 Args: 

1525 indx: Index of the subsystem to trace out. 

1526 

1527 Returns: 

1528 Reduced density matrix. 

1529 """ 

1530 return ptrace(self, indx) 

1531 

1532 def eigenstates(self): 

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

1534 return eigenstates(self) 

1535 

1536 def eigenenergies(self): 

1537 """Eigenvalues of this operator.""" 

1538 return eigenenergies(self) 

1539 

1540 def eigenvalues(self): 

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

1542 return eigenenergies(self) 

1543 

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

1545 """Collapse batch dimensions. 

1546 

1547 Args: 

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

1549 

1550 Returns: 

1551 A non-batched ``Qarray``. 

1552 """ 

1553 return collapse(self, mode=mode) 

1554 

1555 

1556# Qarray operations --------------------------------------------------------------------- 

1557 

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

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

1560 

1561 Args: 

1562 qarr_list: List of Qarrays to concatenate. 

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

1564 

1565 Returns: 

1566 Concatenated Qarray. 

1567 """ 

1568 

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

1570 

1571 if len(non_empty_qarr_list) == 0: 

1572 return Qarray.from_list([]) 

1573 

1574 concatenated_data = jnp.concatenate( 

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

1576 ) 

1577 

1578 dims = non_empty_qarr_list[0].dims 

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

1580 

1581 

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

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

1584 

1585 Args: 

1586 qarr: Quantum array with optional batch dimensions. 

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

1588 

1589 Returns: 

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

1591 """ 

1592 

1593 if mode == "sum": 

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

1595 return qarr 

1596 

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

1598 

1599 # Preserve implementation type 

1600 implementation = qarr.impl_type 

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

1602 

1603 

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

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

1606 

1607 Args: 

1608 qarr: Input quantum array. 

1609 indices: New ordering of subsystem indices. 

1610 

1611 Returns: 

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

1613 """ 

1614 

1615 qarr = qarr.to_dense() 

1616 

1617 indices = list(indices) 

1618 

1619 shaped_data = qarr.shaped_data 

1620 dims = qarr.dims 

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

1622 

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

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

1625 

1626 shaped_data = shaped_data.transpose(reshape_indices) 

1627 new_dims = ( 

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

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

1630 ) 

1631 

1632 full_dims = prod(dims[0]) 

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

1634 

1635 # Preserve implementation type 

1636 implementation = qarr.impl_type 

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

1638 

1639 

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

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

1642 

1643 Args: 

1644 qarr: Input quantum array. 

1645 

1646 Returns: 

1647 Normalized quantum array. 

1648 """ 

1649 return qarr / qarr.norm() 

1650 

1651 

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

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

1654 

1655 Sparse paths (no densification): 

1656 

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

1658 batch dimensions). 

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

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

1661 operators convert to dense first. 

1662 

1663 Args: 

1664 qarr: Input quantum array. 

1665 

1666 Returns: 

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

1668 """ 

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

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

1671 

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

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

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

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

1676 

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

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

1679 

1680 qarr = qarr.to_dense() 

1681 

1682 qdata = qarr.data 

1683 bdims = qarr.bdims 

1684 

1685 if qarr.qtype == Qtypes.oper: 

1686 qdata_dag = qarr.dag().data 

1687 

1688 if len(bdims) > 0: 

1689 qdata = qdata.reshape(-1, qdata.shape[-2], qdata.shape[-1]) 

1690 qdata_dag = qdata_dag.reshape(-1, qdata_dag.shape[-2], qdata_dag.shape[-1]) 

1691 

1692 evals, _ = vmap(jnp.linalg.eigh)(qdata @ qdata_dag) 

1693 rho_norm = jnp.sum(jnp.sqrt(jnp.abs(evals)), axis=-1) 

1694 rho_norm = rho_norm.reshape(*bdims) 

1695 return rho_norm 

1696 else: 

1697 evals, _ = jnp.linalg.eigh(qdata @ qdata_dag) 

1698 rho_norm = jnp.sum(jnp.sqrt(jnp.abs(evals))) 

1699 return rho_norm 

1700 

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

1702 if len(bdims) > 0: 

1703 qdata = qdata.reshape(-1, qdata.shape[-2], qdata.shape[-1]) 

1704 return vmap(jnp.linalg.norm)(qdata).reshape(*bdims) 

1705 else: 

1706 return jnp.linalg.norm(qdata) 

1707 

1708 

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

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

1711 

1712 Args: 

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

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

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

1716 

1717 Returns: 

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

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

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

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

1722 

1723 Note: 

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

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

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

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

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

1729 """ 

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

1731 

1732 if parallel: 

1733 # Determine target implementation: highest PROMOTION_ORDER wins. 

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

1735 target_impl_type = max( 

1736 (arg.impl_type for arg in args), 

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

1738 ) 

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

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

1741 data = dense_args[0].data 

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

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

1744 for arg in dense_args[1:]: 

1745 a, b = data, arg.data 

1746 if len(a.shape) > len(b.shape): 

1747 batch_dim = a.shape[:-2] 

1748 elif len(a.shape) == len(b.shape): 

1749 batch_dim = a.shape[:-2] if prod(a.shape[:-2]) > prod(b.shape[:-2]) else b.shape[:-2] 

1750 else: 

1751 batch_dim = b.shape[:-2] 

1752 

1753 # NOTE: implementation einsum should be used when available 

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

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

1756 ) 

1757 dims_0 = dims_0 + arg.dims[0] 

1758 dims_1 = dims_1 + arg.dims[1] 

1759 return Qarray.create(data, dims=(dims_0, dims_1), implementation=target_impl_type) 

1760 

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

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

1763 current_impl = args[0]._impl 

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

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

1766 for arg in args[1:]: 

1767 current_impl = current_impl.kron(arg._impl) 

1768 dims_0 = dims_0 + arg.dims[0] 

1769 dims_1 = dims_1 + arg.dims[1] 

1770 return Qarray.create(current_impl.data, dims=(dims_0, dims_1), 

1771 implementation=current_impl.impl_type) 

1772 

1773 

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

1775 """Full trace of *qarr*. 

1776 

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

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

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

1780 dimensions in jaxquantum's convention). 

1781 

1782 Args: 

1783 qarr: Input quantum array. 

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

1785 ``axis1``, ``axis2``). 

1786 

1787 Returns: 

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

1789 """ 

1790 if qarr.is_sparse_bcoo: 

1791 return qarr._impl.trace() 

1792 if qarr.is_sparse_dia: 

1793 return qarr._impl.trace() 

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

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

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

1797 

1798 

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

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

1801 

1802 Args: 

1803 qarr: Input quantum array. 

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

1805 

1806 Returns: 

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

1808 """ 

1809 return tr(qarr, **kwargs) 

1810 

1811 

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

1813 """Matrix exponential of a raw array. 

1814 

1815 Args: 

1816 data: Dense matrix array. 

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

1818 

1819 Returns: 

1820 The matrix exponential. 

1821 """ 

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

1823 

1824 

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

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

1827 

1828 Args: 

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

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

1831 

1832 Returns: 

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

1834 """ 

1835 dims = qarr.dims 

1836 # Convert to dense for expm 

1837 dense_data = qarr.to_dense().data 

1838 data = expm_data(dense_data, **kwargs) 

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

1840 

1841 

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

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

1844 

1845 Args: 

1846 qarr: Input quantum array. 

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

1848 powers diagonalise the matrix. 

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

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

1851 

1852 Returns: 

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

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

1855 

1856 Raises: 

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

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

1859 """ 

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

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

1862 new_impl = qarr._impl.powm(n) 

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

1864 

1865 # Convert to dense for powm 

1866 dense_qarr = qarr.to_dense() 

1867 

1868 if isinstance(n, int): 

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

1870 else: 

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

1872 if clip_eigvals: 

1873 evalues = jnp.maximum(evalues, 0) 

1874 else: 

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

1876 raise ValueError( 

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

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

1879 "Got a matrix with a negative eigenvalue." 

1880 ) 

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

1882 

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

1884 

1885 

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

1887 """Matrix cosine of a raw array. 

1888 

1889 Args: 

1890 data: Dense matrix array. 

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

1892 

1893 Returns: 

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

1895 """ 

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

1897 

1898 

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

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

1901 

1902 Args: 

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

1904 

1905 Returns: 

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

1907 """ 

1908 dims = qarr.dims 

1909 # Convert to dense for cosm 

1910 dense_data = qarr.to_dense().data 

1911 data = cosm_data(dense_data) 

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

1913 

1914 

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

1916 """Matrix sine of a raw array. 

1917 

1918 Args: 

1919 data: Dense matrix array. 

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

1921 

1922 Returns: 

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

1924 """ 

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

1926 

1927 

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

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

1930 

1931 Args: 

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

1933 

1934 Returns: 

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

1936 """ 

1937 dims = qarr.dims 

1938 # Convert to dense for sinm 

1939 dense_data = qarr.to_dense().data 

1940 data = sinm_data(dense_data) 

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

1942 

1943 

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

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

1946 

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

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

1949 

1950 Args: 

1951 qarr: Non-batched input quantum array. 

1952 

1953 Returns: 

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

1955 

1956 Raises: 

1957 ValueError: If *qarr* has batch dimensions. 

1958 """ 

1959 if len(qarr.bdims) > 0: 

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

1961 

1962 dims = qarr.dims 

1963 if qarr.is_sparse_bcoo: 

1964 new_impl = qarr._impl.keep_only_diag() 

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

1966 if qarr.is_sparse_dia: 

1967 from jaxquantum.core.sparse_dia import SparseDiaImpl 

1968 impl = qarr._impl 

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

1970 if 0 in impl._offsets: 

1971 i = impl._offsets.index(0) 

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

1973 else: 

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

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

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

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

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

1979 

1980 

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

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

1983 

1984 Args: 

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

1986 

1987 Returns: 

1988 The ket form of *qarr*. 

1989 

1990 Raises: 

1991 ValueError: If *qarr* is an operator. 

1992 """ 

1993 if qarr.qtype == Qtypes.ket: 

1994 return qarr 

1995 elif qarr.qtype == Qtypes.bra: 

1996 return qarr.dag() 

1997 else: 

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

1999 

2000 

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

2002 """Eigenstates of a quantum array. 

2003 

2004 Args: 

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

2006 

2007 Returns: 

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

2009 sorted in ascending order. 

2010 """ 

2011 # Convert to dense for eigenstates 

2012 dense_qarr = qarr.to_dense() 

2013 

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

2015 idxs_sorted = jnp.argsort(evals, axis=-1) 

2016 

2017 dims = ket_from_op_dims(qarr.dims) 

2018 

2019 evals = jnp.take_along_axis(evals, idxs_sorted, axis=-1) 

2020 evecs = jnp.take_along_axis(evecs, idxs_sorted[..., None, :], axis=-1) 

2021 

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

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

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

2025 

2026 evecs = Qarray.create( 

2027 evecs, 

2028 dims=dims, 

2029 bdims=evecs.shape[:-1], 

2030 ) 

2031 

2032 return evals, evecs 

2033 

2034 

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

2036 """Eigenvalues of a quantum array. 

2037 

2038 Args: 

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

2040 

2041 Returns: 

2042 Sorted eigenvalues as a JAX array. 

2043 """ 

2044 # Convert to dense for eigenenergies 

2045 dense_qarr = qarr.to_dense() 

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

2047 return evals 

2048 

2049 

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

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

2052 

2053 Args: 

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

2055 indx: Index of the subsystem to trace out. 

2056 

2057 Returns: 

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

2059 """ 

2060 # Convert to dense for ptrace 

2061 dense_qarr = qarr.to_dense() 

2062 dense_qarr = ket2dm(dense_qarr) 

2063 rho = dense_qarr.shaped_data 

2064 dims = dense_qarr.dims 

2065 

2066 Nq = len(dims[0]) 

2067 

2068 indxs = [indx, indx + Nq] 

2069 for j in range(Nq): 

2070 if j == indx: 

2071 continue 

2072 indxs.append(j) 

2073 indxs.append(j + Nq) 

2074 

2075 bdims = dense_qarr.bdims 

2076 len_bdims = len(bdims) 

2077 bdims_indxs = list(range(len_bdims)) 

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

2079 rho = rho.transpose(indxs) 

2080 

2081 for j in range(Nq - 1): 

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

2083 

2084 return Qarray.create(rho) 

2085 

2086 

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

2088 """Conjugate transpose of *qarr*. 

2089 

2090 Args: 

2091 qarr: Input quantum array. 

2092 

2093 Returns: 

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

2095 """ 

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

2097 new_impl = qarr._impl.dag() 

2098 return Qarray.create( 

2099 new_impl.data, 

2100 dims=dims, 

2101 implementation=new_impl.impl_type, 

2102 ) 

2103 

2104 

2105def dag_data(arr) -> Array: 

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

2107 

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

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

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

2111 required here. 

2112 

2113 Args: 

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

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

2116 conjugation is applied (no transpose). 

2117 

2118 Returns: 

2119 Conjugate transpose with the last two axes swapped. 

2120 

2121 Raises: 

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

2123 """ 

2124 for impl_class in _IMPL_REGISTRY: 

2125 if impl_class.can_handle_data(arr): 

2126 return impl_class.dag_data(arr) 

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

2128 

2129 

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

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

2132 

2133 Args: 

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

2135 

2136 Returns: 

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

2138 """ 

2139 if qarr.qtype == Qtypes.oper: 

2140 return qarr 

2141 

2142 if qarr.qtype == Qtypes.bra: 

2143 qarr = qarr.dag() 

2144 

2145 return qarr @ qarr.dag() 

2146 

2147 

2148# Data level operations 

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

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

2151 

2152 Args: 

2153 data: Array to check. 

2154 

2155 Returns: 

2156 True if the last two dimensions are equal. 

2157 """ 

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

2159 

2160 

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

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

2163 

2164 Args: 

2165 data: Dense square matrix array. 

2166 n: Integer exponent. 

2167 

2168 Returns: 

2169 The *n*-th matrix power. 

2170 """ 

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

2172 

2173 

2174# Type aliases for readability 

2175DenseQarray = Qarray[DenseImpl] 

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

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

2178 

2179ARRAY_TYPES = (Array, ndarray, Qarray)