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

225 statements  

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

1""" 

2Compute Galois groups of polynomials. 

3 

4We use algorithms from [1], with some modifications to use lookup tables for 

5resolvents. 

6 

7References 

8========== 

9 

10.. [1] Cohen, H. *A Course in Computational Algebraic Number Theory*. 

11 

12""" 

13 

14from collections import defaultdict 

15import random 

16 

17from sympy.core.symbol import Dummy, symbols 

18from sympy.ntheory.primetest import is_square 

19from sympy.polys.domains import ZZ 

20from sympy.polys.densebasic import dup_random 

21from sympy.polys.densetools import dup_eval 

22from sympy.polys.euclidtools import dup_discriminant 

23from sympy.polys.factortools import dup_factor_list, dup_irreducible_p 

24from sympy.polys.numberfields.galois_resolvents import ( 

25 GaloisGroupException, get_resolvent_by_lookup, define_resolvents, 

26 Resolvent, 

27) 

28from sympy.polys.numberfields.utilities import coeff_search 

29from sympy.polys.polytools import (Poly, poly_from_expr, 

30 PolificationFailed, ComputationFailed) 

31from sympy.polys.sqfreetools import dup_sqf_p 

32from sympy.utilities import public 

33 

34 

35class MaxTriesException(GaloisGroupException): 

36 ... 

37 

38 

39def tschirnhausen_transformation(T, max_coeff=10, max_tries=30, history=None, 

40 fixed_order=True): 

41 r""" 

42 Given a univariate, monic, irreducible polynomial over the integers, find 

43 another such polynomial defining the same number field. 

44 

45 Explanation 

46 =========== 

47 

48 See Alg 6.3.4 of [1]. 

49 

50 Parameters 

51 ========== 

52 

53 T : Poly 

54 The given polynomial 

55 max_coeff : int 

56 When choosing a transformation as part of the process, 

57 keep the coeffs between plus and minus this. 

58 max_tries : int 

59 Consider at most this many transformations. 

60 history : set, None, optional (default=None) 

61 Pass a set of ``Poly.rep``'s in order to prevent any of these 

62 polynomials from being returned as the polynomial ``U`` i.e. the 

63 transformation of the given polynomial *T*. The given poly *T* will 

64 automatically be added to this set, before we try to find a new one. 

65 fixed_order : bool, default True 

66 If ``True``, work through candidate transformations A(x) in a fixed 

67 order, from small coeffs to large, resulting in deterministic behavior. 

68 If ``False``, the A(x) are chosen randomly, while still working our way 

69 up from small coefficients to larger ones. 

70 

71 Returns 

72 ======= 

73 

74 Pair ``(A, U)`` 

75 

76 ``A`` and ``U`` are ``Poly``, ``A`` is the 

77 transformation, and ``U`` is the transformed polynomial that defines 

78 the same number field as *T*. The polynomial ``A`` maps the roots of 

79 *T* to the roots of ``U``. 

80 

81 Raises 

82 ====== 

83 

84 MaxTriesException 

85 if could not find a polynomial before exceeding *max_tries*. 

86 

87 """ 

88 X = Dummy('X') 

89 n = T.degree() 

90 if history is None: 

91 history = set() 

92 history.add(T.rep) 

93 

94 if fixed_order: 

95 coeff_generators = {} 

96 deg_coeff_sum = 3 

97 current_degree = 2 

98 

99 def get_coeff_generator(degree): 

100 gen = coeff_generators.get(degree, coeff_search(degree, 1)) 

101 coeff_generators[degree] = gen 

102 return gen 

103 

104 for i in range(max_tries): 

105 

106 # We never use linear A(x), since applying a fixed linear transformation 

107 # to all roots will only multiply the discriminant of T by a square 

108 # integer. This will change nothing important. In particular, if disc(T) 

109 # was zero before, it will still be zero now, and typically we apply 

110 # the transformation in hopes of replacing T by a squarefree poly. 

111 

112 if fixed_order: 

113 # If d is degree and c max coeff, we move through the dc-space 

114 # along lines of constant sum. First d + c = 3 with (d, c) = (2, 1). 

115 # Then d + c = 4 with (d, c) = (3, 1), (2, 2). Then d + c = 5 with 

116 # (d, c) = (4, 1), (3, 2), (2, 3), and so forth. For a given (d, c) 

117 # we go though all sets of coeffs where max = c, before moving on. 

118 gen = get_coeff_generator(current_degree) 

119 coeffs = next(gen) 

120 m = max(abs(c) for c in coeffs) 

121 if current_degree + m > deg_coeff_sum: 

122 if current_degree == 2: 

123 deg_coeff_sum += 1 

124 current_degree = deg_coeff_sum - 1 

125 else: 

126 current_degree -= 1 

127 gen = get_coeff_generator(current_degree) 

128 coeffs = next(gen) 

129 a = [ZZ(1)] + [ZZ(c) for c in coeffs] 

130 

131 else: 

132 # We use a progressive coeff bound, up to the max specified, since it 

133 # is preferable to succeed with smaller coeffs. 

134 # Give each coeff bound five tries, before incrementing. 

135 C = min(i//5 + 1, max_coeff) 

136 d = random.randint(2, n - 1) 

137 a = dup_random(d, -C, C, ZZ) 

138 

139 A = Poly(a, T.gen) 

140 U = Poly(T.resultant(X - A), X) 

141 if U.rep not in history and dup_sqf_p(U.rep.rep, ZZ): 

142 return A, U 

143 raise MaxTriesException 

144 

145 

146def has_square_disc(T): 

147 """Convenience to check if a Poly or dup has square discriminant. """ 

148 d = T.discriminant() if isinstance(T, Poly) else dup_discriminant(T, ZZ) 

149 return is_square(d) 

150 

151 

152def _galois_group_degree_3(T, max_tries=30, randomize=False): 

153 r""" 

154 Compute the Galois group of a polynomial of degree 3. 

155 

156 Explanation 

157 =========== 

158 

159 Uses Prop 6.3.5 of [1]. 

160 

161 """ 

162 from sympy.combinatorics.galois import S3TransitiveSubgroups 

163 return ((S3TransitiveSubgroups.A3, True) if has_square_disc(T) 

164 else (S3TransitiveSubgroups.S3, False)) 

165 

166 

167def _galois_group_degree_4_root_approx(T, max_tries=30, randomize=False): 

168 r""" 

169 Compute the Galois group of a polynomial of degree 4. 

170 

171 Explanation 

172 =========== 

173 

174 Follows Alg 6.3.7 of [1], using a pure root approximation approach. 

175 

176 """ 

177 from sympy.combinatorics.permutations import Permutation 

178 from sympy.combinatorics.galois import S4TransitiveSubgroups 

179 

180 X = symbols('X0 X1 X2 X3') 

181 # We start by considering the resolvent for the form 

182 # F = X0*X2 + X1*X3 

183 # and the group G = S4. In this case, the stabilizer H is D4 = < (0123), (02) >, 

184 # and a set of representatives of G/H is {I, (01), (03)} 

185 F1 = X[0]*X[2] + X[1]*X[3] 

186 s1 = [ 

187 Permutation(3), 

188 Permutation(3)(0, 1), 

189 Permutation(3)(0, 3) 

190 ] 

191 R1 = Resolvent(F1, X, s1) 

192 

193 # In the second half of the algorithm (if we reach it), we use another 

194 # form and set of coset representatives. However, we may need to permute 

195 # them first, so cannot form their resolvent now. 

196 F2_pre = X[0]*X[1]**2 + X[1]*X[2]**2 + X[2]*X[3]**2 + X[3]*X[0]**2 

197 s2_pre = [ 

198 Permutation(3), 

199 Permutation(3)(0, 2) 

200 ] 

201 

202 history = set() 

203 for i in range(max_tries): 

204 if i > 0: 

205 # If we're retrying, need a new polynomial T. 

206 _, T = tschirnhausen_transformation(T, max_tries=max_tries, 

207 history=history, 

208 fixed_order=not randomize) 

209 

210 R_dup, _, i0 = R1.eval_for_poly(T, find_integer_root=True) 

211 # If R is not squarefree, must retry. 

212 if not dup_sqf_p(R_dup, ZZ): 

213 continue 

214 

215 # By Prop 6.3.1 of [1], Gal(T) is contained in A4 iff disc(T) is square. 

216 sq_disc = has_square_disc(T) 

217 

218 if i0 is None: 

219 # By Thm 6.3.3 of [1], Gal(T) is not conjugate to any subgroup of the 

220 # stabilizer H = D4 that we chose. This means Gal(T) is either A4 or S4. 

221 return ((S4TransitiveSubgroups.A4, True) if sq_disc 

222 else (S4TransitiveSubgroups.S4, False)) 

223 

224 # Gal(T) is conjugate to a subgroup of H = D4, so it is either V, C4 

225 # or D4 itself. 

226 

227 if sq_disc: 

228 # Neither C4 nor D4 is contained in A4, so Gal(T) must be V. 

229 return (S4TransitiveSubgroups.V, True) 

230 

231 # Gal(T) can only be D4 or C4. 

232 # We will now use our second resolvent, with G being that conjugate of D4 that 

233 # Gal(T) is contained in. To determine the right conjugate, we will need 

234 # the permutation corresponding to the integer root we found. 

235 sigma = s1[i0] 

236 # Applying sigma means permuting the args of F, and 

237 # conjugating the set of coset representatives. 

238 F2 = F2_pre.subs(zip(X, sigma(X)), simultaneous=True) 

239 s2 = [sigma*tau*sigma for tau in s2_pre] 

240 R2 = Resolvent(F2, X, s2) 

241 R_dup, _, _ = R2.eval_for_poly(T) 

242 d = dup_discriminant(R_dup, ZZ) 

243 # If d is zero (R has a repeated root), must retry. 

244 if d == 0: 

245 continue 

246 if is_square(d): 

247 return (S4TransitiveSubgroups.C4, False) 

248 else: 

249 return (S4TransitiveSubgroups.D4, False) 

250 

251 raise MaxTriesException 

252 

253 

254def _galois_group_degree_4_lookup(T, max_tries=30, randomize=False): 

255 r""" 

256 Compute the Galois group of a polynomial of degree 4. 

257 

258 Explanation 

259 =========== 

260 

261 Based on Alg 6.3.6 of [1], but uses resolvent coeff lookup. 

262 

263 """ 

264 from sympy.combinatorics.galois import S4TransitiveSubgroups 

265 

266 history = set() 

267 for i in range(max_tries): 

268 R_dup = get_resolvent_by_lookup(T, 0) 

269 if dup_sqf_p(R_dup, ZZ): 

270 break 

271 _, T = tschirnhausen_transformation(T, max_tries=max_tries, 

272 history=history, 

273 fixed_order=not randomize) 

274 else: 

275 raise MaxTriesException 

276 

277 # Compute list L of degrees of irreducible factors of R, in increasing order: 

278 fl = dup_factor_list(R_dup, ZZ) 

279 L = sorted(sum([ 

280 [len(r) - 1] * e for r, e in fl[1] 

281 ], [])) 

282 

283 if L == [6]: 

284 return ((S4TransitiveSubgroups.A4, True) if has_square_disc(T) 

285 else (S4TransitiveSubgroups.S4, False)) 

286 

287 if L == [1, 1, 4]: 

288 return (S4TransitiveSubgroups.C4, False) 

289 

290 if L == [2, 2, 2]: 

291 return (S4TransitiveSubgroups.V, True) 

292 

293 assert L == [2, 4] 

294 return (S4TransitiveSubgroups.D4, False) 

295 

296 

297def _galois_group_degree_5_hybrid(T, max_tries=30, randomize=False): 

298 r""" 

299 Compute the Galois group of a polynomial of degree 5. 

300 

301 Explanation 

302 =========== 

303 

304 Based on Alg 6.3.9 of [1], but uses a hybrid approach, combining resolvent 

305 coeff lookup, with root approximation. 

306 

307 """ 

308 from sympy.combinatorics.galois import S5TransitiveSubgroups 

309 from sympy.combinatorics.permutations import Permutation 

310 

311 X5 = symbols("X0,X1,X2,X3,X4") 

312 res = define_resolvents() 

313 F51, _, s51 = res[(5, 1)] 

314 F51 = F51.as_expr(*X5) 

315 R51 = Resolvent(F51, X5, s51) 

316 

317 history = set() 

318 reached_second_stage = False 

319 for i in range(max_tries): 

320 if i > 0: 

321 _, T = tschirnhausen_transformation(T, max_tries=max_tries, 

322 history=history, 

323 fixed_order=not randomize) 

324 R51_dup = get_resolvent_by_lookup(T, 1) 

325 if not dup_sqf_p(R51_dup, ZZ): 

326 continue 

327 

328 # First stage 

329 # If we have not yet reached the second stage, then the group still 

330 # might be S5, A5, or M20, so must test for that. 

331 if not reached_second_stage: 

332 sq_disc = has_square_disc(T) 

333 

334 if dup_irreducible_p(R51_dup, ZZ): 

335 return ((S5TransitiveSubgroups.A5, True) if sq_disc 

336 else (S5TransitiveSubgroups.S5, False)) 

337 

338 if not sq_disc: 

339 return (S5TransitiveSubgroups.M20, False) 

340 

341 # Second stage 

342 reached_second_stage = True 

343 # R51 must have an integer root for T. 

344 # To choose our second resolvent, we need to know which conjugate of 

345 # F51 is a root. 

346 rounded_roots = R51.round_roots_to_integers_for_poly(T) 

347 # These are integers, and candidates to be roots of R51. 

348 # We find the first one that actually is a root. 

349 for permutation_index, candidate_root in rounded_roots.items(): 

350 if not dup_eval(R51_dup, candidate_root, ZZ): 

351 break 

352 

353 X = X5 

354 F2_pre = X[0]*X[1]**2 + X[1]*X[2]**2 + X[2]*X[3]**2 + X[3]*X[4]**2 + X[4]*X[0]**2 

355 s2_pre = [ 

356 Permutation(4), 

357 Permutation(4)(0, 1)(2, 4) 

358 ] 

359 

360 i0 = permutation_index 

361 sigma = s51[i0] 

362 F2 = F2_pre.subs(zip(X, sigma(X)), simultaneous=True) 

363 s2 = [sigma*tau*sigma for tau in s2_pre] 

364 R2 = Resolvent(F2, X, s2) 

365 R_dup, _, _ = R2.eval_for_poly(T) 

366 d = dup_discriminant(R_dup, ZZ) 

367 

368 if d == 0: 

369 continue 

370 if is_square(d): 

371 return (S5TransitiveSubgroups.C5, True) 

372 else: 

373 return (S5TransitiveSubgroups.D5, True) 

374 

375 raise MaxTriesException 

376 

377 

378def _galois_group_degree_5_lookup_ext_factor(T, max_tries=30, randomize=False): 

379 r""" 

380 Compute the Galois group of a polynomial of degree 5. 

381 

382 Explanation 

383 =========== 

384 

385 Based on Alg 6.3.9 of [1], but uses resolvent coeff lookup, plus 

386 factorization over an algebraic extension. 

387 

388 """ 

389 from sympy.combinatorics.galois import S5TransitiveSubgroups 

390 

391 _T = T 

392 

393 history = set() 

394 for i in range(max_tries): 

395 R_dup = get_resolvent_by_lookup(T, 1) 

396 if dup_sqf_p(R_dup, ZZ): 

397 break 

398 _, T = tschirnhausen_transformation(T, max_tries=max_tries, 

399 history=history, 

400 fixed_order=not randomize) 

401 else: 

402 raise MaxTriesException 

403 

404 sq_disc = has_square_disc(T) 

405 

406 if dup_irreducible_p(R_dup, ZZ): 

407 return ((S5TransitiveSubgroups.A5, True) if sq_disc 

408 else (S5TransitiveSubgroups.S5, False)) 

409 

410 if not sq_disc: 

411 return (S5TransitiveSubgroups.M20, False) 

412 

413 # If we get this far, Gal(T) can only be D5 or C5. 

414 # But for Gal(T) to have order 5, T must already split completely in 

415 # the extension field obtained by adjoining a single one of its roots. 

416 fl = Poly(_T, domain=ZZ.alg_field_from_poly(_T)).factor_list()[1] 

417 if len(fl) == 5: 

418 return (S5TransitiveSubgroups.C5, True) 

419 else: 

420 return (S5TransitiveSubgroups.D5, True) 

421 

422 

423def _galois_group_degree_6_lookup(T, max_tries=30, randomize=False): 

424 r""" 

425 Compute the Galois group of a polynomial of degree 6. 

426 

427 Explanation 

428 =========== 

429 

430 Based on Alg 6.3.10 of [1], but uses resolvent coeff lookup. 

431 

432 """ 

433 from sympy.combinatorics.galois import S6TransitiveSubgroups 

434 

435 # First resolvent: 

436 

437 history = set() 

438 for i in range(max_tries): 

439 R_dup = get_resolvent_by_lookup(T, 1) 

440 if dup_sqf_p(R_dup, ZZ): 

441 break 

442 _, T = tschirnhausen_transformation(T, max_tries=max_tries, 

443 history=history, 

444 fixed_order=not randomize) 

445 else: 

446 raise MaxTriesException 

447 

448 fl = dup_factor_list(R_dup, ZZ) 

449 

450 # Group the factors by degree. 

451 factors_by_deg = defaultdict(list) 

452 for r, _ in fl[1]: 

453 factors_by_deg[len(r) - 1].append(r) 

454 

455 L = sorted(sum([ 

456 [d] * len(ff) for d, ff in factors_by_deg.items() 

457 ], [])) 

458 

459 T_has_sq_disc = has_square_disc(T) 

460 

461 if L == [1, 2, 3]: 

462 f1 = factors_by_deg[3][0] 

463 return ((S6TransitiveSubgroups.C6, False) if has_square_disc(f1) 

464 else (S6TransitiveSubgroups.D6, False)) 

465 

466 elif L == [3, 3]: 

467 f1, f2 = factors_by_deg[3] 

468 any_square = has_square_disc(f1) or has_square_disc(f2) 

469 return ((S6TransitiveSubgroups.G18, False) if any_square 

470 else (S6TransitiveSubgroups.G36m, False)) 

471 

472 elif L == [2, 4]: 

473 if T_has_sq_disc: 

474 return (S6TransitiveSubgroups.S4p, True) 

475 else: 

476 f1 = factors_by_deg[4][0] 

477 return ((S6TransitiveSubgroups.A4xC2, False) if has_square_disc(f1) 

478 else (S6TransitiveSubgroups.S4xC2, False)) 

479 

480 elif L == [1, 1, 4]: 

481 return ((S6TransitiveSubgroups.A4, True) if T_has_sq_disc 

482 else (S6TransitiveSubgroups.S4m, False)) 

483 

484 elif L == [1, 5]: 

485 return ((S6TransitiveSubgroups.PSL2F5, True) if T_has_sq_disc 

486 else (S6TransitiveSubgroups.PGL2F5, False)) 

487 

488 elif L == [1, 1, 1, 3]: 

489 return (S6TransitiveSubgroups.S3, False) 

490 

491 assert L == [6] 

492 

493 # Second resolvent: 

494 

495 history = set() 

496 for i in range(max_tries): 

497 R_dup = get_resolvent_by_lookup(T, 2) 

498 if dup_sqf_p(R_dup, ZZ): 

499 break 

500 _, T = tschirnhausen_transformation(T, max_tries=max_tries, 

501 history=history, 

502 fixed_order=not randomize) 

503 else: 

504 raise MaxTriesException 

505 

506 T_has_sq_disc = has_square_disc(T) 

507 

508 if dup_irreducible_p(R_dup, ZZ): 

509 return ((S6TransitiveSubgroups.A6, True) if T_has_sq_disc 

510 else (S6TransitiveSubgroups.S6, False)) 

511 else: 

512 return ((S6TransitiveSubgroups.G36p, True) if T_has_sq_disc 

513 else (S6TransitiveSubgroups.G72, False)) 

514 

515 

516@public 

517def galois_group(f, *gens, by_name=False, max_tries=30, randomize=False, **args): 

518 r""" 

519 Compute the Galois group for polynomials *f* up to degree 6. 

520 

521 Examples 

522 ======== 

523 

524 >>> from sympy import galois_group 

525 >>> from sympy.abc import x 

526 >>> f = x**4 + 1 

527 >>> G, alt = galois_group(f) 

528 >>> print(G) 

529 PermutationGroup([ 

530 (0 1)(2 3), 

531 (0 2)(1 3)]) 

532 

533 The group is returned along with a boolean, indicating whether it is 

534 contained in the alternating group $A_n$, where $n$ is the degree of *T*. 

535 Along with other group properties, this can help determine which group it 

536 is: 

537 

538 >>> alt 

539 True 

540 >>> G.order() 

541 4 

542 

543 Alternatively, the group can be returned by name: 

544 

545 >>> G_name, _ = galois_group(f, by_name=True) 

546 >>> print(G_name) 

547 S4TransitiveSubgroups.V 

548 

549 The group itself can then be obtained by calling the name's 

550 ``get_perm_group()`` method: 

551 

552 >>> G_name.get_perm_group() 

553 PermutationGroup([ 

554 (0 1)(2 3), 

555 (0 2)(1 3)]) 

556 

557 Group names are values of the enum classes 

558 :py:class:`sympy.combinatorics.galois.S1TransitiveSubgroups`, 

559 :py:class:`sympy.combinatorics.galois.S2TransitiveSubgroups`, 

560 etc. 

561 

562 Parameters 

563 ========== 

564 

565 f : Expr 

566 Irreducible polynomial over :ref:`ZZ` or :ref:`QQ`, whose Galois group 

567 is to be determined. 

568 gens : optional list of symbols 

569 For converting *f* to Poly, and will be passed on to the 

570 :py:func:`~.poly_from_expr` function. 

571 by_name : bool, default False 

572 If ``True``, the Galois group will be returned by name. 

573 Otherwise it will be returned as a :py:class:`~.PermutationGroup`. 

574 max_tries : int, default 30 

575 Make at most this many attempts in those steps that involve 

576 generating Tschirnhausen transformations. 

577 randomize : bool, default False 

578 If ``True``, then use random coefficients when generating Tschirnhausen 

579 transformations. Otherwise try transformations in a fixed order. Both 

580 approaches start with small coefficients and degrees and work upward. 

581 args : optional 

582 For converting *f* to Poly, and will be passed on to the 

583 :py:func:`~.poly_from_expr` function. 

584 

585 Returns 

586 ======= 

587 

588 Pair ``(G, alt)`` 

589 The first element ``G`` indicates the Galois group. It is an instance 

590 of one of the :py:class:`sympy.combinatorics.galois.S1TransitiveSubgroups` 

591 :py:class:`sympy.combinatorics.galois.S2TransitiveSubgroups`, etc. enum 

592 classes if *by_name* was ``True``, and a :py:class:`~.PermutationGroup` 

593 if ``False``. 

594 

595 The second element is a boolean, saying whether the group is contained 

596 in the alternating group $A_n$ ($n$ the degree of *T*). 

597 

598 Raises 

599 ====== 

600 

601 ValueError 

602 if *f* is of an unsupported degree. 

603 

604 MaxTriesException 

605 if could not complete before exceeding *max_tries* in those steps 

606 that involve generating Tschirnhausen transformations. 

607 

608 See Also 

609 ======== 

610 

611 .Poly.galois_group 

612 

613 """ 

614 gens = gens or [] 

615 args = args or {} 

616 

617 try: 

618 F, opt = poly_from_expr(f, *gens, **args) 

619 except PolificationFailed as exc: 

620 raise ComputationFailed('galois_group', 1, exc) 

621 

622 return F.galois_group(by_name=by_name, max_tries=max_tries, 

623 randomize=randomize)