Coverage for qml_essentials / random_sampling.py: 96%

82 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-08-18 14:58 +0000

1import jax 

2import jax.numpy as jnp 

3from typing import Optional 

4from qml_essentials.operations import _cdtype 

5 

6_DEFAULT_SEED = 1000 

7 

8 

9def _ginibre(key: jax.random.PRNGKey, rows: int, cols: int) -> jnp.ndarray: 

10 r"""Draw a complex Ginibre matrix :math:`G \in \mathbb{C}^{rows \times cols}`. 

11 

12 Each entry is :math:`G_{jk} = a_{jk} + i\,b_{jk}` with 

13 :math:`a_{jk}, b_{jk} \sim \mathcal{N}(0, 1)` i.i.d. standard normal (the 

14 Ginibre ensemble). The result is cast to the active complex dtype via 

15 :func:`qml_essentials.operations._cdtype`. 

16 

17 Args: 

18 key (jax.random.PRNGKey): JAX random key, split internally into a real 

19 and an imaginary part. 

20 rows (int): Number of rows :math:`d`. 

21 cols (int): Number of columns :math:`K`. 

22 

23 Returns: 

24 jnp.ndarray: Complex Ginibre matrix of shape ``(rows, cols)``. 

25 """ 

26 key_re, key_im = jax.random.split(key) 

27 real = jax.random.normal(key_re, shape=(rows, cols)) 

28 imag = jax.random.normal(key_im, shape=(rows, cols)) 

29 return (real + 1j * imag).astype(_cdtype()) 

30 

31 

32def _haar_unitary(key: jax.random.PRNGKey, dim: int) -> jnp.ndarray: 

33 r"""Draw a Haar-random unitary :math:`U \in U(d)`. 

34 

35 Implements the QR construction of Mezzadri (*How to generate random 

36 matrices from the classical compact groups*, arXiv:math-ph/0609050): draw a 

37 Ginibre matrix :math:`Z`, take its QR decomposition :math:`Z = QR`, and 

38 remove the phase ambiguity via 

39 :math:`U = Q\,\Lambda`, :math:`\Lambda = \mathrm{diag}(R_{ii}/|R_{ii}|)`, 

40 so that :math:`U` is Haar-distributed. 

41 

42 Args: 

43 key (jax.random.PRNGKey): JAX random key for the Ginibre draw. 

44 dim (int): Dimension :math:`d` of the unitary. 

45 

46 Returns: 

47 jnp.ndarray: Haar-random unitary of shape ``(dim, dim)``. 

48 """ 

49 z = _ginibre(key, dim, dim) 

50 q, r = jnp.linalg.qr(z) 

51 diag_r = jnp.diagonal(r) 

52 # Guard against a zero pivot (|0| -> 1 keeps the phase at 1). 

53 phases = diag_r / jnp.where(jnp.abs(diag_r) > 0, jnp.abs(diag_r), 1.0) 

54 return q * phases[None, :] 

55 

56 

57def _normalize_density(rho: jnp.ndarray) -> jnp.ndarray: 

58 r"""Hermitize and trace-normalize a single density matrix. 

59 

60 Applies :math:`\rho \leftarrow (\rho + \rho^\dagger)/2` to remove numerical 

61 anti-Hermitian noise, then divides by :math:`\mathrm{Tr}\,\rho` so that 

62 :math:`\mathrm{Tr}\,\rho = 1`. 

63 

64 Args: 

65 rho (jnp.ndarray): Unnormalized matrix of shape ``(d, d)``. 

66 

67 Returns: 

68 jnp.ndarray: Hermitian, unit-trace density matrix of shape ``(d, d)``. 

69 """ 

70 rho = (rho + jnp.conj(rho).T) / 2.0 

71 return rho / jnp.trace(rho) 

72 

73 

74class DensityMatrix: 

75 r"""Random density-matrix samplers. 

76 

77 Samplers for the standard random-density-matrix ensembles, built from the 

78 Ginibre construction 

79 :math:`\rho = G G^\dagger / \mathrm{Tr}(G G^\dagger)` and/or a Haar-random 

80 unitary. Every sampler returns a batched array of shape 

81 ``(n_samples, 2**n_qubits, 2**n_qubits)``, consistent with 

82 :func:`qml_essentials.entanglement.sample_random_separable_states`. 

83 

84 The module is structured so that ``StateVector``, ``Hermitian`` and 

85 ``Unitary`` sampler classes can be added later, reusing the module-level 

86 primitives :func:`_ginibre` and :func:`_haar_unitary`. 

87 """ 

88 

89 @classmethod 

90 def induced( 

91 cls, 

92 n_qubits: int, 

93 n_samples: int = 1, 

94 rank: Optional[int] = None, 

95 random_key: Optional[jax.random.PRNGKey] = None, 

96 ) -> jnp.ndarray: 

97 r"""Sample from the induced measure via the Ginibre construction. 

98 

99 Draws :math:`\rho = G G^\dagger / \mathrm{Tr}(G G^\dagger)` with 

100 :math:`G` a :math:`d \times K` complex Ginibre matrix, 

101 :math:`d = 2^{n\_qubits}` and :math:`K = \mathrm{rank}`. This is the 

102 induced measure of Zyczkowski & Sommers (*Induced measures in the space 

103 of mixed quantum states*, arXiv:quant-ph/0012101). The sampled state 

104 has rank :math:`\min(d, K)` almost surely: :math:`K = 1` yields (Haar) 

105 pure states and :math:`K = d` recovers the Hilbert-Schmidt measure. The 

106 mean purity is :math:`\mathbb{E}[\mathrm{Tr}\,\rho^2] = (d + K)/(dK + 1)`. 

107 

108 Args: 

109 n_qubits (int): Number of qubits; :math:`d = 2^{n\_qubits}`. 

110 n_samples (int): Number of density matrices to draw. Defaults to 1. 

111 rank (Optional[int]): Number of Ginibre columns :math:`K`. When 

112 ``None`` (default) it is set to :math:`d`, recovering the 

113 Hilbert-Schmidt measure. Must satisfy ``rank >= 1``. 

114 random_key (Optional[jax.random.PRNGKey]): JAX random key. When 

115 ``None``, falls back to ``jax.random.key(1000)`` (matching the 

116 default ``random_seed`` of :class:`~qml_essentials.model.Model`). 

117 

118 Returns: 

119 jnp.ndarray: Density matrices of shape 

120 ``(n_samples, 2**n_qubits, 2**n_qubits)``. 

121 

122 Raises: 

123 ValueError: If ``rank`` is given and ``rank < 1``. 

124 """ 

125 d = 2**n_qubits 

126 if rank is None: 

127 rank = d 

128 if rank < 1: 

129 raise ValueError(f"rank must be >= 1, got {rank}.") 

130 if random_key is None: 

131 random_key = jax.random.key(_DEFAULT_SEED) 

132 

133 def _sample(key: jax.random.PRNGKey) -> jnp.ndarray: 

134 g = _ginibre(key, d, rank) 

135 rho = g @ jnp.conj(g).T 

136 return _normalize_density(rho) 

137 

138 keys = jax.random.split(random_key, n_samples) 

139 return jax.vmap(_sample)(keys) 

140 

141 @classmethod 

142 def hilbert_schmidt( 

143 cls, 

144 n_qubits: int, 

145 n_samples: int = 1, 

146 random_key: Optional[jax.random.PRNGKey] = None, 

147 ) -> jnp.ndarray: 

148 r"""Sample from the Hilbert-Schmidt measure. 

149 

150 Special case of the induced measure with :math:`K = d`: 

151 :math:`\rho = G G^\dagger / \mathrm{Tr}(G G^\dagger)` with :math:`G` a 

152 square :math:`d \times d` complex Ginibre matrix. The Hilbert-Schmidt 

153 measure is the flat measure induced by the Hilbert-Schmidt metric 

154 (Zyczkowski & Sommers, arXiv:quant-ph/0012101). Delegates to 

155 :meth:`induced` with ``rank = d``. 

156 

157 Args: 

158 n_qubits (int): Number of qubits; :math:`d = 2^{n\_qubits}`. 

159 n_samples (int): Number of density matrices to draw. Defaults to 1. 

160 random_key (Optional[jax.random.PRNGKey]): JAX random key. When 

161 ``None``, falls back to ``jax.random.key(1000)``. 

162 

163 Returns: 

164 jnp.ndarray: Density matrices of shape 

165 ``(n_samples, 2**n_qubits, 2**n_qubits)``. 

166 """ 

167 return cls.induced( 

168 n_qubits=n_qubits, 

169 n_samples=n_samples, 

170 rank=2**n_qubits, 

171 random_key=random_key, 

172 ) 

173 

174 @classmethod 

175 def bures( 

176 cls, 

177 n_qubits: int, 

178 n_samples: int = 1, 

179 random_key: Optional[jax.random.PRNGKey] = None, 

180 ) -> jnp.ndarray: 

181 r"""Sample from the Bures measure. 

182 

183 Uses the construction of Osipov, Sommers & Zyczkowski (*Random Bures 

184 mixed states and the distribution of their purity*, arXiv:1004.1655): 

185 

186 .. math:: 

187 \rho = \frac{(\mathbb{1} + U)\, G G^\dagger\, (\mathbb{1} + U)^\dagger} 

188 {\mathrm{Tr}\!\left[(\mathbb{1} + U)\, G G^\dagger\, 

189 (\mathbb{1} + U)^\dagger\right]}, 

190 

191 where :math:`G` is a square :math:`d \times d` complex Ginibre matrix 

192 and :math:`U` is an independently drawn Haar-random unitary. The Bures 

193 measure is induced by the Bures (statistical-distance) metric. 

194 

195 Args: 

196 n_qubits (int): Number of qubits; :math:`d = 2^{n\_qubits}`. 

197 n_samples (int): Number of density matrices to draw. Defaults to 1. 

198 random_key (Optional[jax.random.PRNGKey]): JAX random key. When 

199 ``None``, falls back to ``jax.random.key(1000)``. 

200 

201 Returns: 

202 jnp.ndarray: Density matrices of shape 

203 ``(n_samples, 2**n_qubits, 2**n_qubits)``. 

204 """ 

205 d = 2**n_qubits 

206 if random_key is None: 

207 random_key = jax.random.key(_DEFAULT_SEED) 

208 

209 eye = jnp.eye(d, dtype=_cdtype()) 

210 

211 def _sample(key: jax.random.PRNGKey) -> jnp.ndarray: 

212 key_g, key_u = jax.random.split(key) 

213 g = _ginibre(key_g, d, d) 

214 u = _haar_unitary(key_u, d) 

215 a = eye + u 

216 rho = a @ (g @ jnp.conj(g).T) @ jnp.conj(a).T 

217 return _normalize_density(rho) 

218 

219 keys = jax.random.split(random_key, n_samples) 

220 return jax.vmap(_sample)(keys) 

221 

222 @classmethod 

223 def eigen( 

224 cls, 

225 n_qubits: int, 

226 n_samples: int = 1, 

227 alpha: float = 1.0, 

228 eigenvalues: Optional[jnp.ndarray] = None, 

229 random_key: Optional[jax.random.PRNGKey] = None, 

230 ) -> jnp.ndarray: 

231 r"""Sample density matrices with a prescribed or Dirichlet spectrum. 

232 

233 Builds :math:`\rho = U\,\mathrm{diag}(\lambda)\,U^\dagger` with :math:`U` 

234 Haar-random. The eigenvalue vector :math:`\lambda` is either supplied 

235 via ``eigenvalues`` (then identical for every sample), or drawn per 

236 sample from a symmetric Dirichlet distribution 

237 :math:`\lambda \sim \mathrm{Dir}(\alpha \mathbf{1}_d)`. The default 

238 :math:`\alpha = 1` gives the uniform (flat) distribution on the 

239 probability simplex. 

240 

241 Note that this ensemble is, by construction, different from the 

242 Hilbert-Schmidt and induced measures: it lacks their eigenvalue 

243 repulsion (the squared Vandermonde factor). 

244 

245 Args: 

246 n_qubits (int): Number of qubits; :math:`d = 2^{n\_qubits}`. 

247 n_samples (int): Number of density matrices to draw. Defaults to 1. 

248 alpha (float): Symmetric Dirichlet concentration parameter, used 

249 only when ``eigenvalues`` is ``None``. Defaults to ``1.0``. 

250 eigenvalues (Optional[jnp.ndarray]): Optional fixed spectrum of 

251 length :math:`d`. Entries must be nonnegative and sum to 1. When 

252 given, the same spectrum is used for every sample and only 

253 :math:`U` is randomized. 

254 random_key (Optional[jax.random.PRNGKey]): JAX random key. When 

255 ``None``, falls back to ``jax.random.key(1000)``. 

256 

257 Returns: 

258 jnp.ndarray: Density matrices of shape 

259 ``(n_samples, 2**n_qubits, 2**n_qubits)``. 

260 

261 Raises: 

262 ValueError: If ``eigenvalues`` is supplied and is not a 

263 length-:math:`d` vector of nonnegative entries summing to 1. 

264 """ 

265 d = 2**n_qubits 

266 if random_key is None: 

267 random_key = jax.random.key(_DEFAULT_SEED) 

268 

269 fixed_eigs = None 

270 if eigenvalues is not None: 

271 fixed_eigs = cls._validate_eigenvalues(eigenvalues, d) 

272 

273 def _sample(key: jax.random.PRNGKey) -> jnp.ndarray: 

274 key_u, key_lam = jax.random.split(key) 

275 u = _haar_unitary(key_u, d) 

276 if fixed_eigs is None: 

277 lam = jax.random.dirichlet(key_lam, alpha * jnp.ones(d)) 

278 else: 

279 lam = fixed_eigs 

280 rho = (u * lam[None, :].astype(_cdtype())) @ jnp.conj(u).T 

281 return _normalize_density(rho) 

282 

283 keys = jax.random.split(random_key, n_samples) 

284 return jax.vmap(_sample)(keys) 

285 

286 @classmethod 

287 def _validate_eigenvalues(cls, eigenvalues: jnp.ndarray, dim: int) -> jnp.ndarray: 

288 r"""Validate a user-supplied eigenvalue (spectrum) vector. 

289 

290 Checks that the vector has length :math:`d`, is real and nonnegative, 

291 and sums to 1 (within tolerance). 

292 

293 Args: 

294 eigenvalues (jnp.ndarray): Candidate spectrum. 

295 dim (int): Expected length :math:`d = 2^{n\_qubits}`. 

296 

297 Returns: 

298 jnp.ndarray: Validated, real eigenvalue vector of shape ``(dim,)``. 

299 

300 Raises: 

301 ValueError: If the shape, nonnegativity, or unit-sum constraint is 

302 violated. 

303 """ 

304 eigs = jnp.asarray(eigenvalues) 

305 if eigs.shape != (dim,): 

306 raise ValueError(f"eigenvalues must have shape ({dim},), got {eigs.shape}.") 

307 eigs = jnp.real(eigs) 

308 if bool(jnp.any(eigs < -1e-10)): 

309 raise ValueError("eigenvalues must be nonnegative.") 

310 if not bool(jnp.isclose(jnp.sum(eigs), 1.0, atol=1e-8)): 

311 raise ValueError(f"eigenvalues must sum to 1, got {float(jnp.sum(eigs))}.") 

312 return eigs