Coverage for /usr/lib/python3/dist-packages/sympy/polys/numberfields/subfield.py: 14%

174 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1r""" 

2Functions in ``polys.numberfields.subfield`` solve the "Subfield Problem" and 

3allied problems, for algebraic number fields. 

4 

5Following Cohen (see [Cohen93]_ Section 4.5), we can define the main problem as 

6follows: 

7 

8* **Subfield Problem:** 

9 

10 Given two number fields $\mathbb{Q}(\alpha)$, $\mathbb{Q}(\beta)$ 

11 via the minimal polynomials for their generators $\alpha$ and $\beta$, decide 

12 whether one field is isomorphic to a subfield of the other. 

13 

14From a solution to this problem flow solutions to the following problems as 

15well: 

16 

17* **Primitive Element Problem:** 

18 

19 Given several algebraic numbers 

20 $\alpha_1, \ldots, \alpha_m$, compute a single algebraic number $\theta$ 

21 such that $\mathbb{Q}(\alpha_1, \ldots, \alpha_m) = \mathbb{Q}(\theta)$. 

22 

23* **Field Isomorphism Problem:** 

24 

25 Decide whether two number fields 

26 $\mathbb{Q}(\alpha)$, $\mathbb{Q}(\beta)$ are isomorphic. 

27 

28* **Field Membership Problem:** 

29 

30 Given two algebraic numbers $\alpha$, 

31 $\beta$, decide whether $\alpha \in \mathbb{Q}(\beta)$, and if so write 

32 $\alpha = f(\beta)$ for some $f(x) \in \mathbb{Q}[x]$. 

33""" 

34 

35from sympy.core.add import Add 

36from sympy.core.numbers import AlgebraicNumber 

37from sympy.core.singleton import S 

38from sympy.core.symbol import Dummy 

39from sympy.core.sympify import sympify, _sympify 

40from sympy.ntheory import sieve 

41from sympy.polys.densetools import dup_eval 

42from sympy.polys.domains import QQ 

43from sympy.polys.numberfields.minpoly import _choose_factor, minimal_polynomial 

44from sympy.polys.polyerrors import IsomorphismFailed 

45from sympy.polys.polytools import Poly, PurePoly, factor_list 

46from sympy.utilities import public 

47 

48from mpmath import MPContext 

49 

50 

51def is_isomorphism_possible(a, b): 

52 """Necessary but not sufficient test for isomorphism. """ 

53 n = a.minpoly.degree() 

54 m = b.minpoly.degree() 

55 

56 if m % n != 0: 

57 return False 

58 

59 if n == m: 

60 return True 

61 

62 da = a.minpoly.discriminant() 

63 db = b.minpoly.discriminant() 

64 

65 i, k, half = 1, m//n, db//2 

66 

67 while True: 

68 p = sieve[i] 

69 P = p**k 

70 

71 if P > half: 

72 break 

73 

74 if ((da % p) % 2) and not (db % P): 

75 return False 

76 

77 i += 1 

78 

79 return True 

80 

81 

82def field_isomorphism_pslq(a, b): 

83 """Construct field isomorphism using PSLQ algorithm. """ 

84 if not a.root.is_real or not b.root.is_real: 

85 raise NotImplementedError("PSLQ doesn't support complex coefficients") 

86 

87 f = a.minpoly 

88 g = b.minpoly.replace(f.gen) 

89 

90 n, m, prev = 100, b.minpoly.degree(), None 

91 ctx = MPContext() 

92 

93 for i in range(1, 5): 

94 A = a.root.evalf(n) 

95 B = b.root.evalf(n) 

96 

97 basis = [1, B] + [ B**i for i in range(2, m) ] + [-A] 

98 

99 ctx.dps = n 

100 coeffs = ctx.pslq(basis, maxcoeff=10**10, maxsteps=1000) 

101 

102 if coeffs is None: 

103 # PSLQ can't find an integer linear combination. Give up. 

104 break 

105 

106 if coeffs != prev: 

107 prev = coeffs 

108 else: 

109 # Increasing precision didn't produce anything new. Give up. 

110 break 

111 

112 # We have 

113 # c0 + c1*B + c2*B^2 + ... + cm-1*B^(m-1) - cm*A ~ 0. 

114 # So bring cm*A to the other side, and divide through by cm, 

115 # for an approximate representation of A as a polynomial in B. 

116 # (We know cm != 0 since `b.minpoly` is irreducible.) 

117 coeffs = [S(c)/coeffs[-1] for c in coeffs[:-1]] 

118 

119 # Throw away leading zeros. 

120 while not coeffs[-1]: 

121 coeffs.pop() 

122 

123 coeffs = list(reversed(coeffs)) 

124 h = Poly(coeffs, f.gen, domain='QQ') 

125 

126 # We only have A ~ h(B). We must check whether the relation is exact. 

127 if f.compose(h).rem(g).is_zero: 

128 # Now we know that h(b) is in fact equal to _some conjugate of_ a. 

129 # But from the very precise approximation A ~ h(B) we can assume 

130 # the conjugate is a itself. 

131 return coeffs 

132 else: 

133 n *= 2 

134 

135 return None 

136 

137 

138def field_isomorphism_factor(a, b): 

139 """Construct field isomorphism via factorization. """ 

140 _, factors = factor_list(a.minpoly, extension=b) 

141 for f, _ in factors: 

142 if f.degree() == 1: 

143 # Any linear factor f(x) represents some conjugate of a in QQ(b). 

144 # We want to know whether this linear factor represents a itself. 

145 # Let f = x - c 

146 c = -f.rep.TC() 

147 # Write c as polynomial in b 

148 coeffs = c.to_sympy_list() 

149 d, terms = len(coeffs) - 1, [] 

150 for i, coeff in enumerate(coeffs): 

151 terms.append(coeff*b.root**(d - i)) 

152 r = Add(*terms) 

153 # Check whether we got the number a 

154 if a.minpoly.same_root(r, a): 

155 return coeffs 

156 

157 # If none of the linear factors represented a in QQ(b), then in fact a is 

158 # not an element of QQ(b). 

159 return None 

160 

161 

162@public 

163def field_isomorphism(a, b, *, fast=True): 

164 r""" 

165 Find an embedding of one number field into another. 

166 

167 Explanation 

168 =========== 

169 

170 This function looks for an isomorphism from $\mathbb{Q}(a)$ onto some 

171 subfield of $\mathbb{Q}(b)$. Thus, it solves the Subfield Problem. 

172 

173 Examples 

174 ======== 

175 

176 >>> from sympy import sqrt, field_isomorphism, I 

177 >>> print(field_isomorphism(3, sqrt(2))) # doctest: +SKIP 

178 [3] 

179 >>> print(field_isomorphism( I*sqrt(3), I*sqrt(3)/2)) # doctest: +SKIP 

180 [2, 0] 

181 

182 Parameters 

183 ========== 

184 

185 a : :py:class:`~.Expr` 

186 Any expression representing an algebraic number. 

187 b : :py:class:`~.Expr` 

188 Any expression representing an algebraic number. 

189 fast : boolean, optional (default=True) 

190 If ``True``, we first attempt a potentially faster way of computing the 

191 isomorphism, falling back on a slower method if this fails. If 

192 ``False``, we go directly to the slower method, which is guaranteed to 

193 return a result. 

194 

195 Returns 

196 ======= 

197 

198 List of rational numbers, or None 

199 If $\mathbb{Q}(a)$ is not isomorphic to some subfield of 

200 $\mathbb{Q}(b)$, then return ``None``. Otherwise, return a list of 

201 rational numbers representing an element of $\mathbb{Q}(b)$ to which 

202 $a$ may be mapped, in order to define a monomorphism, i.e. an 

203 isomorphism from $\mathbb{Q}(a)$ to some subfield of $\mathbb{Q}(b)$. 

204 The elements of the list are the coefficients of falling powers of $b$. 

205 

206 """ 

207 a, b = sympify(a), sympify(b) 

208 

209 if not a.is_AlgebraicNumber: 

210 a = AlgebraicNumber(a) 

211 

212 if not b.is_AlgebraicNumber: 

213 b = AlgebraicNumber(b) 

214 

215 a = a.to_primitive_element() 

216 b = b.to_primitive_element() 

217 

218 if a == b: 

219 return a.coeffs() 

220 

221 n = a.minpoly.degree() 

222 m = b.minpoly.degree() 

223 

224 if n == 1: 

225 return [a.root] 

226 

227 if m % n != 0: 

228 return None 

229 

230 if fast: 

231 try: 

232 result = field_isomorphism_pslq(a, b) 

233 

234 if result is not None: 

235 return result 

236 except NotImplementedError: 

237 pass 

238 

239 return field_isomorphism_factor(a, b) 

240 

241 

242def _switch_domain(g, K): 

243 # An algebraic relation f(a, b) = 0 over Q can also be written 

244 # g(b) = 0 where g is in Q(a)[x] and h(a) = 0 where h is in Q(b)[x]. 

245 # This function transforms g into h where Q(b) = K. 

246 frep = g.rep.inject() 

247 hrep = frep.eject(K, front=True) 

248 

249 return g.new(hrep, g.gens[0]) 

250 

251 

252def _linsolve(p): 

253 # Compute root of linear polynomial. 

254 c, d = p.rep.rep 

255 return -d/c 

256 

257 

258@public 

259def primitive_element(extension, x=None, *, ex=False, polys=False): 

260 r""" 

261 Find a single generator for a number field given by several generators. 

262 

263 Explanation 

264 =========== 

265 

266 The basic problem is this: Given several algebraic numbers 

267 $\alpha_1, \alpha_2, \ldots, \alpha_n$, find a single algebraic number 

268 $\theta$ such that 

269 $\mathbb{Q}(\alpha_1, \alpha_2, \ldots, \alpha_n) = \mathbb{Q}(\theta)$. 

270 

271 This function actually guarantees that $\theta$ will be a linear 

272 combination of the $\alpha_i$, with non-negative integer coefficients. 

273 

274 Furthermore, if desired, this function will tell you how to express each 

275 $\alpha_i$ as a $\mathbb{Q}$-linear combination of the powers of $\theta$. 

276 

277 Examples 

278 ======== 

279 

280 >>> from sympy import primitive_element, sqrt, S, minpoly, simplify 

281 >>> from sympy.abc import x 

282 >>> f, lincomb, reps = primitive_element([sqrt(2), sqrt(3)], x, ex=True) 

283 

284 Then ``lincomb`` tells us the primitive element as a linear combination of 

285 the given generators ``sqrt(2)`` and ``sqrt(3)``. 

286 

287 >>> print(lincomb) 

288 [1, 1] 

289 

290 This means the primtiive element is $\sqrt{2} + \sqrt{3}$. 

291 Meanwhile ``f`` is the minimal polynomial for this primitive element. 

292 

293 >>> print(f) 

294 x**4 - 10*x**2 + 1 

295 >>> print(minpoly(sqrt(2) + sqrt(3), x)) 

296 x**4 - 10*x**2 + 1 

297 

298 Finally, ``reps`` (which was returned only because we set keyword arg 

299 ``ex=True``) tells us how to recover each of the generators $\sqrt{2}$ and 

300 $\sqrt{3}$ as $\mathbb{Q}$-linear combinations of the powers of the 

301 primitive element $\sqrt{2} + \sqrt{3}$. 

302 

303 >>> print([S(r) for r in reps[0]]) 

304 [1/2, 0, -9/2, 0] 

305 >>> theta = sqrt(2) + sqrt(3) 

306 >>> print(simplify(theta**3/2 - 9*theta/2)) 

307 sqrt(2) 

308 >>> print([S(r) for r in reps[1]]) 

309 [-1/2, 0, 11/2, 0] 

310 >>> print(simplify(-theta**3/2 + 11*theta/2)) 

311 sqrt(3) 

312 

313 Parameters 

314 ========== 

315 

316 extension : list of :py:class:`~.Expr` 

317 Each expression must represent an algebraic number $\alpha_i$. 

318 x : :py:class:`~.Symbol`, optional (default=None) 

319 The desired symbol to appear in the computed minimal polynomial for the 

320 primitive element $\theta$. If ``None``, we use a dummy symbol. 

321 ex : boolean, optional (default=False) 

322 If and only if ``True``, compute the representation of each $\alpha_i$ 

323 as a $\mathbb{Q}$-linear combination over the powers of $\theta$. 

324 polys : boolean, optional (default=False) 

325 If ``True``, return the minimal polynomial as a :py:class:`~.Poly`. 

326 Otherwise return it as an :py:class:`~.Expr`. 

327 

328 Returns 

329 ======= 

330 

331 Pair (f, coeffs) or triple (f, coeffs, reps), where: 

332 ``f`` is the minimal polynomial for the primitive element. 

333 ``coeffs`` gives the primitive element as a linear combination of the 

334 given generators. 

335 ``reps`` is present if and only if argument ``ex=True`` was passed, 

336 and is a list of lists of rational numbers. Each list gives the 

337 coefficients of falling powers of the primitive element, to recover 

338 one of the original, given generators. 

339 

340 """ 

341 if not extension: 

342 raise ValueError("Cannot compute primitive element for empty extension") 

343 extension = [_sympify(ext) for ext in extension] 

344 

345 if x is not None: 

346 x, cls = sympify(x), Poly 

347 else: 

348 x, cls = Dummy('x'), PurePoly 

349 

350 if not ex: 

351 gen, coeffs = extension[0], [1] 

352 g = minimal_polynomial(gen, x, polys=True) 

353 for ext in extension[1:]: 

354 if ext.is_Rational: 

355 coeffs.append(0) 

356 continue 

357 _, factors = factor_list(g, extension=ext) 

358 g = _choose_factor(factors, x, gen) 

359 s, _, g = g.sqf_norm() 

360 gen += s*ext 

361 coeffs.append(s) 

362 

363 if not polys: 

364 return g.as_expr(), coeffs 

365 else: 

366 return cls(g), coeffs 

367 

368 gen, coeffs = extension[0], [1] 

369 f = minimal_polynomial(gen, x, polys=True) 

370 K = QQ.algebraic_field((f, gen)) # incrementally constructed field 

371 reps = [K.unit] # representations of extension elements in K 

372 for ext in extension[1:]: 

373 if ext.is_Rational: 

374 coeffs.append(0) # rational ext is not included in the expression of a primitive element 

375 reps.append(K.convert(ext)) # but it is included in reps 

376 continue 

377 p = minimal_polynomial(ext, x, polys=True) 

378 L = QQ.algebraic_field((p, ext)) 

379 _, factors = factor_list(f, domain=L) 

380 f = _choose_factor(factors, x, gen) 

381 s, g, f = f.sqf_norm() 

382 gen += s*ext 

383 coeffs.append(s) 

384 K = QQ.algebraic_field((f, gen)) 

385 h = _switch_domain(g, K) 

386 erep = _linsolve(h.gcd(p)) # ext as element of K 

387 ogen = K.unit - s*erep # old gen as element of K 

388 reps = [dup_eval(_.rep, ogen, K) for _ in reps] + [erep] 

389 

390 if K.ext.root.is_Rational: # all extensions are rational 

391 H = [K.convert(_).rep for _ in extension] 

392 coeffs = [0]*len(extension) 

393 f = cls(x, domain=QQ) 

394 else: 

395 H = [_.rep for _ in reps] 

396 if not polys: 

397 return f.as_expr(), coeffs, H 

398 else: 

399 return f, coeffs, H 

400 

401 

402@public 

403def to_number_field(extension, theta=None, *, gen=None, alias=None): 

404 r""" 

405 Express one algebraic number in the field generated by another. 

406 

407 Explanation 

408 =========== 

409 

410 Given two algebraic numbers $\eta, \theta$, this function either expresses 

411 $\eta$ as an element of $\mathbb{Q}(\theta)$, or else raises an exception 

412 if $\eta \not\in \mathbb{Q}(\theta)$. 

413 

414 This function is essentially just a convenience, utilizing 

415 :py:func:`~.field_isomorphism` (our solution of the Subfield Problem) to 

416 solve this, the Field Membership Problem. 

417 

418 As an additional convenience, this function allows you to pass a list of 

419 algebraic numbers $\alpha_1, \alpha_2, \ldots, \alpha_n$ instead of $\eta$. 

420 It then computes $\eta$ for you, as a solution of the Primitive Element 

421 Problem, using :py:func:`~.primitive_element` on the list of $\alpha_i$. 

422 

423 Examples 

424 ======== 

425 

426 >>> from sympy import sqrt, to_number_field 

427 >>> eta = sqrt(2) 

428 >>> theta = sqrt(2) + sqrt(3) 

429 >>> a = to_number_field(eta, theta) 

430 >>> print(type(a)) 

431 <class 'sympy.core.numbers.AlgebraicNumber'> 

432 >>> a.root 

433 sqrt(2) + sqrt(3) 

434 >>> print(a) 

435 sqrt(2) 

436 >>> a.coeffs() 

437 [1/2, 0, -9/2, 0] 

438 

439 We get an :py:class:`~.AlgebraicNumber`, whose ``.root`` is $\theta$, whose 

440 value is $\eta$, and whose ``.coeffs()`` show how to write $\eta$ as a 

441 $\mathbb{Q}$-linear combination in falling powers of $\theta$. 

442 

443 Parameters 

444 ========== 

445 

446 extension : :py:class:`~.Expr` or list of :py:class:`~.Expr` 

447 Either the algebraic number that is to be expressed in the other field, 

448 or else a list of algebraic numbers, a primitive element for which is 

449 to be expressed in the other field. 

450 theta : :py:class:`~.Expr`, None, optional (default=None) 

451 If an :py:class:`~.Expr` representing an algebraic number, behavior is 

452 as described under **Explanation**. If ``None``, then this function 

453 reduces to a shorthand for calling :py:func:`~.primitive_element` on 

454 ``extension`` and turning the computed primitive element into an 

455 :py:class:`~.AlgebraicNumber`. 

456 gen : :py:class:`~.Symbol`, None, optional (default=None) 

457 If provided, this will be used as the generator symbol for the minimal 

458 polynomial in the returned :py:class:`~.AlgebraicNumber`. 

459 alias : str, :py:class:`~.Symbol`, None, optional (default=None) 

460 If provided, this will be used as the alias symbol for the returned 

461 :py:class:`~.AlgebraicNumber`. 

462 

463 Returns 

464 ======= 

465 

466 AlgebraicNumber 

467 Belonging to $\mathbb{Q}(\theta)$ and equaling $\eta$. 

468 

469 Raises 

470 ====== 

471 

472 IsomorphismFailed 

473 If $\eta \not\in \mathbb{Q}(\theta)$. 

474 

475 See Also 

476 ======== 

477 

478 field_isomorphism 

479 primitive_element 

480 

481 """ 

482 if hasattr(extension, '__iter__'): 

483 extension = list(extension) 

484 else: 

485 extension = [extension] 

486 

487 if len(extension) == 1 and isinstance(extension[0], tuple): 

488 return AlgebraicNumber(extension[0], alias=alias) 

489 

490 minpoly, coeffs = primitive_element(extension, gen, polys=True) 

491 root = sum([ coeff*ext for coeff, ext in zip(coeffs, extension) ]) 

492 

493 if theta is None: 

494 return AlgebraicNumber((minpoly, root), alias=alias) 

495 else: 

496 theta = sympify(theta) 

497 

498 if not theta.is_AlgebraicNumber: 

499 theta = AlgebraicNumber(theta, gen=gen, alias=alias) 

500 

501 coeffs = field_isomorphism(root, theta) 

502 

503 if coeffs is not None: 

504 return AlgebraicNumber(theta, coeffs, alias=alias) 

505 else: 

506 raise IsomorphismFailed( 

507 "%s is not in a subfield of %s" % (root, theta.root))