Coverage for qml_essentials / math.py: 97%

105 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-06-22 12:56 +0000

1import jax 

2import jax.numpy as jnp 

3from qml_essentials.operations import _cdtype 

4from scipy.linalg import logm 

5 

6 

7def logm_v(A: jnp.ndarray, **kwargs) -> jnp.ndarray: 

8 """ 

9 Compute the logarithm of a matrix. If the provided matrix has an additional 

10 batch dimension, the logarithm of each matrix is computed. 

11 

12 Args: 

13 A (jnp.ndarray): The (potentially batched) matrices of which to compute 

14 the logarithm. 

15 

16 Returns: 

17 jnp.ndarray: The log matrices 

18 """ 

19 # TODO: check warnings 

20 if len(A.shape) == 2: 

21 return logm(A, **kwargs) 

22 elif len(A.shape) == 3: 

23 AV = jnp.zeros(A.shape, dtype=_cdtype()) 

24 for i in range(A.shape[0]): 

25 AV = AV.at[i].set(logm(A[i], **kwargs)) 

26 return AV 

27 else: 

28 raise NotImplementedError("Unsupported shape of input matrix") 

29 

30 

31def _sqrt_matrix(density_matrix: jnp.ndarray) -> jnp.ndarray: 

32 r"""Compute the matrix square root of a density matrix. 

33 

34 Uses eigendecomposition: if :math:`\rho = V \Lambda V^\dagger`, then 

35 :math:`\sqrt{\rho} = V \sqrt{\Lambda} V^\dagger`. 

36 

37 Negative eigenvalues (numerical noise) are clamped to zero. 

38 

39 Args: 

40 density_matrix: Density matrix of shape ``(d, d)`` or ``(B, d, d)``. 

41 

42 Returns: 

43 The matrix square root with the same shape as the input. 

44 """ 

45 evs, vecs = jnp.linalg.eigh(density_matrix) 

46 evs = jnp.real(evs) 

47 evs = jnp.where(evs > 0.0, evs, 0.0) 

48 

49 if density_matrix.ndim == 3: 

50 # batched: (B, d, d) 

51 sqrt_evs = jnp.sqrt(evs)[:, :, None] * jnp.eye( 

52 density_matrix.shape[-1], dtype=_cdtype() 

53 ) 

54 return vecs @ sqrt_evs @ jnp.conj(jnp.transpose(vecs, (0, 2, 1))) 

55 

56 # single: (d, d) 

57 return vecs @ jnp.diag(jnp.sqrt(evs)) @ jnp.conj(vecs.T) 

58 

59 

60def _fidelity_statevector( 

61 state0: jnp.ndarray, 

62 state1: jnp.ndarray, 

63) -> jnp.ndarray: 

64 r"""Fidelity between two pure states (state vectors). 

65 

66 .. math:: 

67 

68 F(\ket{\psi}, \ket{\phi}) = \left|\braket{\psi | \phi}\right|^2 

69 The inputs are normalised before the overlap is computed so that 

70 the result is always in :math:`[0, 1]`. 

71 """ 

72 # Normalise so that unnormalised inputs don't produce F > 1. 

73 norm0 = jnp.linalg.norm(state0, axis=-1, keepdims=True) 

74 norm1 = jnp.linalg.norm(state1, axis=-1, keepdims=True) 

75 state0 = state0 / jnp.where(norm0 > 0, norm0, 1.0) 

76 state1 = state1 / jnp.where(norm1 > 0, norm1, 1.0) 

77 

78 batched0 = state0.ndim > 1 

79 batched1 = state1.ndim > 1 

80 

81 idx0 = "ab" if batched0 else "b" 

82 idx1 = "ab" if batched1 else "b" 

83 target = "a" if (batched0 or batched1) else "" 

84 

85 overlap = jnp.einsum(f"{idx0},{idx1}->{target}", jnp.conj(state0), state1) 

86 return jnp.abs(overlap) ** 2 

87 

88 

89def _fidelity_dm( 

90 state0: jnp.ndarray, 

91 state1: jnp.ndarray, 

92) -> jnp.ndarray: 

93 r"""Fidelity between two mixed states (density matrices).""" 

94 sqrt_state0 = _sqrt_matrix(state0) 

95 product = sqrt_state0 @ state1 @ sqrt_state0 

96 

97 evs = jnp.linalg.eigvalsh(product) 

98 evs = jnp.real(evs) 

99 evs = jnp.where(evs > 0.0, evs, 0.0) 

100 

101 return jnp.sum(jnp.sqrt(evs), axis=-1) ** 2 

102 

103 

104def fidelity( 

105 state0: jnp.ndarray, 

106 state1: jnp.ndarray, 

107) -> jnp.ndarray: 

108 r"""Compute the fidelity between two quantum states. 

109 

110 Accepts either state vectors or density matrices. 

111 

112 Args: 

113 state0: State vector or density matrix. 

114 state1: State vector or density matrix (same kind as *state0*). 

115 

116 Returns: 

117 Fidelity (scalar or shape ``(B,)``). 

118 

119 Raises: 

120 ValueError: If the two states have incompatible shapes or 

121 different representations (vector vs. matrix). 

122 """ 

123 state0 = jnp.asarray(state0, dtype=_cdtype()) 

124 state1 = jnp.asarray(state1, dtype=_cdtype()) 

125 

126 if state0.shape[-1] != state1.shape[-1]: 

127 raise ValueError("The two states must have the same number of wires.") 

128 

129 is_sv0 = state0.ndim <= 2 and ( 

130 state0.ndim == 1 or state0.shape[-2] != state0.shape[-1] 

131 ) 

132 is_sv1 = state1.ndim <= 2 and ( 

133 state1.ndim == 1 or state1.shape[-2] != state1.shape[-1] 

134 ) 

135 

136 if is_sv0 != is_sv1: 

137 raise ValueError( 

138 "Both states must be of the same kind " 

139 "(both state vectors or both density matrices)." 

140 ) 

141 

142 if is_sv0: 

143 return _fidelity_statevector(state0, state1) 

144 return _fidelity_dm(state0, state1) 

145 

146 

147def trace_distance( 

148 state0: jnp.ndarray, 

149 state1: jnp.ndarray, 

150) -> jnp.ndarray: 

151 r"""Compute the trace distance between two quantum states. 

152 

153 Supports single density matrices of shape ``(2**N, 2**N)`` and batched 

154 density matrices of shape ``(B, 2**N, 2**N)``. 

155 

156 Args: 

157 state0: Density matrix of shape ``(2**N, 2**N)`` or ``(B, 2**N, 2**N)``. 

158 state1: Density matrix of shape ``(2**N, 2**N)`` or ``(B, 2**N, 2**N)``. 

159 

160 Returns: 

161 Trace distance (scalar or shape ``(B,)``). 

162 """ 

163 state0 = jnp.asarray(state0, dtype=_cdtype()) 

164 state1 = jnp.asarray(state1, dtype=_cdtype()) 

165 

166 if state0.shape[-1] != state1.shape[-1]: 

167 raise ValueError("The two states must have the same number of wires.") 

168 

169 eigvals = jnp.abs(jnp.linalg.eigvalsh(state0 - state1)) 

170 return jnp.sum(eigvals, axis=-1) / 2 

171 

172 

173def phase_difference( 

174 state0: jnp.ndarray, 

175 state1: jnp.ndarray, 

176) -> jnp.ndarray: 

177 r"""Compute the phase difference between two state vectors. 

178 

179 A value of zero indicates the two states are related by at most a 

180 real global factor (i.e. no relative phase). The result lies in 

181 :math:`[-\pi, 1 + \pi]`. 

182 

183 Supports single state vectors of shape ``(2**N,)`` and batched state 

184 vectors of shape ``(B, 2**N)``. 

185 

186 Args: 

187 state0: State vector of shape ``(2**N,)`` or ``(B, 2**N)``. 

188 state1: State vector of shape ``(2**N,)`` or ``(B, 2**N)``. 

189 

190 Returns: 

191 Phase difference (scalar or shape ``(B,)``). 

192 """ 

193 state0 = jnp.asarray(state0, dtype=_cdtype()) 

194 state1 = jnp.asarray(state1, dtype=_cdtype()) 

195 

196 if state0.shape[-1] != state1.shape[-1]: 

197 raise ValueError("The two states must have the same number of wires.") 

198 

199 batched0 = state0.ndim > 1 

200 batched1 = state1.ndim > 1 

201 

202 idx0 = "ab" if batched0 else "b" 

203 idx1 = "ab" if batched1 else "b" 

204 target = "a" if (batched0 or batched1) else "" 

205 

206 inner = jnp.einsum(f"{idx0},{idx1}->{target}", jnp.conj(state0), state1) 

207 return jnp.angle(inner) 

208 

209 

210def _fubini_study_statevector( 

211 jac: jnp.ndarray, 

212 state: jnp.ndarray, 

213) -> jnp.ndarray: 

214 r"""Fubini-Study metric of a pure state. 

215 

216 The Fubini-Study metric is the real part of the quantum geometric tensor: 

217 

218 .. math:: 

219 

220 g_{ij} = \mathrm{Re}\left[ 

221 \braket{\partial_i\psi | \partial_j\psi} 

222 - \braket{\partial_i\psi | \psi}\braket{\psi | \partial_j\psi} 

223 \right] 

224 

225 It relates to the pure-state quantum Fisher information by 

226 :math:`F_{ij} = 4\,g_{ij}`. 

227 

228 Args: 

229 jac: Jacobian :math:`\partial\ket{\psi}/\partial\theta` of shape 

230 ``(d, P)`` with ``d = 2**N`` and ``P`` the number of parameters. 

231 state: State vector of shape ``(d,)``. 

232 

233 Returns: 

234 Real, symmetric metric of shape ``(P, P)``. 

235 """ 

236 A = jnp.conj(jac.T) @ jac # A_ij = <∂_i ψ | ∂_j ψ> 

237 v = jnp.conj(jac.T) @ state # v_i = <∂_i ψ | ψ> 

238 return jnp.real(A - jnp.outer(v, jnp.conj(v))) 

239 

240 

241def _qfi_statevector( 

242 jac: jnp.ndarray, 

243 state: jnp.ndarray, 

244) -> jnp.ndarray: 

245 r"""Quantum Fisher Information of a pure state. 

246 

247 For a normalised state :math:`\ket{\psi(\theta)}` the QFI is four times the 

248 Fubini-Study metric (see :func:`_fubini_study_statevector`): 

249 

250 .. math:: 

251 

252 F_{ij} = 4\,\mathrm{Re}\left[ 

253 \braket{\partial_i\psi | \partial_j\psi} 

254 - \braket{\partial_i\psi | \psi}\braket{\psi | \partial_j\psi} 

255 \right] 

256 

257 Args: 

258 jac: Jacobian :math:`\partial\ket{\psi}/\partial\theta` of shape 

259 ``(d, P)`` with ``d = 2**N`` and ``P`` the number of parameters. 

260 state: State vector of shape ``(d,)``. 

261 

262 Returns: 

263 Real, symmetric QFI matrix of shape ``(P, P)``. 

264 """ 

265 return 4.0 * _fubini_study_statevector(jac, state) 

266 

267 

268def _qfi_density( 

269 jac: jnp.ndarray, 

270 state: jnp.ndarray, 

271 eps: float = 1e-12, 

272) -> jnp.ndarray: 

273 r"""Quantum Fisher Information of a mixed state. 

274 

275 Using the symmetric logarithmic derivative, the QFI of a density matrix 

276 :math:`\rho = \sum_k p_k \ket{k}\bra{k}` reads 

277 

278 .. math:: 

279 

280 F_{ij} = 2 \sum_{k, l : p_k + p_l > 0} 

281 \frac{\mathrm{Re}\left( 

282 \braket{k | \partial_i\rho | l}\braket{l | \partial_j\rho | k} 

283 \right)}{p_k + p_l} 

284 

285 Eigenvalue pairs with :math:`p_k + p_l \le` ``eps`` are excluded from the 

286 sum. Negative eigenvalues (numerical noise) are clamped to zero. 

287 

288 Args: 

289 jac: Jacobian :math:`\partial\rho/\partial\theta` of shape 

290 ``(d, d, P)`` with ``d = 2**N`` and ``P`` the number of parameters. 

291 state: Density matrix of shape ``(d, d)``. 

292 eps: Threshold below which an eigenvalue pair is masked out. 

293 

294 Returns: 

295 Real, symmetric QFI matrix of shape ``(P, P)``. 

296 """ 

297 evals, evecs = jnp.linalg.eigh(state) 

298 evals = jnp.where(jnp.real(evals) > 0.0, jnp.real(evals), 0.0) 

299 

300 # ∂_i ρ in the eigenbasis: M[i]_kl = <k| ∂_i ρ |l>. 

301 drho = jnp.moveaxis(jac, -1, 0) # (P, d, d) 

302 M = jnp.conj(evecs.T) @ drho @ evecs # broadcast (d, d) over P 

303 

304 s = evals[:, None] + evals[None, :] # (d, d) 

305 weights = jnp.where(s > eps, 2.0 / s, 0.0) 

306 

307 # ∂_i ρ is Hermitian, so <l| ∂_j ρ |k> = conj(<k| ∂_j ρ |l>). 

308 F = jnp.einsum("ikl,jkl->ij", M * weights[None], jnp.conj(M)) 

309 return jnp.real(F) 

310 

311 

312def _state_and_jacobian(state_fn, params: jnp.ndarray): 

313 r"""Evaluate *state_fn* and its Jacobian at *params*. 

314 

315 The Jacobian is obtained with forward-mode automatic differentiation 

316 (:func:`jax.jacfwd`), which yields the complex Jacobian directly for the 

317 real-valued parameters. 

318 

319 Args: 

320 state_fn: Callable mapping *params* to a quantum state. 

321 params: Parameters at which to evaluate. 

322 

323 Returns: 

324 Tuple ``(state, jac)`` of the state and its Jacobian, both cast to the 

325 complex working dtype. 

326 """ 

327 state = jnp.asarray(state_fn(params), dtype=_cdtype()) 

328 jac = jnp.asarray(jax.jacfwd(state_fn)(params), dtype=_cdtype()) 

329 return state, jac 

330 

331 

332def quantum_fisher_information( 

333 state_fn, 

334 params: jnp.ndarray, 

335) -> jnp.ndarray: 

336 r"""Compute the Quantum Fisher Information (QFI) at a parameter point. 

337 

338 The QFI is the metric tensor of the state manifold evaluated at 

339 ``params``. It therefore requires the state as a *function* of the 

340 parameters rather than a single state; the Jacobian is obtained with 

341 forward-mode automatic differentiation (:func:`jax.jacfwd`), which yields 

342 the complex Jacobian directly for real-valued parameters. 

343 

344 Both pure and mixed states are supported and dispatched on the kind of 

345 object returned by *state_fn* (state vector vs. density matrix), mirroring 

346 :func:`fidelity`: 

347 

348 - state vector of shape ``(d,)`` -> Fubini-Study formula 

349 (see :func:`_qfi_statevector`), 

350 - density matrix of shape ``(d, d)`` -> symmetric logarithmic derivative 

351 formula (see :func:`_qfi_density`). 

352 

353 The returned matrix has shape ``(P, P)`` where ``P`` is the total number of 

354 parameters (the parameter axes of *params* are flattened). 

355 

356 Args: 

357 state_fn: Callable mapping *params* to a normalised quantum state. 

358 Typically ``lambda p: model(params=p, inputs=x)`` with the model's 

359 ``execution_type`` set to ``"state"`` (pure) or ``"density"`` 

360 (mixed). 

361 params: Parameters at which the QFI is evaluated. Must be passed in the 

362 shape expected by *state_fn* (e.g. the model's batched 

363 ``model.params``). 

364 

365 Returns: 

366 Real, symmetric QFI matrix of shape ``(P, P)``. 

367 

368 Raises: 

369 ValueError: If *state_fn* returns neither a state vector nor a square 

370 density matrix. 

371 """ 

372 state, jac = _state_and_jacobian(state_fn, params) 

373 

374 if state.ndim == 1: 

375 jac = jac.reshape(state.shape[0], -1) 

376 return _qfi_statevector(jac, state) 

377 elif state.ndim == 2 and state.shape[-1] == state.shape[-2]: 

378 jac = jac.reshape(state.shape[0], state.shape[1], -1) 

379 return _qfi_density(jac, state) 

380 else: 

381 raise ValueError( 

382 "state_fn must return a state vector of shape (d,) or a density " 

383 f"matrix of shape (d, d), got shape {state.shape}." 

384 ) 

385 

386 

387def fubini_study_metric( 

388 state_fn, 

389 params: jnp.ndarray, 

390) -> jnp.ndarray: 

391 r"""Compute the Fubini-Study metric tensor at a parameter point. 

392 

393 The Fubini-Study metric is the real part of the quantum geometric tensor on 

394 the manifold of pure states and equals the pure-state quantum Fisher 

395 information up to a factor of four, :math:`F_{ij} = 4\,g_{ij}`: 

396 

397 .. math:: 

398 

399 g_{ij} = \mathrm{Re}\left[ 

400 \braket{\partial_i\psi | \partial_j\psi} 

401 - \braket{\partial_i\psi | \psi}\braket{\psi | \partial_j\psi} 

402 \right] 

403 

404 It is only defined for pure states; *state_fn* must therefore return a 

405 normalised state vector. See :func:`quantum_fisher_information` for the 

406 calling convention. 

407 

408 Args: 

409 state_fn: Callable mapping *params* to a normalised state vector. 

410 Typically ``lambda p: model(params=p, inputs=x)`` with the model's 

411 ``execution_type`` set to ``"state"``. 

412 params: Parameters at which the metric is evaluated. 

413 

414 Returns: 

415 Real, symmetric metric of shape ``(P, P)`` where ``P`` is the total 

416 number of parameters. 

417 

418 Raises: 

419 ValueError: If *state_fn* does not return a state vector. 

420 """ 

421 state, jac = _state_and_jacobian(state_fn, params) 

422 

423 if state.ndim != 1: 

424 raise ValueError( 

425 "The Fubini-Study metric is only defined for pure states; " 

426 f"state_fn must return a state vector of shape (d,), got shape " 

427 f"{state.shape}." 

428 ) 

429 

430 jac = jac.reshape(state.shape[0], -1) 

431 return _fubini_study_statevector(jac, state)