Coverage for /usr/lib/python3/dist-packages/sympy/combinatorics/pc_groups.py: 11%

220 statements  

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

1from sympy.ntheory.primetest import isprime 

2from sympy.combinatorics.perm_groups import PermutationGroup 

3from sympy.printing.defaults import DefaultPrinting 

4from sympy.combinatorics.free_groups import free_group 

5 

6 

7class PolycyclicGroup(DefaultPrinting): 

8 

9 is_group = True 

10 is_solvable = True 

11 

12 def __init__(self, pc_sequence, pc_series, relative_order, collector=None): 

13 """ 

14 

15 Parameters 

16 ========== 

17 

18 pc_sequence : list 

19 A sequence of elements whose classes generate the cyclic factor 

20 groups of pc_series. 

21 pc_series : list 

22 A subnormal sequence of subgroups where each factor group is cyclic. 

23 relative_order : list 

24 The orders of factor groups of pc_series. 

25 collector : Collector 

26 By default, it is None. Collector class provides the 

27 polycyclic presentation with various other functionalities. 

28 

29 """ 

30 self.pcgs = pc_sequence 

31 self.pc_series = pc_series 

32 self.relative_order = relative_order 

33 self.collector = Collector(self.pcgs, pc_series, relative_order) if not collector else collector 

34 

35 def is_prime_order(self): 

36 return all(isprime(order) for order in self.relative_order) 

37 

38 def length(self): 

39 return len(self.pcgs) 

40 

41 

42class Collector(DefaultPrinting): 

43 

44 """ 

45 References 

46 ========== 

47 

48 .. [1] Holt, D., Eick, B., O'Brien, E. 

49 "Handbook of Computational Group Theory" 

50 Section 8.1.3 

51 """ 

52 

53 def __init__(self, pcgs, pc_series, relative_order, free_group_=None, pc_presentation=None): 

54 """ 

55 

56 Most of the parameters for the Collector class are the same as for PolycyclicGroup. 

57 Others are described below. 

58 

59 Parameters 

60 ========== 

61 

62 free_group_ : tuple 

63 free_group_ provides the mapping of polycyclic generating 

64 sequence with the free group elements. 

65 pc_presentation : dict 

66 Provides the presentation of polycyclic groups with the 

67 help of power and conjugate relators. 

68 

69 See Also 

70 ======== 

71 

72 PolycyclicGroup 

73 

74 """ 

75 self.pcgs = pcgs 

76 self.pc_series = pc_series 

77 self.relative_order = relative_order 

78 self.free_group = free_group('x:{}'.format(len(pcgs)))[0] if not free_group_ else free_group_ 

79 self.index = {s: i for i, s in enumerate(self.free_group.symbols)} 

80 self.pc_presentation = self.pc_relators() 

81 

82 def minimal_uncollected_subword(self, word): 

83 r""" 

84 Returns the minimal uncollected subwords. 

85 

86 Explanation 

87 =========== 

88 

89 A word ``v`` defined on generators in ``X`` is a minimal 

90 uncollected subword of the word ``w`` if ``v`` is a subword 

91 of ``w`` and it has one of the following form 

92 

93 * `v = {x_{i+1}}^{a_j}x_i` 

94 

95 * `v = {x_{i+1}}^{a_j}{x_i}^{-1}` 

96 

97 * `v = {x_i}^{a_j}` 

98 

99 for `a_j` not in `\{1, \ldots, s-1\}`. Where, ``s`` is the power 

100 exponent of the corresponding generator. 

101 

102 Examples 

103 ======== 

104 

105 >>> from sympy.combinatorics.named_groups import SymmetricGroup 

106 >>> from sympy.combinatorics import free_group 

107 >>> G = SymmetricGroup(4) 

108 >>> PcGroup = G.polycyclic_group() 

109 >>> collector = PcGroup.collector 

110 >>> F, x1, x2 = free_group("x1, x2") 

111 >>> word = x2**2*x1**7 

112 >>> collector.minimal_uncollected_subword(word) 

113 ((x2, 2),) 

114 

115 """ 

116 # To handle the case word = <identity> 

117 if not word: 

118 return None 

119 

120 array = word.array_form 

121 re = self.relative_order 

122 index = self.index 

123 

124 for i in range(len(array)): 

125 s1, e1 = array[i] 

126 

127 if re[index[s1]] and (e1 < 0 or e1 > re[index[s1]]-1): 

128 return ((s1, e1), ) 

129 

130 for i in range(len(array)-1): 

131 s1, e1 = array[i] 

132 s2, e2 = array[i+1] 

133 

134 if index[s1] > index[s2]: 

135 e = 1 if e2 > 0 else -1 

136 return ((s1, e1), (s2, e)) 

137 

138 return None 

139 

140 def relations(self): 

141 """ 

142 Separates the given relators of pc presentation in power and 

143 conjugate relations. 

144 

145 Returns 

146 ======= 

147 

148 (power_rel, conj_rel) 

149 Separates pc presentation into power and conjugate relations. 

150 

151 Examples 

152 ======== 

153 

154 >>> from sympy.combinatorics.named_groups import SymmetricGroup 

155 >>> G = SymmetricGroup(3) 

156 >>> PcGroup = G.polycyclic_group() 

157 >>> collector = PcGroup.collector 

158 >>> power_rel, conj_rel = collector.relations() 

159 >>> power_rel 

160 {x0**2: (), x1**3: ()} 

161 >>> conj_rel 

162 {x0**-1*x1*x0: x1**2} 

163 

164 See Also 

165 ======== 

166 

167 pc_relators 

168 

169 """ 

170 power_relators = {} 

171 conjugate_relators = {} 

172 for key, value in self.pc_presentation.items(): 

173 if len(key.array_form) == 1: 

174 power_relators[key] = value 

175 else: 

176 conjugate_relators[key] = value 

177 return power_relators, conjugate_relators 

178 

179 def subword_index(self, word, w): 

180 """ 

181 Returns the start and ending index of a given 

182 subword in a word. 

183 

184 Parameters 

185 ========== 

186 

187 word : FreeGroupElement 

188 word defined on free group elements for a 

189 polycyclic group. 

190 w : FreeGroupElement 

191 subword of a given word, whose starting and 

192 ending index to be computed. 

193 

194 Returns 

195 ======= 

196 

197 (i, j) 

198 A tuple containing starting and ending index of ``w`` 

199 in the given word. 

200 

201 Examples 

202 ======== 

203 

204 >>> from sympy.combinatorics.named_groups import SymmetricGroup 

205 >>> from sympy.combinatorics import free_group 

206 >>> G = SymmetricGroup(4) 

207 >>> PcGroup = G.polycyclic_group() 

208 >>> collector = PcGroup.collector 

209 >>> F, x1, x2 = free_group("x1, x2") 

210 >>> word = x2**2*x1**7 

211 >>> w = x2**2*x1 

212 >>> collector.subword_index(word, w) 

213 (0, 3) 

214 >>> w = x1**7 

215 >>> collector.subword_index(word, w) 

216 (2, 9) 

217 

218 """ 

219 low = -1 

220 high = -1 

221 for i in range(len(word)-len(w)+1): 

222 if word.subword(i, i+len(w)) == w: 

223 low = i 

224 high = i+len(w) 

225 break 

226 if low == high == -1: 

227 return -1, -1 

228 return low, high 

229 

230 def map_relation(self, w): 

231 """ 

232 Return a conjugate relation. 

233 

234 Explanation 

235 =========== 

236 

237 Given a word formed by two free group elements, the 

238 corresponding conjugate relation with those free 

239 group elements is formed and mapped with the collected 

240 word in the polycyclic presentation. 

241 

242 Examples 

243 ======== 

244 

245 >>> from sympy.combinatorics.named_groups import SymmetricGroup 

246 >>> from sympy.combinatorics import free_group 

247 >>> G = SymmetricGroup(3) 

248 >>> PcGroup = G.polycyclic_group() 

249 >>> collector = PcGroup.collector 

250 >>> F, x0, x1 = free_group("x0, x1") 

251 >>> w = x1*x0 

252 >>> collector.map_relation(w) 

253 x1**2 

254 

255 See Also 

256 ======== 

257 

258 pc_presentation 

259 

260 """ 

261 array = w.array_form 

262 s1 = array[0][0] 

263 s2 = array[1][0] 

264 key = ((s2, -1), (s1, 1), (s2, 1)) 

265 key = self.free_group.dtype(key) 

266 return self.pc_presentation[key] 

267 

268 

269 def collected_word(self, word): 

270 r""" 

271 Return the collected form of a word. 

272 

273 Explanation 

274 =========== 

275 

276 A word ``w`` is called collected, if `w = {x_{i_1}}^{a_1} * \ldots * 

277 {x_{i_r}}^{a_r}` with `i_1 < i_2< \ldots < i_r` and `a_j` is in 

278 `\{1, \ldots, {s_j}-1\}`. 

279 

280 Otherwise w is uncollected. 

281 

282 Parameters 

283 ========== 

284 

285 word : FreeGroupElement 

286 An uncollected word. 

287 

288 Returns 

289 ======= 

290 

291 word 

292 A collected word of form `w = {x_{i_1}}^{a_1}, \ldots, 

293 {x_{i_r}}^{a_r}` with `i_1, i_2, \ldots, i_r` and `a_j \in 

294 \{1, \ldots, {s_j}-1\}`. 

295 

296 Examples 

297 ======== 

298 

299 >>> from sympy.combinatorics.named_groups import SymmetricGroup 

300 >>> from sympy.combinatorics.perm_groups import PermutationGroup 

301 >>> from sympy.combinatorics import free_group 

302 >>> G = SymmetricGroup(4) 

303 >>> PcGroup = G.polycyclic_group() 

304 >>> collector = PcGroup.collector 

305 >>> F, x0, x1, x2, x3 = free_group("x0, x1, x2, x3") 

306 >>> word = x3*x2*x1*x0 

307 >>> collected_word = collector.collected_word(word) 

308 >>> free_to_perm = {} 

309 >>> free_group = collector.free_group 

310 >>> for sym, gen in zip(free_group.symbols, collector.pcgs): 

311 ... free_to_perm[sym] = gen 

312 >>> G1 = PermutationGroup() 

313 >>> for w in word: 

314 ... sym = w[0] 

315 ... perm = free_to_perm[sym] 

316 ... G1 = PermutationGroup([perm] + G1.generators) 

317 >>> G2 = PermutationGroup() 

318 >>> for w in collected_word: 

319 ... sym = w[0] 

320 ... perm = free_to_perm[sym] 

321 ... G2 = PermutationGroup([perm] + G2.generators) 

322 

323 The two are not identical, but they are equivalent: 

324 

325 >>> G1.equals(G2), G1 == G2 

326 (True, False) 

327 

328 See Also 

329 ======== 

330 

331 minimal_uncollected_subword 

332 

333 """ 

334 free_group = self.free_group 

335 while True: 

336 w = self.minimal_uncollected_subword(word) 

337 if not w: 

338 break 

339 

340 low, high = self.subword_index(word, free_group.dtype(w)) 

341 if low == -1: 

342 continue 

343 

344 s1, e1 = w[0] 

345 if len(w) == 1: 

346 re = self.relative_order[self.index[s1]] 

347 q = e1 // re 

348 r = e1-q*re 

349 

350 key = ((w[0][0], re), ) 

351 key = free_group.dtype(key) 

352 if self.pc_presentation[key]: 

353 presentation = self.pc_presentation[key].array_form 

354 sym, exp = presentation[0] 

355 word_ = ((w[0][0], r), (sym, q*exp)) 

356 word_ = free_group.dtype(word_) 

357 else: 

358 if r != 0: 

359 word_ = ((w[0][0], r), ) 

360 word_ = free_group.dtype(word_) 

361 else: 

362 word_ = None 

363 word = word.eliminate_word(free_group.dtype(w), word_) 

364 

365 if len(w) == 2 and w[1][1] > 0: 

366 s2, e2 = w[1] 

367 s2 = ((s2, 1), ) 

368 s2 = free_group.dtype(s2) 

369 word_ = self.map_relation(free_group.dtype(w)) 

370 word_ = s2*word_**e1 

371 word_ = free_group.dtype(word_) 

372 word = word.substituted_word(low, high, word_) 

373 

374 elif len(w) == 2 and w[1][1] < 0: 

375 s2, e2 = w[1] 

376 s2 = ((s2, 1), ) 

377 s2 = free_group.dtype(s2) 

378 word_ = self.map_relation(free_group.dtype(w)) 

379 word_ = s2**-1*word_**e1 

380 word_ = free_group.dtype(word_) 

381 word = word.substituted_word(low, high, word_) 

382 

383 return word 

384 

385 

386 def pc_relators(self): 

387 r""" 

388 Return the polycyclic presentation. 

389 

390 Explanation 

391 =========== 

392 

393 There are two types of relations used in polycyclic 

394 presentation. 

395 

396 * Power relations : Power relators are of the form `x_i^{re_i}`, 

397 where `i \in \{0, \ldots, \mathrm{len(pcgs)}\}`, ``x`` represents polycyclic 

398 generator and ``re`` is the corresponding relative order. 

399 

400 * Conjugate relations : Conjugate relators are of the form `x_j^-1x_ix_j`, 

401 where `j < i \in \{0, \ldots, \mathrm{len(pcgs)}\}`. 

402 

403 Returns 

404 ======= 

405 

406 A dictionary with power and conjugate relations as key and 

407 their collected form as corresponding values. 

408 

409 Notes 

410 ===== 

411 

412 Identity Permutation is mapped with empty ``()``. 

413 

414 Examples 

415 ======== 

416 

417 >>> from sympy.combinatorics.named_groups import SymmetricGroup 

418 >>> from sympy.combinatorics.permutations import Permutation 

419 >>> S = SymmetricGroup(49).sylow_subgroup(7) 

420 >>> der = S.derived_series() 

421 >>> G = der[len(der)-2] 

422 >>> PcGroup = G.polycyclic_group() 

423 >>> collector = PcGroup.collector 

424 >>> pcgs = PcGroup.pcgs 

425 >>> len(pcgs) 

426 6 

427 >>> free_group = collector.free_group 

428 >>> pc_resentation = collector.pc_presentation 

429 >>> free_to_perm = {} 

430 >>> for s, g in zip(free_group.symbols, pcgs): 

431 ... free_to_perm[s] = g 

432 

433 >>> for k, v in pc_resentation.items(): 

434 ... k_array = k.array_form 

435 ... if v != (): 

436 ... v_array = v.array_form 

437 ... lhs = Permutation() 

438 ... for gen in k_array: 

439 ... s = gen[0] 

440 ... e = gen[1] 

441 ... lhs = lhs*free_to_perm[s]**e 

442 ... if v == (): 

443 ... assert lhs.is_identity 

444 ... continue 

445 ... rhs = Permutation() 

446 ... for gen in v_array: 

447 ... s = gen[0] 

448 ... e = gen[1] 

449 ... rhs = rhs*free_to_perm[s]**e 

450 ... assert lhs == rhs 

451 

452 """ 

453 free_group = self.free_group 

454 rel_order = self.relative_order 

455 pc_relators = {} 

456 perm_to_free = {} 

457 pcgs = self.pcgs 

458 

459 for gen, s in zip(pcgs, free_group.generators): 

460 perm_to_free[gen**-1] = s**-1 

461 perm_to_free[gen] = s 

462 

463 pcgs = pcgs[::-1] 

464 series = self.pc_series[::-1] 

465 rel_order = rel_order[::-1] 

466 collected_gens = [] 

467 

468 for i, gen in enumerate(pcgs): 

469 re = rel_order[i] 

470 relation = perm_to_free[gen]**re 

471 G = series[i] 

472 

473 l = G.generator_product(gen**re, original = True) 

474 l.reverse() 

475 

476 word = free_group.identity 

477 for g in l: 

478 word = word*perm_to_free[g] 

479 

480 word = self.collected_word(word) 

481 pc_relators[relation] = word if word else () 

482 self.pc_presentation = pc_relators 

483 

484 collected_gens.append(gen) 

485 if len(collected_gens) > 1: 

486 conj = collected_gens[len(collected_gens)-1] 

487 conjugator = perm_to_free[conj] 

488 

489 for j in range(len(collected_gens)-1): 

490 conjugated = perm_to_free[collected_gens[j]] 

491 

492 relation = conjugator**-1*conjugated*conjugator 

493 gens = conj**-1*collected_gens[j]*conj 

494 

495 l = G.generator_product(gens, original = True) 

496 l.reverse() 

497 word = free_group.identity 

498 for g in l: 

499 word = word*perm_to_free[g] 

500 

501 word = self.collected_word(word) 

502 pc_relators[relation] = word if word else () 

503 self.pc_presentation = pc_relators 

504 

505 return pc_relators 

506 

507 def exponent_vector(self, element): 

508 r""" 

509 Return the exponent vector of length equal to the 

510 length of polycyclic generating sequence. 

511 

512 Explanation 

513 =========== 

514 

515 For a given generator/element ``g`` of the polycyclic group, 

516 it can be represented as `g = {x_1}^{e_1}, \ldots, {x_n}^{e_n}`, 

517 where `x_i` represents polycyclic generators and ``n`` is 

518 the number of generators in the free_group equal to the length 

519 of pcgs. 

520 

521 Parameters 

522 ========== 

523 

524 element : Permutation 

525 Generator of a polycyclic group. 

526 

527 Examples 

528 ======== 

529 

530 >>> from sympy.combinatorics.named_groups import SymmetricGroup 

531 >>> from sympy.combinatorics.permutations import Permutation 

532 >>> G = SymmetricGroup(4) 

533 >>> PcGroup = G.polycyclic_group() 

534 >>> collector = PcGroup.collector 

535 >>> pcgs = PcGroup.pcgs 

536 >>> collector.exponent_vector(G[0]) 

537 [1, 0, 0, 0] 

538 >>> exp = collector.exponent_vector(G[1]) 

539 >>> g = Permutation() 

540 >>> for i in range(len(exp)): 

541 ... g = g*pcgs[i]**exp[i] if exp[i] else g 

542 >>> assert g == G[1] 

543 

544 References 

545 ========== 

546 

547 .. [1] Holt, D., Eick, B., O'Brien, E. 

548 "Handbook of Computational Group Theory" 

549 Section 8.1.1, Definition 8.4 

550 

551 """ 

552 free_group = self.free_group 

553 G = PermutationGroup() 

554 for g in self.pcgs: 

555 G = PermutationGroup([g] + G.generators) 

556 gens = G.generator_product(element, original = True) 

557 gens.reverse() 

558 

559 perm_to_free = {} 

560 for sym, g in zip(free_group.generators, self.pcgs): 

561 perm_to_free[g**-1] = sym**-1 

562 perm_to_free[g] = sym 

563 w = free_group.identity 

564 for g in gens: 

565 w = w*perm_to_free[g] 

566 

567 word = self.collected_word(w) 

568 

569 index = self.index 

570 exp_vector = [0]*len(free_group) 

571 word = word.array_form 

572 for t in word: 

573 exp_vector[index[t[0]]] = t[1] 

574 return exp_vector 

575 

576 def depth(self, element): 

577 r""" 

578 Return the depth of a given element. 

579 

580 Explanation 

581 =========== 

582 

583 The depth of a given element ``g`` is defined by 

584 `\mathrm{dep}[g] = i` if `e_1 = e_2 = \ldots = e_{i-1} = 0` 

585 and `e_i != 0`, where ``e`` represents the exponent-vector. 

586 

587 Examples 

588 ======== 

589 

590 >>> from sympy.combinatorics.named_groups import SymmetricGroup 

591 >>> G = SymmetricGroup(3) 

592 >>> PcGroup = G.polycyclic_group() 

593 >>> collector = PcGroup.collector 

594 >>> collector.depth(G[0]) 

595 2 

596 >>> collector.depth(G[1]) 

597 1 

598 

599 References 

600 ========== 

601 

602 .. [1] Holt, D., Eick, B., O'Brien, E. 

603 "Handbook of Computational Group Theory" 

604 Section 8.1.1, Definition 8.5 

605 

606 """ 

607 exp_vector = self.exponent_vector(element) 

608 return next((i+1 for i, x in enumerate(exp_vector) if x), len(self.pcgs)+1) 

609 

610 def leading_exponent(self, element): 

611 r""" 

612 Return the leading non-zero exponent. 

613 

614 Explanation 

615 =========== 

616 

617 The leading exponent for a given element `g` is defined 

618 by `\mathrm{leading\_exponent}[g]` `= e_i`, if `\mathrm{depth}[g] = i`. 

619 

620 Examples 

621 ======== 

622 

623 >>> from sympy.combinatorics.named_groups import SymmetricGroup 

624 >>> G = SymmetricGroup(3) 

625 >>> PcGroup = G.polycyclic_group() 

626 >>> collector = PcGroup.collector 

627 >>> collector.leading_exponent(G[1]) 

628 1 

629 

630 """ 

631 exp_vector = self.exponent_vector(element) 

632 depth = self.depth(element) 

633 if depth != len(self.pcgs)+1: 

634 return exp_vector[depth-1] 

635 return None 

636 

637 def _sift(self, z, g): 

638 h = g 

639 d = self.depth(h) 

640 while d < len(self.pcgs) and z[d-1] != 1: 

641 k = z[d-1] 

642 e = self.leading_exponent(h)*(self.leading_exponent(k))**-1 

643 e = e % self.relative_order[d-1] 

644 h = k**-e*h 

645 d = self.depth(h) 

646 return h 

647 

648 def induced_pcgs(self, gens): 

649 """ 

650 

651 Parameters 

652 ========== 

653 

654 gens : list 

655 A list of generators on which polycyclic subgroup 

656 is to be defined. 

657 

658 Examples 

659 ======== 

660 

661 >>> from sympy.combinatorics.named_groups import SymmetricGroup 

662 >>> S = SymmetricGroup(8) 

663 >>> G = S.sylow_subgroup(2) 

664 >>> PcGroup = G.polycyclic_group() 

665 >>> collector = PcGroup.collector 

666 >>> gens = [G[0], G[1]] 

667 >>> ipcgs = collector.induced_pcgs(gens) 

668 >>> [gen.order() for gen in ipcgs] 

669 [2, 2, 2] 

670 >>> G = S.sylow_subgroup(3) 

671 >>> PcGroup = G.polycyclic_group() 

672 >>> collector = PcGroup.collector 

673 >>> gens = [G[0], G[1]] 

674 >>> ipcgs = collector.induced_pcgs(gens) 

675 >>> [gen.order() for gen in ipcgs] 

676 [3] 

677 

678 """ 

679 z = [1]*len(self.pcgs) 

680 G = gens 

681 while G: 

682 g = G.pop(0) 

683 h = self._sift(z, g) 

684 d = self.depth(h) 

685 if d < len(self.pcgs): 

686 for gen in z: 

687 if gen != 1: 

688 G.append(h**-1*gen**-1*h*gen) 

689 z[d-1] = h; 

690 z = [gen for gen in z if gen != 1] 

691 return z 

692 

693 def constructive_membership_test(self, ipcgs, g): 

694 """ 

695 Return the exponent vector for induced pcgs. 

696 """ 

697 e = [0]*len(ipcgs) 

698 h = g 

699 d = self.depth(h) 

700 for i, gen in enumerate(ipcgs): 

701 while self.depth(gen) == d: 

702 f = self.leading_exponent(h)*self.leading_exponent(gen) 

703 f = f % self.relative_order[d-1] 

704 h = gen**(-f)*h 

705 e[i] = f 

706 d = self.depth(h) 

707 if h == 1: 

708 return e 

709 return False