123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472 |
- from math import factorial as _factorial, log, prod
- from itertools import chain, islice, product
- from sympy.combinatorics import Permutation
- from sympy.combinatorics.permutations import (_af_commutes_with, _af_invert,
- _af_rmul, _af_rmuln, _af_pow, Cycle)
- from sympy.combinatorics.util import (_check_cycles_alt_sym,
- _distribute_gens_by_base, _orbits_transversals_from_bsgs,
- _handle_precomputed_bsgs, _base_ordering, _strong_gens_from_distr,
- _strip, _strip_af)
- from sympy.core import Basic
- from sympy.core.random import _randrange, randrange, choice
- from sympy.core.symbol import Symbol
- from sympy.core.sympify import _sympify
- from sympy.functions.combinatorial.factorials import factorial
- from sympy.ntheory import primefactors, sieve
- from sympy.ntheory.factor_ import (factorint, multiplicity)
- from sympy.ntheory.primetest import isprime
- from sympy.utilities.iterables import has_variety, is_sequence, uniq
- rmul = Permutation.rmul_with_af
- _af_new = Permutation._af_new
- class PermutationGroup(Basic):
- r"""The class defining a Permutation group.
- Explanation
- ===========
- ``PermutationGroup([p1, p2, ..., pn])`` returns the permutation group
- generated by the list of permutations. This group can be supplied
- to Polyhedron if one desires to decorate the elements to which the
- indices of the permutation refer.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> from sympy.combinatorics import Polyhedron
- The permutations corresponding to motion of the front, right and
- bottom face of a $2 \times 2$ Rubik's cube are defined:
- >>> F = Permutation(2, 19, 21, 8)(3, 17, 20, 10)(4, 6, 7, 5)
- >>> R = Permutation(1, 5, 21, 14)(3, 7, 23, 12)(8, 10, 11, 9)
- >>> D = Permutation(6, 18, 14, 10)(7, 19, 15, 11)(20, 22, 23, 21)
- These are passed as permutations to PermutationGroup:
- >>> G = PermutationGroup(F, R, D)
- >>> G.order()
- 3674160
- The group can be supplied to a Polyhedron in order to track the
- objects being moved. An example involving the $2 \times 2$ Rubik's cube is
- given there, but here is a simple demonstration:
- >>> a = Permutation(2, 1)
- >>> b = Permutation(1, 0)
- >>> G = PermutationGroup(a, b)
- >>> P = Polyhedron(list('ABC'), pgroup=G)
- >>> P.corners
- (A, B, C)
- >>> P.rotate(0) # apply permutation 0
- >>> P.corners
- (A, C, B)
- >>> P.reset()
- >>> P.corners
- (A, B, C)
- Or one can make a permutation as a product of selected permutations
- and apply them to an iterable directly:
- >>> P10 = G.make_perm([0, 1])
- >>> P10('ABC')
- ['C', 'A', 'B']
- See Also
- ========
- sympy.combinatorics.polyhedron.Polyhedron,
- sympy.combinatorics.permutations.Permutation
- References
- ==========
- .. [1] Holt, D., Eick, B., O'Brien, E.
- "Handbook of Computational Group Theory"
- .. [2] Seress, A.
- "Permutation Group Algorithms"
- .. [3] https://en.wikipedia.org/wiki/Schreier_vector
- .. [4] https://en.wikipedia.org/wiki/Nielsen_transformation#Product_replacement_algorithm
- .. [5] Frank Celler, Charles R.Leedham-Green, Scott H.Murray,
- Alice C.Niemeyer, and E.A.O'Brien. "Generating Random
- Elements of a Finite Group"
- .. [6] https://en.wikipedia.org/wiki/Block_%28permutation_group_theory%29
- .. [7] https://algorithmist.com/wiki/Union_find
- .. [8] https://en.wikipedia.org/wiki/Multiply_transitive_group#Multiply_transitive_groups
- .. [9] https://en.wikipedia.org/wiki/Center_%28group_theory%29
- .. [10] https://en.wikipedia.org/wiki/Centralizer_and_normalizer
- .. [11] https://groupprops.subwiki.org/wiki/Derived_subgroup
- .. [12] https://en.wikipedia.org/wiki/Nilpotent_group
- .. [13] https://www.math.colostate.edu/~hulpke/CGT/cgtnotes.pdf
- .. [14] https://docs.gap-system.org/doc/ref/manual.pdf
- """
- is_group = True
- def __new__(cls, *args, dups=True, **kwargs):
- """The default constructor. Accepts Cycle and Permutation forms.
- Removes duplicates unless ``dups`` keyword is ``False``.
- """
- if not args:
- args = [Permutation()]
- else:
- args = list(args[0] if is_sequence(args[0]) else args)
- if not args:
- args = [Permutation()]
- if any(isinstance(a, Cycle) for a in args):
- args = [Permutation(a) for a in args]
- if has_variety(a.size for a in args):
- degree = kwargs.pop('degree', None)
- if degree is None:
- degree = max(a.size for a in args)
- for i in range(len(args)):
- if args[i].size != degree:
- args[i] = Permutation(args[i], size=degree)
- if dups:
- args = list(uniq([_af_new(list(a)) for a in args]))
- if len(args) > 1:
- args = [g for g in args if not g.is_identity]
- return Basic.__new__(cls, *args, **kwargs)
- def __init__(self, *args, **kwargs):
- self._generators = list(self.args)
- self._order = None
- self._center = []
- self._is_abelian = None
- self._is_transitive = None
- self._is_sym = None
- self._is_alt = None
- self._is_primitive = None
- self._is_nilpotent = None
- self._is_solvable = None
- self._is_trivial = None
- self._transitivity_degree = None
- self._max_div = None
- self._is_perfect = None
- self._is_cyclic = None
- self._is_dihedral = None
- self._r = len(self._generators)
- self._degree = self._generators[0].size
- # these attributes are assigned after running schreier_sims
- self._base = []
- self._strong_gens = []
- self._strong_gens_slp = []
- self._basic_orbits = []
- self._transversals = []
- self._transversal_slp = []
- # these attributes are assigned after running _random_pr_init
- self._random_gens = []
- # finite presentation of the group as an instance of `FpGroup`
- self._fp_presentation = None
- def __getitem__(self, i):
- return self._generators[i]
- def __contains__(self, i):
- """Return ``True`` if *i* is contained in PermutationGroup.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> p = Permutation(1, 2, 3)
- >>> Permutation(3) in PermutationGroup(p)
- True
- """
- if not isinstance(i, Permutation):
- raise TypeError("A PermutationGroup contains only Permutations as "
- "elements, not elements of type %s" % type(i))
- return self.contains(i)
- def __len__(self):
- return len(self._generators)
- def equals(self, other):
- """Return ``True`` if PermutationGroup generated by elements in the
- group are same i.e they represent the same PermutationGroup.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> p = Permutation(0, 1, 2, 3, 4, 5)
- >>> G = PermutationGroup([p, p**2])
- >>> H = PermutationGroup([p**2, p])
- >>> G.generators == H.generators
- False
- >>> G.equals(H)
- True
- """
- if not isinstance(other, PermutationGroup):
- return False
- set_self_gens = set(self.generators)
- set_other_gens = set(other.generators)
- # before reaching the general case there are also certain
- # optimisation and obvious cases requiring less or no actual
- # computation.
- if set_self_gens == set_other_gens:
- return True
- # in the most general case it will check that each generator of
- # one group belongs to the other PermutationGroup and vice-versa
- for gen1 in set_self_gens:
- if not other.contains(gen1):
- return False
- for gen2 in set_other_gens:
- if not self.contains(gen2):
- return False
- return True
- def __mul__(self, other):
- """
- Return the direct product of two permutation groups as a permutation
- group.
- Explanation
- ===========
- This implementation realizes the direct product by shifting the index
- set for the generators of the second group: so if we have ``G`` acting
- on ``n1`` points and ``H`` acting on ``n2`` points, ``G*H`` acts on
- ``n1 + n2`` points.
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import CyclicGroup
- >>> G = CyclicGroup(5)
- >>> H = G*G
- >>> H
- PermutationGroup([
- (9)(0 1 2 3 4),
- (5 6 7 8 9)])
- >>> H.order()
- 25
- """
- if isinstance(other, Permutation):
- return Coset(other, self, dir='+')
- gens1 = [perm._array_form for perm in self.generators]
- gens2 = [perm._array_form for perm in other.generators]
- n1 = self._degree
- n2 = other._degree
- start = list(range(n1))
- end = list(range(n1, n1 + n2))
- for i in range(len(gens2)):
- gens2[i] = [x + n1 for x in gens2[i]]
- gens2 = [start + gen for gen in gens2]
- gens1 = [gen + end for gen in gens1]
- together = gens1 + gens2
- gens = [_af_new(x) for x in together]
- return PermutationGroup(gens)
- def _random_pr_init(self, r, n, _random_prec_n=None):
- r"""Initialize random generators for the product replacement algorithm.
- Explanation
- ===========
- The implementation uses a modification of the original product
- replacement algorithm due to Leedham-Green, as described in [1],
- pp. 69-71; also, see [2], pp. 27-29 for a detailed theoretical
- analysis of the original product replacement algorithm, and [4].
- The product replacement algorithm is used for producing random,
- uniformly distributed elements of a group `G` with a set of generators
- `S`. For the initialization ``_random_pr_init``, a list ``R`` of
- `\max\{r, |S|\}` group generators is created as the attribute
- ``G._random_gens``, repeating elements of `S` if necessary, and the
- identity element of `G` is appended to ``R`` - we shall refer to this
- last element as the accumulator. Then the function ``random_pr()``
- is called ``n`` times, randomizing the list ``R`` while preserving
- the generation of `G` by ``R``. The function ``random_pr()`` itself
- takes two random elements ``g, h`` among all elements of ``R`` but
- the accumulator and replaces ``g`` with a randomly chosen element
- from `\{gh, g(~h), hg, (~h)g\}`. Then the accumulator is multiplied
- by whatever ``g`` was replaced by. The new value of the accumulator is
- then returned by ``random_pr()``.
- The elements returned will eventually (for ``n`` large enough) become
- uniformly distributed across `G` ([5]). For practical purposes however,
- the values ``n = 50, r = 11`` are suggested in [1].
- Notes
- =====
- THIS FUNCTION HAS SIDE EFFECTS: it changes the attribute
- self._random_gens
- See Also
- ========
- random_pr
- """
- deg = self.degree
- random_gens = [x._array_form for x in self.generators]
- k = len(random_gens)
- if k < r:
- for i in range(k, r):
- random_gens.append(random_gens[i - k])
- acc = list(range(deg))
- random_gens.append(acc)
- self._random_gens = random_gens
- # handle randomized input for testing purposes
- if _random_prec_n is None:
- for i in range(n):
- self.random_pr()
- else:
- for i in range(n):
- self.random_pr(_random_prec=_random_prec_n[i])
- def _union_find_merge(self, first, second, ranks, parents, not_rep):
- """Merges two classes in a union-find data structure.
- Explanation
- ===========
- Used in the implementation of Atkinson's algorithm as suggested in [1],
- pp. 83-87. The class merging process uses union by rank as an
- optimization. ([7])
- Notes
- =====
- THIS FUNCTION HAS SIDE EFFECTS: the list of class representatives,
- ``parents``, the list of class sizes, ``ranks``, and the list of
- elements that are not representatives, ``not_rep``, are changed due to
- class merging.
- See Also
- ========
- minimal_block, _union_find_rep
- References
- ==========
- .. [1] Holt, D., Eick, B., O'Brien, E.
- "Handbook of computational group theory"
- .. [7] https://algorithmist.com/wiki/Union_find
- """
- rep_first = self._union_find_rep(first, parents)
- rep_second = self._union_find_rep(second, parents)
- if rep_first != rep_second:
- # union by rank
- if ranks[rep_first] >= ranks[rep_second]:
- new_1, new_2 = rep_first, rep_second
- else:
- new_1, new_2 = rep_second, rep_first
- total_rank = ranks[new_1] + ranks[new_2]
- if total_rank > self.max_div:
- return -1
- parents[new_2] = new_1
- ranks[new_1] = total_rank
- not_rep.append(new_2)
- return 1
- return 0
- def _union_find_rep(self, num, parents):
- """Find representative of a class in a union-find data structure.
- Explanation
- ===========
- Used in the implementation of Atkinson's algorithm as suggested in [1],
- pp. 83-87. After the representative of the class to which ``num``
- belongs is found, path compression is performed as an optimization
- ([7]).
- Notes
- =====
- THIS FUNCTION HAS SIDE EFFECTS: the list of class representatives,
- ``parents``, is altered due to path compression.
- See Also
- ========
- minimal_block, _union_find_merge
- References
- ==========
- .. [1] Holt, D., Eick, B., O'Brien, E.
- "Handbook of computational group theory"
- .. [7] https://algorithmist.com/wiki/Union_find
- """
- rep, parent = num, parents[num]
- while parent != rep:
- rep = parent
- parent = parents[rep]
- # path compression
- temp, parent = num, parents[num]
- while parent != rep:
- parents[temp] = rep
- temp = parent
- parent = parents[temp]
- return rep
- @property
- def base(self):
- r"""Return a base from the Schreier-Sims algorithm.
- Explanation
- ===========
- For a permutation group `G`, a base is a sequence of points
- `B = (b_1, b_2, \dots, b_k)` such that no element of `G` apart
- from the identity fixes all the points in `B`. The concepts of
- a base and strong generating set and their applications are
- discussed in depth in [1], pp. 87-89 and [2], pp. 55-57.
- An alternative way to think of `B` is that it gives the
- indices of the stabilizer cosets that contain more than the
- identity permutation.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> G = PermutationGroup([Permutation(0, 1, 3)(2, 4)])
- >>> G.base
- [0, 2]
- See Also
- ========
- strong_gens, basic_transversals, basic_orbits, basic_stabilizers
- """
- if self._base == []:
- self.schreier_sims()
- return self._base
- def baseswap(self, base, strong_gens, pos, randomized=False,
- transversals=None, basic_orbits=None, strong_gens_distr=None):
- r"""Swap two consecutive base points in base and strong generating set.
- Explanation
- ===========
- If a base for a group `G` is given by `(b_1, b_2, \dots, b_k)`, this
- function returns a base `(b_1, b_2, \dots, b_{i+1}, b_i, \dots, b_k)`,
- where `i` is given by ``pos``, and a strong generating set relative
- to that base. The original base and strong generating set are not
- modified.
- The randomized version (default) is of Las Vegas type.
- Parameters
- ==========
- base, strong_gens
- The base and strong generating set.
- pos
- The position at which swapping is performed.
- randomized
- A switch between randomized and deterministic version.
- transversals
- The transversals for the basic orbits, if known.
- basic_orbits
- The basic orbits, if known.
- strong_gens_distr
- The strong generators distributed by basic stabilizers, if known.
- Returns
- =======
- (base, strong_gens)
- ``base`` is the new base, and ``strong_gens`` is a generating set
- relative to it.
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import SymmetricGroup
- >>> from sympy.combinatorics.testutil import _verify_bsgs
- >>> from sympy.combinatorics.perm_groups import PermutationGroup
- >>> S = SymmetricGroup(4)
- >>> S.schreier_sims()
- >>> S.base
- [0, 1, 2]
- >>> base, gens = S.baseswap(S.base, S.strong_gens, 1, randomized=False)
- >>> base, gens
- ([0, 2, 1],
- [(0 1 2 3), (3)(0 1), (1 3 2),
- (2 3), (1 3)])
- check that base, gens is a BSGS
- >>> S1 = PermutationGroup(gens)
- >>> _verify_bsgs(S1, base, gens)
- True
- See Also
- ========
- schreier_sims
- Notes
- =====
- The deterministic version of the algorithm is discussed in
- [1], pp. 102-103; the randomized version is discussed in [1], p.103, and
- [2], p.98. It is of Las Vegas type.
- Notice that [1] contains a mistake in the pseudocode and
- discussion of BASESWAP: on line 3 of the pseudocode,
- `|\beta_{i+1}^{\left\langle T\right\rangle}|` should be replaced by
- `|\beta_{i}^{\left\langle T\right\rangle}|`, and the same for the
- discussion of the algorithm.
- """
- # construct the basic orbits, generators for the stabilizer chain
- # and transversal elements from whatever was provided
- transversals, basic_orbits, strong_gens_distr = \
- _handle_precomputed_bsgs(base, strong_gens, transversals,
- basic_orbits, strong_gens_distr)
- base_len = len(base)
- degree = self.degree
- # size of orbit of base[pos] under the stabilizer we seek to insert
- # in the stabilizer chain at position pos + 1
- size = len(basic_orbits[pos])*len(basic_orbits[pos + 1]) \
- //len(_orbit(degree, strong_gens_distr[pos], base[pos + 1]))
- # initialize the wanted stabilizer by a subgroup
- if pos + 2 > base_len - 1:
- T = []
- else:
- T = strong_gens_distr[pos + 2][:]
- # randomized version
- if randomized is True:
- stab_pos = PermutationGroup(strong_gens_distr[pos])
- schreier_vector = stab_pos.schreier_vector(base[pos + 1])
- # add random elements of the stabilizer until they generate it
- while len(_orbit(degree, T, base[pos])) != size:
- new = stab_pos.random_stab(base[pos + 1],
- schreier_vector=schreier_vector)
- T.append(new)
- # deterministic version
- else:
- Gamma = set(basic_orbits[pos])
- Gamma.remove(base[pos])
- if base[pos + 1] in Gamma:
- Gamma.remove(base[pos + 1])
- # add elements of the stabilizer until they generate it by
- # ruling out member of the basic orbit of base[pos] along the way
- while len(_orbit(degree, T, base[pos])) != size:
- gamma = next(iter(Gamma))
- x = transversals[pos][gamma]
- temp = x._array_form.index(base[pos + 1]) # (~x)(base[pos + 1])
- if temp not in basic_orbits[pos + 1]:
- Gamma = Gamma - _orbit(degree, T, gamma)
- else:
- y = transversals[pos + 1][temp]
- el = rmul(x, y)
- if el(base[pos]) not in _orbit(degree, T, base[pos]):
- T.append(el)
- Gamma = Gamma - _orbit(degree, T, base[pos])
- # build the new base and strong generating set
- strong_gens_new_distr = strong_gens_distr[:]
- strong_gens_new_distr[pos + 1] = T
- base_new = base[:]
- base_new[pos], base_new[pos + 1] = base_new[pos + 1], base_new[pos]
- strong_gens_new = _strong_gens_from_distr(strong_gens_new_distr)
- for gen in T:
- if gen not in strong_gens_new:
- strong_gens_new.append(gen)
- return base_new, strong_gens_new
- @property
- def basic_orbits(self):
- r"""
- Return the basic orbits relative to a base and strong generating set.
- Explanation
- ===========
- If `(b_1, b_2, \dots, b_k)` is a base for a group `G`, and
- `G^{(i)} = G_{b_1, b_2, \dots, b_{i-1}}` is the ``i``-th basic stabilizer
- (so that `G^{(1)} = G`), the ``i``-th basic orbit relative to this base
- is the orbit of `b_i` under `G^{(i)}`. See [1], pp. 87-89 for more
- information.
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import SymmetricGroup
- >>> S = SymmetricGroup(4)
- >>> S.basic_orbits
- [[0, 1, 2, 3], [1, 2, 3], [2, 3]]
- See Also
- ========
- base, strong_gens, basic_transversals, basic_stabilizers
- """
- if self._basic_orbits == []:
- self.schreier_sims()
- return self._basic_orbits
- @property
- def basic_stabilizers(self):
- r"""
- Return a chain of stabilizers relative to a base and strong generating
- set.
- Explanation
- ===========
- The ``i``-th basic stabilizer `G^{(i)}` relative to a base
- `(b_1, b_2, \dots, b_k)` is `G_{b_1, b_2, \dots, b_{i-1}}`. For more
- information, see [1], pp. 87-89.
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import AlternatingGroup
- >>> A = AlternatingGroup(4)
- >>> A.schreier_sims()
- >>> A.base
- [0, 1]
- >>> for g in A.basic_stabilizers:
- ... print(g)
- ...
- PermutationGroup([
- (3)(0 1 2),
- (1 2 3)])
- PermutationGroup([
- (1 2 3)])
- See Also
- ========
- base, strong_gens, basic_orbits, basic_transversals
- """
- if self._transversals == []:
- self.schreier_sims()
- strong_gens = self._strong_gens
- base = self._base
- if not base: # e.g. if self is trivial
- return []
- strong_gens_distr = _distribute_gens_by_base(base, strong_gens)
- basic_stabilizers = []
- for gens in strong_gens_distr:
- basic_stabilizers.append(PermutationGroup(gens))
- return basic_stabilizers
- @property
- def basic_transversals(self):
- """
- Return basic transversals relative to a base and strong generating set.
- Explanation
- ===========
- The basic transversals are transversals of the basic orbits. They
- are provided as a list of dictionaries, each dictionary having
- keys - the elements of one of the basic orbits, and values - the
- corresponding transversal elements. See [1], pp. 87-89 for more
- information.
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import AlternatingGroup
- >>> A = AlternatingGroup(4)
- >>> A.basic_transversals
- [{0: (3), 1: (3)(0 1 2), 2: (3)(0 2 1), 3: (0 3 1)}, {1: (3), 2: (1 2 3), 3: (1 3 2)}]
- See Also
- ========
- strong_gens, base, basic_orbits, basic_stabilizers
- """
- if self._transversals == []:
- self.schreier_sims()
- return self._transversals
- def composition_series(self):
- r"""
- Return the composition series for a group as a list
- of permutation groups.
- Explanation
- ===========
- The composition series for a group `G` is defined as a
- subnormal series `G = H_0 > H_1 > H_2 \ldots` A composition
- series is a subnormal series such that each factor group
- `H(i+1) / H(i)` is simple.
- A subnormal series is a composition series only if it is of
- maximum length.
- The algorithm works as follows:
- Starting with the derived series the idea is to fill
- the gap between `G = der[i]` and `H = der[i+1]` for each
- `i` independently. Since, all subgroups of the abelian group
- `G/H` are normal so, first step is to take the generators
- `g` of `G` and add them to generators of `H` one by one.
- The factor groups formed are not simple in general. Each
- group is obtained from the previous one by adding one
- generator `g`, if the previous group is denoted by `H`
- then the next group `K` is generated by `g` and `H`.
- The factor group `K/H` is cyclic and it's order is
- `K.order()//G.order()`. The series is then extended between
- `K` and `H` by groups generated by powers of `g` and `H`.
- The series formed is then prepended to the already existing
- series.
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import SymmetricGroup
- >>> from sympy.combinatorics.named_groups import CyclicGroup
- >>> S = SymmetricGroup(12)
- >>> G = S.sylow_subgroup(2)
- >>> C = G.composition_series()
- >>> [H.order() for H in C]
- [1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1]
- >>> G = S.sylow_subgroup(3)
- >>> C = G.composition_series()
- >>> [H.order() for H in C]
- [243, 81, 27, 9, 3, 1]
- >>> G = CyclicGroup(12)
- >>> C = G.composition_series()
- >>> [H.order() for H in C]
- [12, 6, 3, 1]
- """
- der = self.derived_series()
- if not all(g.is_identity for g in der[-1].generators):
- raise NotImplementedError('Group should be solvable')
- series = []
- for i in range(len(der)-1):
- H = der[i+1]
- up_seg = []
- for g in der[i].generators:
- K = PermutationGroup([g] + H.generators)
- order = K.order() // H.order()
- down_seg = []
- for p, e in factorint(order).items():
- for _ in range(e):
- down_seg.append(PermutationGroup([g] + H.generators))
- g = g**p
- up_seg = down_seg + up_seg
- H = K
- up_seg[0] = der[i]
- series.extend(up_seg)
- series.append(der[-1])
- return series
- def coset_transversal(self, H):
- """Return a transversal of the right cosets of self by its subgroup H
- using the second method described in [1], Subsection 4.6.7
- """
- if not H.is_subgroup(self):
- raise ValueError("The argument must be a subgroup")
- if H.order() == 1:
- return self._elements
- self._schreier_sims(base=H.base) # make G.base an extension of H.base
- base = self.base
- base_ordering = _base_ordering(base, self.degree)
- identity = Permutation(self.degree - 1)
- transversals = self.basic_transversals[:]
- # transversals is a list of dictionaries. Get rid of the keys
- # so that it is a list of lists and sort each list in
- # the increasing order of base[l]^x
- for l, t in enumerate(transversals):
- transversals[l] = sorted(t.values(),
- key = lambda x: base_ordering[base[l]^x])
- orbits = H.basic_orbits
- h_stabs = H.basic_stabilizers
- g_stabs = self.basic_stabilizers
- indices = [x.order()//y.order() for x, y in zip(g_stabs, h_stabs)]
- # T^(l) should be a right transversal of H^(l) in G^(l) for
- # 1<=l<=len(base). While H^(l) is the trivial group, T^(l)
- # contains all the elements of G^(l) so we might just as well
- # start with l = len(h_stabs)-1
- if len(g_stabs) > len(h_stabs):
- T = g_stabs[len(h_stabs)]._elements
- else:
- T = [identity]
- l = len(h_stabs)-1
- t_len = len(T)
- while l > -1:
- T_next = []
- for u in transversals[l]:
- if u == identity:
- continue
- b = base_ordering[base[l]^u]
- for t in T:
- p = t*u
- if all(base_ordering[h^p] >= b for h in orbits[l]):
- T_next.append(p)
- if t_len + len(T_next) == indices[l]:
- break
- if t_len + len(T_next) == indices[l]:
- break
- T += T_next
- t_len += len(T_next)
- l -= 1
- T.remove(identity)
- T = [identity] + T
- return T
- def _coset_representative(self, g, H):
- """Return the representative of Hg from the transversal that
- would be computed by ``self.coset_transversal(H)``.
- """
- if H.order() == 1:
- return g
- # The base of self must be an extension of H.base.
- if not(self.base[:len(H.base)] == H.base):
- self._schreier_sims(base=H.base)
- orbits = H.basic_orbits[:]
- h_transversals = [list(_.values()) for _ in H.basic_transversals]
- transversals = [list(_.values()) for _ in self.basic_transversals]
- base = self.base
- base_ordering = _base_ordering(base, self.degree)
- def step(l, x):
- gamma = sorted(orbits[l], key = lambda y: base_ordering[y^x])[0]
- i = [base[l]^h for h in h_transversals[l]].index(gamma)
- x = h_transversals[l][i]*x
- if l < len(orbits)-1:
- for u in transversals[l]:
- if base[l]^u == base[l]^x:
- break
- x = step(l+1, x*u**-1)*u
- return x
- return step(0, g)
- def coset_table(self, H):
- """Return the standardised (right) coset table of self in H as
- a list of lists.
- """
- # Maybe this should be made to return an instance of CosetTable
- # from fp_groups.py but the class would need to be changed first
- # to be compatible with PermutationGroups
- if not H.is_subgroup(self):
- raise ValueError("The argument must be a subgroup")
- T = self.coset_transversal(H)
- n = len(T)
- A = list(chain.from_iterable((gen, gen**-1)
- for gen in self.generators))
- table = []
- for i in range(n):
- row = [self._coset_representative(T[i]*x, H) for x in A]
- row = [T.index(r) for r in row]
- table.append(row)
- # standardize (this is the same as the algorithm used in coset_table)
- # If CosetTable is made compatible with PermutationGroups, this
- # should be replaced by table.standardize()
- A = range(len(A))
- gamma = 1
- for alpha, a in product(range(n), A):
- beta = table[alpha][a]
- if beta >= gamma:
- if beta > gamma:
- for x in A:
- z = table[gamma][x]
- table[gamma][x] = table[beta][x]
- table[beta][x] = z
- for i in range(n):
- if table[i][x] == beta:
- table[i][x] = gamma
- elif table[i][x] == gamma:
- table[i][x] = beta
- gamma += 1
- if gamma >= n-1:
- return table
- def center(self):
- r"""
- Return the center of a permutation group.
- Explanation
- ===========
- The center for a group `G` is defined as
- `Z(G) = \{z\in G | \forall g\in G, zg = gz \}`,
- the set of elements of `G` that commute with all elements of `G`.
- It is equal to the centralizer of `G` inside `G`, and is naturally a
- subgroup of `G` ([9]).
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import DihedralGroup
- >>> D = DihedralGroup(4)
- >>> G = D.center()
- >>> G.order()
- 2
- See Also
- ========
- centralizer
- Notes
- =====
- This is a naive implementation that is a straightforward application
- of ``.centralizer()``
- """
- return self.centralizer(self)
- def centralizer(self, other):
- r"""
- Return the centralizer of a group/set/element.
- Explanation
- ===========
- The centralizer of a set of permutations ``S`` inside
- a group ``G`` is the set of elements of ``G`` that commute with all
- elements of ``S``::
- `C_G(S) = \{ g \in G | gs = sg \forall s \in S\}` ([10])
- Usually, ``S`` is a subset of ``G``, but if ``G`` is a proper subgroup of
- the full symmetric group, we allow for ``S`` to have elements outside
- ``G``.
- It is naturally a subgroup of ``G``; the centralizer of a permutation
- group is equal to the centralizer of any set of generators for that
- group, since any element commuting with the generators commutes with
- any product of the generators.
- Parameters
- ==========
- other
- a permutation group/list of permutations/single permutation
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import (SymmetricGroup,
- ... CyclicGroup)
- >>> S = SymmetricGroup(6)
- >>> C = CyclicGroup(6)
- >>> H = S.centralizer(C)
- >>> H.is_subgroup(C)
- True
- See Also
- ========
- subgroup_search
- Notes
- =====
- The implementation is an application of ``.subgroup_search()`` with
- tests using a specific base for the group ``G``.
- """
- if hasattr(other, 'generators'):
- if other.is_trivial or self.is_trivial:
- return self
- degree = self.degree
- identity = _af_new(list(range(degree)))
- orbits = other.orbits()
- num_orbits = len(orbits)
- orbits.sort(key=lambda x: -len(x))
- long_base = []
- orbit_reps = [None]*num_orbits
- orbit_reps_indices = [None]*num_orbits
- orbit_descr = [None]*degree
- for i in range(num_orbits):
- orbit = list(orbits[i])
- orbit_reps[i] = orbit[0]
- orbit_reps_indices[i] = len(long_base)
- for point in orbit:
- orbit_descr[point] = i
- long_base = long_base + orbit
- base, strong_gens = self.schreier_sims_incremental(base=long_base)
- strong_gens_distr = _distribute_gens_by_base(base, strong_gens)
- i = 0
- for i in range(len(base)):
- if strong_gens_distr[i] == [identity]:
- break
- base = base[:i]
- base_len = i
- for j in range(num_orbits):
- if base[base_len - 1] in orbits[j]:
- break
- rel_orbits = orbits[: j + 1]
- num_rel_orbits = len(rel_orbits)
- transversals = [None]*num_rel_orbits
- for j in range(num_rel_orbits):
- rep = orbit_reps[j]
- transversals[j] = dict(
- other.orbit_transversal(rep, pairs=True))
- trivial_test = lambda x: True
- tests = [None]*base_len
- for l in range(base_len):
- if base[l] in orbit_reps:
- tests[l] = trivial_test
- else:
- def test(computed_words, l=l):
- g = computed_words[l]
- rep_orb_index = orbit_descr[base[l]]
- rep = orbit_reps[rep_orb_index]
- im = g._array_form[base[l]]
- im_rep = g._array_form[rep]
- tr_el = transversals[rep_orb_index][base[l]]
- # using the definition of transversal,
- # base[l]^g = rep^(tr_el*g);
- # if g belongs to the centralizer, then
- # base[l]^g = (rep^g)^tr_el
- return im == tr_el._array_form[im_rep]
- tests[l] = test
- def prop(g):
- return [rmul(g, gen) for gen in other.generators] == \
- [rmul(gen, g) for gen in other.generators]
- return self.subgroup_search(prop, base=base,
- strong_gens=strong_gens, tests=tests)
- elif hasattr(other, '__getitem__'):
- gens = list(other)
- return self.centralizer(PermutationGroup(gens))
- elif hasattr(other, 'array_form'):
- return self.centralizer(PermutationGroup([other]))
- def commutator(self, G, H):
- """
- Return the commutator of two subgroups.
- Explanation
- ===========
- For a permutation group ``K`` and subgroups ``G``, ``H``, the
- commutator of ``G`` and ``H`` is defined as the group generated
- by all the commutators `[g, h] = hgh^{-1}g^{-1}` for ``g`` in ``G`` and
- ``h`` in ``H``. It is naturally a subgroup of ``K`` ([1], p.27).
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import (SymmetricGroup,
- ... AlternatingGroup)
- >>> S = SymmetricGroup(5)
- >>> A = AlternatingGroup(5)
- >>> G = S.commutator(S, A)
- >>> G.is_subgroup(A)
- True
- See Also
- ========
- derived_subgroup
- Notes
- =====
- The commutator of two subgroups `H, G` is equal to the normal closure
- of the commutators of all the generators, i.e. `hgh^{-1}g^{-1}` for `h`
- a generator of `H` and `g` a generator of `G` ([1], p.28)
- """
- ggens = G.generators
- hgens = H.generators
- commutators = []
- for ggen in ggens:
- for hgen in hgens:
- commutator = rmul(hgen, ggen, ~hgen, ~ggen)
- if commutator not in commutators:
- commutators.append(commutator)
- res = self.normal_closure(commutators)
- return res
- def coset_factor(self, g, factor_index=False):
- """Return ``G``'s (self's) coset factorization of ``g``
- Explanation
- ===========
- If ``g`` is an element of ``G`` then it can be written as the product
- of permutations drawn from the Schreier-Sims coset decomposition,
- The permutations returned in ``f`` are those for which
- the product gives ``g``: ``g = f[n]*...f[1]*f[0]`` where ``n = len(B)``
- and ``B = G.base``. f[i] is one of the permutations in
- ``self._basic_orbits[i]``.
- If factor_index==True,
- returns a tuple ``[b[0],..,b[n]]``, where ``b[i]``
- belongs to ``self._basic_orbits[i]``
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation(0, 1, 3, 7, 6, 4)(2, 5)
- >>> b = Permutation(0, 1, 3, 2)(4, 5, 7, 6)
- >>> G = PermutationGroup([a, b])
- Define g:
- >>> g = Permutation(7)(1, 2, 4)(3, 6, 5)
- Confirm that it is an element of G:
- >>> G.contains(g)
- True
- Thus, it can be written as a product of factors (up to
- 3) drawn from u. See below that a factor from u1 and u2
- and the Identity permutation have been used:
- >>> f = G.coset_factor(g)
- >>> f[2]*f[1]*f[0] == g
- True
- >>> f1 = G.coset_factor(g, True); f1
- [0, 4, 4]
- >>> tr = G.basic_transversals
- >>> f[0] == tr[0][f1[0]]
- True
- If g is not an element of G then [] is returned:
- >>> c = Permutation(5, 6, 7)
- >>> G.coset_factor(c)
- []
- See Also
- ========
- sympy.combinatorics.util._strip
- """
- if isinstance(g, (Cycle, Permutation)):
- g = g.list()
- if len(g) != self._degree:
- # this could either adjust the size or return [] immediately
- # but we don't choose between the two and just signal a possible
- # error
- raise ValueError('g should be the same size as permutations of G')
- I = list(range(self._degree))
- basic_orbits = self.basic_orbits
- transversals = self._transversals
- factors = []
- base = self.base
- h = g
- for i in range(len(base)):
- beta = h[base[i]]
- if beta == base[i]:
- factors.append(beta)
- continue
- if beta not in basic_orbits[i]:
- return []
- u = transversals[i][beta]._array_form
- h = _af_rmul(_af_invert(u), h)
- factors.append(beta)
- if h != I:
- return []
- if factor_index:
- return factors
- tr = self.basic_transversals
- factors = [tr[i][factors[i]] for i in range(len(base))]
- return factors
- def generator_product(self, g, original=False):
- r'''
- Return a list of strong generators `[s1, \dots, sn]`
- s.t `g = sn \times \dots \times s1`. If ``original=True``, make the
- list contain only the original group generators
- '''
- product = []
- if g.is_identity:
- return []
- if g in self.strong_gens:
- if not original or g in self.generators:
- return [g]
- else:
- slp = self._strong_gens_slp[g]
- for s in slp:
- product.extend(self.generator_product(s, original=True))
- return product
- elif g**-1 in self.strong_gens:
- g = g**-1
- if not original or g in self.generators:
- return [g**-1]
- else:
- slp = self._strong_gens_slp[g]
- for s in slp:
- product.extend(self.generator_product(s, original=True))
- l = len(product)
- product = [product[l-i-1]**-1 for i in range(l)]
- return product
- f = self.coset_factor(g, True)
- for i, j in enumerate(f):
- slp = self._transversal_slp[i][j]
- for s in slp:
- if not original:
- product.append(self.strong_gens[s])
- else:
- s = self.strong_gens[s]
- product.extend(self.generator_product(s, original=True))
- return product
- def coset_rank(self, g):
- """rank using Schreier-Sims representation.
- Explanation
- ===========
- The coset rank of ``g`` is the ordering number in which
- it appears in the lexicographic listing according to the
- coset decomposition
- The ordering is the same as in G.generate(method='coset').
- If ``g`` does not belong to the group it returns None.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation(0, 1, 3, 7, 6, 4)(2, 5)
- >>> b = Permutation(0, 1, 3, 2)(4, 5, 7, 6)
- >>> G = PermutationGroup([a, b])
- >>> c = Permutation(7)(2, 4)(3, 5)
- >>> G.coset_rank(c)
- 16
- >>> G.coset_unrank(16)
- (7)(2 4)(3 5)
- See Also
- ========
- coset_factor
- """
- factors = self.coset_factor(g, True)
- if not factors:
- return None
- rank = 0
- b = 1
- transversals = self._transversals
- base = self._base
- basic_orbits = self._basic_orbits
- for i in range(len(base)):
- k = factors[i]
- j = basic_orbits[i].index(k)
- rank += b*j
- b = b*len(transversals[i])
- return rank
- def coset_unrank(self, rank, af=False):
- """unrank using Schreier-Sims representation
- coset_unrank is the inverse operation of coset_rank
- if 0 <= rank < order; otherwise it returns None.
- """
- if rank < 0 or rank >= self.order():
- return None
- base = self.base
- transversals = self.basic_transversals
- basic_orbits = self.basic_orbits
- m = len(base)
- v = [0]*m
- for i in range(m):
- rank, c = divmod(rank, len(transversals[i]))
- v[i] = basic_orbits[i][c]
- a = [transversals[i][v[i]]._array_form for i in range(m)]
- h = _af_rmuln(*a)
- if af:
- return h
- else:
- return _af_new(h)
- @property
- def degree(self):
- """Returns the size of the permutations in the group.
- Explanation
- ===========
- The number of permutations comprising the group is given by
- ``len(group)``; the number of permutations that can be generated
- by the group is given by ``group.order()``.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation([1, 0, 2])
- >>> G = PermutationGroup([a])
- >>> G.degree
- 3
- >>> len(G)
- 1
- >>> G.order()
- 2
- >>> list(G.generate())
- [(2), (2)(0 1)]
- See Also
- ========
- order
- """
- return self._degree
- @property
- def identity(self):
- '''
- Return the identity element of the permutation group.
- '''
- return _af_new(list(range(self.degree)))
- @property
- def elements(self):
- """Returns all the elements of the permutation group as a set
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> p = PermutationGroup(Permutation(1, 3), Permutation(1, 2))
- >>> p.elements
- {(1 2 3), (1 3 2), (1 3), (2 3), (3), (3)(1 2)}
- """
- return set(self._elements)
- @property
- def _elements(self):
- """Returns all the elements of the permutation group as a list
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> p = PermutationGroup(Permutation(1, 3), Permutation(1, 2))
- >>> p._elements
- [(3), (3)(1 2), (1 3), (2 3), (1 2 3), (1 3 2)]
- """
- return list(islice(self.generate(), None))
- def derived_series(self):
- r"""Return the derived series for the group.
- Explanation
- ===========
- The derived series for a group `G` is defined as
- `G = G_0 > G_1 > G_2 > \ldots` where `G_i = [G_{i-1}, G_{i-1}]`,
- i.e. `G_i` is the derived subgroup of `G_{i-1}`, for
- `i\in\mathbb{N}`. When we have `G_k = G_{k-1}` for some
- `k\in\mathbb{N}`, the series terminates.
- Returns
- =======
- A list of permutation groups containing the members of the derived
- series in the order `G = G_0, G_1, G_2, \ldots`.
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import (SymmetricGroup,
- ... AlternatingGroup, DihedralGroup)
- >>> A = AlternatingGroup(5)
- >>> len(A.derived_series())
- 1
- >>> S = SymmetricGroup(4)
- >>> len(S.derived_series())
- 4
- >>> S.derived_series()[1].is_subgroup(AlternatingGroup(4))
- True
- >>> S.derived_series()[2].is_subgroup(DihedralGroup(2))
- True
- See Also
- ========
- derived_subgroup
- """
- res = [self]
- current = self
- nxt = self.derived_subgroup()
- while not current.is_subgroup(nxt):
- res.append(nxt)
- current = nxt
- nxt = nxt.derived_subgroup()
- return res
- def derived_subgroup(self):
- r"""Compute the derived subgroup.
- Explanation
- ===========
- The derived subgroup, or commutator subgroup is the subgroup generated
- by all commutators `[g, h] = hgh^{-1}g^{-1}` for `g, h\in G` ; it is
- equal to the normal closure of the set of commutators of the generators
- ([1], p.28, [11]).
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation([1, 0, 2, 4, 3])
- >>> b = Permutation([0, 1, 3, 2, 4])
- >>> G = PermutationGroup([a, b])
- >>> C = G.derived_subgroup()
- >>> list(C.generate(af=True))
- [[0, 1, 2, 3, 4], [0, 1, 3, 4, 2], [0, 1, 4, 2, 3]]
- See Also
- ========
- derived_series
- """
- r = self._r
- gens = [p._array_form for p in self.generators]
- set_commutators = set()
- degree = self._degree
- rng = list(range(degree))
- for i in range(r):
- for j in range(r):
- p1 = gens[i]
- p2 = gens[j]
- c = list(range(degree))
- for k in rng:
- c[p2[p1[k]]] = p1[p2[k]]
- ct = tuple(c)
- if ct not in set_commutators:
- set_commutators.add(ct)
- cms = [_af_new(p) for p in set_commutators]
- G2 = self.normal_closure(cms)
- return G2
- def generate(self, method="coset", af=False):
- """Return iterator to generate the elements of the group.
- Explanation
- ===========
- Iteration is done with one of these methods::
- method='coset' using the Schreier-Sims coset representation
- method='dimino' using the Dimino method
- If ``af = True`` it yields the array form of the permutations
- Examples
- ========
- >>> from sympy.combinatorics import PermutationGroup
- >>> from sympy.combinatorics.polyhedron import tetrahedron
- The permutation group given in the tetrahedron object is also
- true groups:
- >>> G = tetrahedron.pgroup
- >>> G.is_group
- True
- Also the group generated by the permutations in the tetrahedron
- pgroup -- even the first two -- is a proper group:
- >>> H = PermutationGroup(G[0], G[1])
- >>> J = PermutationGroup(list(H.generate())); J
- PermutationGroup([
- (0 1)(2 3),
- (1 2 3),
- (1 3 2),
- (0 3 1),
- (0 2 3),
- (0 3)(1 2),
- (0 1 3),
- (3)(0 2 1),
- (0 3 2),
- (3)(0 1 2),
- (0 2)(1 3)])
- >>> _.is_group
- True
- """
- if method == "coset":
- return self.generate_schreier_sims(af)
- elif method == "dimino":
- return self.generate_dimino(af)
- else:
- raise NotImplementedError('No generation defined for %s' % method)
- def generate_dimino(self, af=False):
- """Yield group elements using Dimino's algorithm.
- If ``af == True`` it yields the array form of the permutations.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation([0, 2, 1, 3])
- >>> b = Permutation([0, 2, 3, 1])
- >>> g = PermutationGroup([a, b])
- >>> list(g.generate_dimino(af=True))
- [[0, 1, 2, 3], [0, 2, 1, 3], [0, 2, 3, 1],
- [0, 1, 3, 2], [0, 3, 2, 1], [0, 3, 1, 2]]
- References
- ==========
- .. [1] The Implementation of Various Algorithms for Permutation Groups in
- the Computer Algebra System: AXIOM, N.J. Doye, M.Sc. Thesis
- """
- idn = list(range(self.degree))
- order = 0
- element_list = [idn]
- set_element_list = {tuple(idn)}
- if af:
- yield idn
- else:
- yield _af_new(idn)
- gens = [p._array_form for p in self.generators]
- for i in range(len(gens)):
- # D elements of the subgroup G_i generated by gens[:i]
- D = element_list[:]
- N = [idn]
- while N:
- A = N
- N = []
- for a in A:
- for g in gens[:i + 1]:
- ag = _af_rmul(a, g)
- if tuple(ag) not in set_element_list:
- # produce G_i*g
- for d in D:
- order += 1
- ap = _af_rmul(d, ag)
- if af:
- yield ap
- else:
- p = _af_new(ap)
- yield p
- element_list.append(ap)
- set_element_list.add(tuple(ap))
- N.append(ap)
- self._order = len(element_list)
- def generate_schreier_sims(self, af=False):
- """Yield group elements using the Schreier-Sims representation
- in coset_rank order
- If ``af = True`` it yields the array form of the permutations
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation([0, 2, 1, 3])
- >>> b = Permutation([0, 2, 3, 1])
- >>> g = PermutationGroup([a, b])
- >>> list(g.generate_schreier_sims(af=True))
- [[0, 1, 2, 3], [0, 2, 1, 3], [0, 3, 2, 1],
- [0, 1, 3, 2], [0, 2, 3, 1], [0, 3, 1, 2]]
- """
- n = self._degree
- u = self.basic_transversals
- basic_orbits = self._basic_orbits
- if len(u) == 0:
- for x in self.generators:
- if af:
- yield x._array_form
- else:
- yield x
- return
- if len(u) == 1:
- for i in basic_orbits[0]:
- if af:
- yield u[0][i]._array_form
- else:
- yield u[0][i]
- return
- u = list(reversed(u))
- basic_orbits = basic_orbits[::-1]
- # stg stack of group elements
- stg = [list(range(n))]
- posmax = [len(x) for x in u]
- n1 = len(posmax) - 1
- pos = [0]*n1
- h = 0
- while 1:
- # backtrack when finished iterating over coset
- if pos[h] >= posmax[h]:
- if h == 0:
- return
- pos[h] = 0
- h -= 1
- stg.pop()
- continue
- p = _af_rmul(u[h][basic_orbits[h][pos[h]]]._array_form, stg[-1])
- pos[h] += 1
- stg.append(p)
- h += 1
- if h == n1:
- if af:
- for i in basic_orbits[-1]:
- p = _af_rmul(u[-1][i]._array_form, stg[-1])
- yield p
- else:
- for i in basic_orbits[-1]:
- p = _af_rmul(u[-1][i]._array_form, stg[-1])
- p1 = _af_new(p)
- yield p1
- stg.pop()
- h -= 1
- @property
- def generators(self):
- """Returns the generators of the group.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation([0, 2, 1])
- >>> b = Permutation([1, 0, 2])
- >>> G = PermutationGroup([a, b])
- >>> G.generators
- [(1 2), (2)(0 1)]
- """
- return self._generators
- def contains(self, g, strict=True):
- """Test if permutation ``g`` belong to self, ``G``.
- Explanation
- ===========
- If ``g`` is an element of ``G`` it can be written as a product
- of factors drawn from the cosets of ``G``'s stabilizers. To see
- if ``g`` is one of the actual generators defining the group use
- ``G.has(g)``.
- If ``strict`` is not ``True``, ``g`` will be resized, if necessary,
- to match the size of permutations in ``self``.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation(1, 2)
- >>> b = Permutation(2, 3, 1)
- >>> G = PermutationGroup(a, b, degree=5)
- >>> G.contains(G[0]) # trivial check
- True
- >>> elem = Permutation([[2, 3]], size=5)
- >>> G.contains(elem)
- True
- >>> G.contains(Permutation(4)(0, 1, 2, 3))
- False
- If strict is False, a permutation will be resized, if
- necessary:
- >>> H = PermutationGroup(Permutation(5))
- >>> H.contains(Permutation(3))
- False
- >>> H.contains(Permutation(3), strict=False)
- True
- To test if a given permutation is present in the group:
- >>> elem in G.generators
- False
- >>> G.has(elem)
- False
- See Also
- ========
- coset_factor, sympy.core.basic.Basic.has, __contains__
- """
- if not isinstance(g, Permutation):
- return False
- if g.size != self.degree:
- if strict:
- return False
- g = Permutation(g, size=self.degree)
- if g in self.generators:
- return True
- return bool(self.coset_factor(g.array_form, True))
- @property
- def is_perfect(self):
- """Return ``True`` if the group is perfect.
- A group is perfect if it equals to its derived subgroup.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation(1,2,3)(4,5)
- >>> b = Permutation(1,2,3,4,5)
- >>> G = PermutationGroup([a, b])
- >>> G.is_perfect
- False
- """
- if self._is_perfect is None:
- self._is_perfect = self.equals(self.derived_subgroup())
- return self._is_perfect
- @property
- def is_abelian(self):
- """Test if the group is Abelian.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation([0, 2, 1])
- >>> b = Permutation([1, 0, 2])
- >>> G = PermutationGroup([a, b])
- >>> G.is_abelian
- False
- >>> a = Permutation([0, 2, 1])
- >>> G = PermutationGroup([a])
- >>> G.is_abelian
- True
- """
- if self._is_abelian is not None:
- return self._is_abelian
- self._is_abelian = True
- gens = [p._array_form for p in self.generators]
- for x in gens:
- for y in gens:
- if y <= x:
- continue
- if not _af_commutes_with(x, y):
- self._is_abelian = False
- return False
- return True
- def abelian_invariants(self):
- """
- Returns the abelian invariants for the given group.
- Let ``G`` be a nontrivial finite abelian group. Then G is isomorphic to
- the direct product of finitely many nontrivial cyclic groups of
- prime-power order.
- Explanation
- ===========
- The prime-powers that occur as the orders of the factors are uniquely
- determined by G. More precisely, the primes that occur in the orders of the
- factors in any such decomposition of ``G`` are exactly the primes that divide
- ``|G|`` and for any such prime ``p``, if the orders of the factors that are
- p-groups in one such decomposition of ``G`` are ``p^{t_1} >= p^{t_2} >= ... p^{t_r}``,
- then the orders of the factors that are p-groups in any such decomposition of ``G``
- are ``p^{t_1} >= p^{t_2} >= ... p^{t_r}``.
- The uniquely determined integers ``p^{t_1} >= p^{t_2} >= ... p^{t_r}``, taken
- for all primes that divide ``|G|`` are called the invariants of the nontrivial
- group ``G`` as suggested in ([14], p. 542).
- Notes
- =====
- We adopt the convention that the invariants of a trivial group are [].
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation([0, 2, 1])
- >>> b = Permutation([1, 0, 2])
- >>> G = PermutationGroup([a, b])
- >>> G.abelian_invariants()
- [2]
- >>> from sympy.combinatorics import CyclicGroup
- >>> G = CyclicGroup(7)
- >>> G.abelian_invariants()
- [7]
- """
- if self.is_trivial:
- return []
- gns = self.generators
- inv = []
- G = self
- H = G.derived_subgroup()
- Hgens = H.generators
- for p in primefactors(G.order()):
- ranks = []
- while True:
- pows = []
- for g in gns:
- elm = g**p
- if not H.contains(elm):
- pows.append(elm)
- K = PermutationGroup(Hgens + pows) if pows else H
- r = G.order()//K.order()
- G = K
- gns = pows
- if r == 1:
- break
- ranks.append(multiplicity(p, r))
- if ranks:
- pows = [1]*ranks[0]
- for i in ranks:
- for j in range(i):
- pows[j] = pows[j]*p
- inv.extend(pows)
- inv.sort()
- return inv
- def is_elementary(self, p):
- """Return ``True`` if the group is elementary abelian. An elementary
- abelian group is a finite abelian group, where every nontrivial
- element has order `p`, where `p` is a prime.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation([0, 2, 1])
- >>> G = PermutationGroup([a])
- >>> G.is_elementary(2)
- True
- >>> a = Permutation([0, 2, 1, 3])
- >>> b = Permutation([3, 1, 2, 0])
- >>> G = PermutationGroup([a, b])
- >>> G.is_elementary(2)
- True
- >>> G.is_elementary(3)
- False
- """
- return self.is_abelian and all(g.order() == p for g in self.generators)
- def _eval_is_alt_sym_naive(self, only_sym=False, only_alt=False):
- """A naive test using the group order."""
- if only_sym and only_alt:
- raise ValueError(
- "Both {} and {} cannot be set to True"
- .format(only_sym, only_alt))
- n = self.degree
- sym_order = _factorial(n)
- order = self.order()
- if order == sym_order:
- self._is_sym = True
- self._is_alt = False
- if only_alt:
- return False
- return True
- elif 2*order == sym_order:
- self._is_sym = False
- self._is_alt = True
- if only_sym:
- return False
- return True
- return False
- def _eval_is_alt_sym_monte_carlo(self, eps=0.05, perms=None):
- """A test using monte-carlo algorithm.
- Parameters
- ==========
- eps : float, optional
- The criterion for the incorrect ``False`` return.
- perms : list[Permutation], optional
- If explicitly given, it tests over the given candidates
- for testing.
- If ``None``, it randomly computes ``N_eps`` and chooses
- ``N_eps`` sample of the permutation from the group.
- See Also
- ========
- _check_cycles_alt_sym
- """
- if perms is None:
- n = self.degree
- if n < 17:
- c_n = 0.34
- else:
- c_n = 0.57
- d_n = (c_n*log(2))/log(n)
- N_eps = int(-log(eps)/d_n)
- perms = (self.random_pr() for i in range(N_eps))
- return self._eval_is_alt_sym_monte_carlo(perms=perms)
- for perm in perms:
- if _check_cycles_alt_sym(perm):
- return True
- return False
- def is_alt_sym(self, eps=0.05, _random_prec=None):
- r"""Monte Carlo test for the symmetric/alternating group for degrees
- >= 8.
- Explanation
- ===========
- More specifically, it is one-sided Monte Carlo with the
- answer True (i.e., G is symmetric/alternating) guaranteed to be
- correct, and the answer False being incorrect with probability eps.
- For degree < 8, the order of the group is checked so the test
- is deterministic.
- Notes
- =====
- The algorithm itself uses some nontrivial results from group theory and
- number theory:
- 1) If a transitive group ``G`` of degree ``n`` contains an element
- with a cycle of length ``n/2 < p < n-2`` for ``p`` a prime, ``G`` is the
- symmetric or alternating group ([1], pp. 81-82)
- 2) The proportion of elements in the symmetric/alternating group having
- the property described in 1) is approximately `\log(2)/\log(n)`
- ([1], p.82; [2], pp. 226-227).
- The helper function ``_check_cycles_alt_sym`` is used to
- go over the cycles in a permutation and look for ones satisfying 1).
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import DihedralGroup
- >>> D = DihedralGroup(10)
- >>> D.is_alt_sym()
- False
- See Also
- ========
- _check_cycles_alt_sym
- """
- if _random_prec is not None:
- N_eps = _random_prec['N_eps']
- perms= (_random_prec[i] for i in range(N_eps))
- return self._eval_is_alt_sym_monte_carlo(perms=perms)
- if self._is_sym or self._is_alt:
- return True
- if self._is_sym is False and self._is_alt is False:
- return False
- n = self.degree
- if n < 8:
- return self._eval_is_alt_sym_naive()
- elif self.is_transitive():
- return self._eval_is_alt_sym_monte_carlo(eps=eps)
- self._is_sym, self._is_alt = False, False
- return False
- @property
- def is_nilpotent(self):
- """Test if the group is nilpotent.
- Explanation
- ===========
- A group `G` is nilpotent if it has a central series of finite length.
- Alternatively, `G` is nilpotent if its lower central series terminates
- with the trivial group. Every nilpotent group is also solvable
- ([1], p.29, [12]).
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import (SymmetricGroup,
- ... CyclicGroup)
- >>> C = CyclicGroup(6)
- >>> C.is_nilpotent
- True
- >>> S = SymmetricGroup(5)
- >>> S.is_nilpotent
- False
- See Also
- ========
- lower_central_series, is_solvable
- """
- if self._is_nilpotent is None:
- lcs = self.lower_central_series()
- terminator = lcs[len(lcs) - 1]
- gens = terminator.generators
- degree = self.degree
- identity = _af_new(list(range(degree)))
- if all(g == identity for g in gens):
- self._is_solvable = True
- self._is_nilpotent = True
- return True
- else:
- self._is_nilpotent = False
- return False
- else:
- return self._is_nilpotent
- def is_normal(self, gr, strict=True):
- """Test if ``G=self`` is a normal subgroup of ``gr``.
- Explanation
- ===========
- G is normal in gr if
- for each g2 in G, g1 in gr, ``g = g1*g2*g1**-1`` belongs to G
- It is sufficient to check this for each g1 in gr.generators and
- g2 in G.generators.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation([1, 2, 0])
- >>> b = Permutation([1, 0, 2])
- >>> G = PermutationGroup([a, b])
- >>> G1 = PermutationGroup([a, Permutation([2, 0, 1])])
- >>> G1.is_normal(G)
- True
- """
- if not self.is_subgroup(gr, strict=strict):
- return False
- d_self = self.degree
- d_gr = gr.degree
- if self.is_trivial and (d_self == d_gr or not strict):
- return True
- if self._is_abelian:
- return True
- new_self = self.copy()
- if not strict and d_self != d_gr:
- if d_self < d_gr:
- new_self = PermGroup(new_self.generators + [Permutation(d_gr - 1)])
- else:
- gr = PermGroup(gr.generators + [Permutation(d_self - 1)])
- gens2 = [p._array_form for p in new_self.generators]
- gens1 = [p._array_form for p in gr.generators]
- for g1 in gens1:
- for g2 in gens2:
- p = _af_rmuln(g1, g2, _af_invert(g1))
- if not new_self.coset_factor(p, True):
- return False
- return True
- def is_primitive(self, randomized=True):
- r"""Test if a group is primitive.
- Explanation
- ===========
- A permutation group ``G`` acting on a set ``S`` is called primitive if
- ``S`` contains no nontrivial block under the action of ``G``
- (a block is nontrivial if its cardinality is more than ``1``).
- Notes
- =====
- The algorithm is described in [1], p.83, and uses the function
- minimal_block to search for blocks of the form `\{0, k\}` for ``k``
- ranging over representatives for the orbits of `G_0`, the stabilizer of
- ``0``. This algorithm has complexity `O(n^2)` where ``n`` is the degree
- of the group, and will perform badly if `G_0` is small.
- There are two implementations offered: one finds `G_0`
- deterministically using the function ``stabilizer``, and the other
- (default) produces random elements of `G_0` using ``random_stab``,
- hoping that they generate a subgroup of `G_0` with not too many more
- orbits than `G_0` (this is suggested in [1], p.83). Behavior is changed
- by the ``randomized`` flag.
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import DihedralGroup
- >>> D = DihedralGroup(10)
- >>> D.is_primitive()
- False
- See Also
- ========
- minimal_block, random_stab
- """
- if self._is_primitive is not None:
- return self._is_primitive
- if self.is_transitive() is False:
- return False
- if randomized:
- random_stab_gens = []
- v = self.schreier_vector(0)
- for _ in range(len(self)):
- random_stab_gens.append(self.random_stab(0, v))
- stab = PermutationGroup(random_stab_gens)
- else:
- stab = self.stabilizer(0)
- orbits = stab.orbits()
- for orb in orbits:
- x = orb.pop()
- if x != 0 and any(e != 0 for e in self.minimal_block([0, x])):
- self._is_primitive = False
- return False
- self._is_primitive = True
- return True
- def minimal_blocks(self, randomized=True):
- '''
- For a transitive group, return the list of all minimal
- block systems. If a group is intransitive, return `False`.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> from sympy.combinatorics.named_groups import DihedralGroup
- >>> DihedralGroup(6).minimal_blocks()
- [[0, 1, 0, 1, 0, 1], [0, 1, 2, 0, 1, 2]]
- >>> G = PermutationGroup(Permutation(1,2,5))
- >>> G.minimal_blocks()
- False
- See Also
- ========
- minimal_block, is_transitive, is_primitive
- '''
- def _number_blocks(blocks):
- # number the blocks of a block system
- # in order and return the number of
- # blocks and the tuple with the
- # reordering
- n = len(blocks)
- appeared = {}
- m = 0
- b = [None]*n
- for i in range(n):
- if blocks[i] not in appeared:
- appeared[blocks[i]] = m
- b[i] = m
- m += 1
- else:
- b[i] = appeared[blocks[i]]
- return tuple(b), m
- if not self.is_transitive():
- return False
- blocks = []
- num_blocks = []
- rep_blocks = []
- if randomized:
- random_stab_gens = []
- v = self.schreier_vector(0)
- for i in range(len(self)):
- random_stab_gens.append(self.random_stab(0, v))
- stab = PermutationGroup(random_stab_gens)
- else:
- stab = self.stabilizer(0)
- orbits = stab.orbits()
- for orb in orbits:
- x = orb.pop()
- if x != 0:
- block = self.minimal_block([0, x])
- num_block, _ = _number_blocks(block)
- # a representative block (containing 0)
- rep = {j for j in range(self.degree) if num_block[j] == 0}
- # check if the system is minimal with
- # respect to the already discovere ones
- minimal = True
- blocks_remove_mask = [False] * len(blocks)
- for i, r in enumerate(rep_blocks):
- if len(r) > len(rep) and rep.issubset(r):
- # i-th block system is not minimal
- blocks_remove_mask[i] = True
- elif len(r) < len(rep) and r.issubset(rep):
- # the system being checked is not minimal
- minimal = False
- break
- # remove non-minimal representative blocks
- blocks = [b for i, b in enumerate(blocks) if not blocks_remove_mask[i]]
- num_blocks = [n for i, n in enumerate(num_blocks) if not blocks_remove_mask[i]]
- rep_blocks = [r for i, r in enumerate(rep_blocks) if not blocks_remove_mask[i]]
- if minimal and num_block not in num_blocks:
- blocks.append(block)
- num_blocks.append(num_block)
- rep_blocks.append(rep)
- return blocks
- @property
- def is_solvable(self):
- """Test if the group is solvable.
- ``G`` is solvable if its derived series terminates with the trivial
- group ([1], p.29).
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import SymmetricGroup
- >>> S = SymmetricGroup(3)
- >>> S.is_solvable
- True
- See Also
- ========
- is_nilpotent, derived_series
- """
- if self._is_solvable is None:
- if self.order() % 2 != 0:
- return True
- ds = self.derived_series()
- terminator = ds[len(ds) - 1]
- gens = terminator.generators
- degree = self.degree
- identity = _af_new(list(range(degree)))
- if all(g == identity for g in gens):
- self._is_solvable = True
- return True
- else:
- self._is_solvable = False
- return False
- else:
- return self._is_solvable
- def is_subgroup(self, G, strict=True):
- """Return ``True`` if all elements of ``self`` belong to ``G``.
- If ``strict`` is ``False`` then if ``self``'s degree is smaller
- than ``G``'s, the elements will be resized to have the same degree.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> from sympy.combinatorics import SymmetricGroup, CyclicGroup
- Testing is strict by default: the degree of each group must be the
- same:
- >>> p = Permutation(0, 1, 2, 3, 4, 5)
- >>> G1 = PermutationGroup([Permutation(0, 1, 2), Permutation(0, 1)])
- >>> G2 = PermutationGroup([Permutation(0, 2), Permutation(0, 1, 2)])
- >>> G3 = PermutationGroup([p, p**2])
- >>> assert G1.order() == G2.order() == G3.order() == 6
- >>> G1.is_subgroup(G2)
- True
- >>> G1.is_subgroup(G3)
- False
- >>> G3.is_subgroup(PermutationGroup(G3[1]))
- False
- >>> G3.is_subgroup(PermutationGroup(G3[0]))
- True
- To ignore the size, set ``strict`` to ``False``:
- >>> S3 = SymmetricGroup(3)
- >>> S5 = SymmetricGroup(5)
- >>> S3.is_subgroup(S5, strict=False)
- True
- >>> C7 = CyclicGroup(7)
- >>> G = S5*C7
- >>> S5.is_subgroup(G, False)
- True
- >>> C7.is_subgroup(G, 0)
- False
- """
- if isinstance(G, SymmetricPermutationGroup):
- if self.degree != G.degree:
- return False
- return True
- if not isinstance(G, PermutationGroup):
- return False
- if self == G or self.generators[0]==Permutation():
- return True
- if G.order() % self.order() != 0:
- return False
- if self.degree == G.degree or \
- (self.degree < G.degree and not strict):
- gens = self.generators
- else:
- return False
- return all(G.contains(g, strict=strict) for g in gens)
- @property
- def is_polycyclic(self):
- """Return ``True`` if a group is polycyclic. A group is polycyclic if
- it has a subnormal series with cyclic factors. For finite groups,
- this is the same as if the group is solvable.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation([0, 2, 1, 3])
- >>> b = Permutation([2, 0, 1, 3])
- >>> G = PermutationGroup([a, b])
- >>> G.is_polycyclic
- True
- """
- return self.is_solvable
- def is_transitive(self, strict=True):
- """Test if the group is transitive.
- Explanation
- ===========
- A group is transitive if it has a single orbit.
- If ``strict`` is ``False`` the group is transitive if it has
- a single orbit of length different from 1.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation([0, 2, 1, 3])
- >>> b = Permutation([2, 0, 1, 3])
- >>> G1 = PermutationGroup([a, b])
- >>> G1.is_transitive()
- False
- >>> G1.is_transitive(strict=False)
- True
- >>> c = Permutation([2, 3, 0, 1])
- >>> G2 = PermutationGroup([a, c])
- >>> G2.is_transitive()
- True
- >>> d = Permutation([1, 0, 2, 3])
- >>> e = Permutation([0, 1, 3, 2])
- >>> G3 = PermutationGroup([d, e])
- >>> G3.is_transitive() or G3.is_transitive(strict=False)
- False
- """
- if self._is_transitive: # strict or not, if True then True
- return self._is_transitive
- if strict:
- if self._is_transitive is not None: # we only store strict=True
- return self._is_transitive
- ans = len(self.orbit(0)) == self.degree
- self._is_transitive = ans
- return ans
- got_orb = False
- for x in self.orbits():
- if len(x) > 1:
- if got_orb:
- return False
- got_orb = True
- return got_orb
- @property
- def is_trivial(self):
- """Test if the group is the trivial group.
- This is true if the group contains only the identity permutation.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> G = PermutationGroup([Permutation([0, 1, 2])])
- >>> G.is_trivial
- True
- """
- if self._is_trivial is None:
- self._is_trivial = len(self) == 1 and self[0].is_Identity
- return self._is_trivial
- def lower_central_series(self):
- r"""Return the lower central series for the group.
- The lower central series for a group `G` is the series
- `G = G_0 > G_1 > G_2 > \ldots` where
- `G_k = [G, G_{k-1}]`, i.e. every term after the first is equal to the
- commutator of `G` and the previous term in `G1` ([1], p.29).
- Returns
- =======
- A list of permutation groups in the order `G = G_0, G_1, G_2, \ldots`
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import (AlternatingGroup,
- ... DihedralGroup)
- >>> A = AlternatingGroup(4)
- >>> len(A.lower_central_series())
- 2
- >>> A.lower_central_series()[1].is_subgroup(DihedralGroup(2))
- True
- See Also
- ========
- commutator, derived_series
- """
- res = [self]
- current = self
- nxt = self.commutator(self, current)
- while not current.is_subgroup(nxt):
- res.append(nxt)
- current = nxt
- nxt = self.commutator(self, current)
- return res
- @property
- def max_div(self):
- """Maximum proper divisor of the degree of a permutation group.
- Explanation
- ===========
- Obviously, this is the degree divided by its minimal proper divisor
- (larger than ``1``, if one exists). As it is guaranteed to be prime,
- the ``sieve`` from ``sympy.ntheory`` is used.
- This function is also used as an optimization tool for the functions
- ``minimal_block`` and ``_union_find_merge``.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> G = PermutationGroup([Permutation([0, 2, 1, 3])])
- >>> G.max_div
- 2
- See Also
- ========
- minimal_block, _union_find_merge
- """
- if self._max_div is not None:
- return self._max_div
- n = self.degree
- if n == 1:
- return 1
- for x in sieve:
- if n % x == 0:
- d = n//x
- self._max_div = d
- return d
- def minimal_block(self, points):
- r"""For a transitive group, finds the block system generated by
- ``points``.
- Explanation
- ===========
- If a group ``G`` acts on a set ``S``, a nonempty subset ``B`` of ``S``
- is called a block under the action of ``G`` if for all ``g`` in ``G``
- we have ``gB = B`` (``g`` fixes ``B``) or ``gB`` and ``B`` have no
- common points (``g`` moves ``B`` entirely). ([1], p.23; [6]).
- The distinct translates ``gB`` of a block ``B`` for ``g`` in ``G``
- partition the set ``S`` and this set of translates is known as a block
- system. Moreover, we obviously have that all blocks in the partition
- have the same size, hence the block size divides ``|S|`` ([1], p.23).
- A ``G``-congruence is an equivalence relation ``~`` on the set ``S``
- such that ``a ~ b`` implies ``g(a) ~ g(b)`` for all ``g`` in ``G``.
- For a transitive group, the equivalence classes of a ``G``-congruence
- and the blocks of a block system are the same thing ([1], p.23).
- The algorithm below checks the group for transitivity, and then finds
- the ``G``-congruence generated by the pairs ``(p_0, p_1), (p_0, p_2),
- ..., (p_0,p_{k-1})`` which is the same as finding the maximal block
- system (i.e., the one with minimum block size) such that
- ``p_0, ..., p_{k-1}`` are in the same block ([1], p.83).
- It is an implementation of Atkinson's algorithm, as suggested in [1],
- and manipulates an equivalence relation on the set ``S`` using a
- union-find data structure. The running time is just above
- `O(|points||S|)`. ([1], pp. 83-87; [7]).
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import DihedralGroup
- >>> D = DihedralGroup(10)
- >>> D.minimal_block([0, 5])
- [0, 1, 2, 3, 4, 0, 1, 2, 3, 4]
- >>> D.minimal_block([0, 1])
- [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
- See Also
- ========
- _union_find_rep, _union_find_merge, is_transitive, is_primitive
- """
- if not self.is_transitive():
- return False
- n = self.degree
- gens = self.generators
- # initialize the list of equivalence class representatives
- parents = list(range(n))
- ranks = [1]*n
- not_rep = []
- k = len(points)
- # the block size must divide the degree of the group
- if k > self.max_div:
- return [0]*n
- for i in range(k - 1):
- parents[points[i + 1]] = points[0]
- not_rep.append(points[i + 1])
- ranks[points[0]] = k
- i = 0
- len_not_rep = k - 1
- while i < len_not_rep:
- gamma = not_rep[i]
- i += 1
- for gen in gens:
- # find has side effects: performs path compression on the list
- # of representatives
- delta = self._union_find_rep(gamma, parents)
- # union has side effects: performs union by rank on the list
- # of representatives
- temp = self._union_find_merge(gen(gamma), gen(delta), ranks,
- parents, not_rep)
- if temp == -1:
- return [0]*n
- len_not_rep += temp
- for i in range(n):
- # force path compression to get the final state of the equivalence
- # relation
- self._union_find_rep(i, parents)
- # rewrite result so that block representatives are minimal
- new_reps = {}
- return [new_reps.setdefault(r, i) for i, r in enumerate(parents)]
- def conjugacy_class(self, x):
- r"""Return the conjugacy class of an element in the group.
- Explanation
- ===========
- The conjugacy class of an element ``g`` in a group ``G`` is the set of
- elements ``x`` in ``G`` that are conjugate with ``g``, i.e. for which
- ``g = xax^{-1}``
- for some ``a`` in ``G``.
- Note that conjugacy is an equivalence relation, and therefore that
- conjugacy classes are partitions of ``G``. For a list of all the
- conjugacy classes of the group, use the conjugacy_classes() method.
- In a permutation group, each conjugacy class corresponds to a particular
- `cycle structure': for example, in ``S_3``, the conjugacy classes are:
- * the identity class, ``{()}``
- * all transpositions, ``{(1 2), (1 3), (2 3)}``
- * all 3-cycles, ``{(1 2 3), (1 3 2)}``
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, SymmetricGroup
- >>> S3 = SymmetricGroup(3)
- >>> S3.conjugacy_class(Permutation(0, 1, 2))
- {(0 1 2), (0 2 1)}
- Notes
- =====
- This procedure computes the conjugacy class directly by finding the
- orbit of the element under conjugation in G. This algorithm is only
- feasible for permutation groups of relatively small order, but is like
- the orbit() function itself in that respect.
- """
- # Ref: "Computing the conjugacy classes of finite groups"; Butler, G.
- # Groups '93 Galway/St Andrews; edited by Campbell, C. M.
- new_class = {x}
- last_iteration = new_class
- while len(last_iteration) > 0:
- this_iteration = set()
- for y in last_iteration:
- for s in self.generators:
- conjugated = s * y * (~s)
- if conjugated not in new_class:
- this_iteration.add(conjugated)
- new_class.update(last_iteration)
- last_iteration = this_iteration
- return new_class
- def conjugacy_classes(self):
- r"""Return the conjugacy classes of the group.
- Explanation
- ===========
- As described in the documentation for the .conjugacy_class() function,
- conjugacy is an equivalence relation on a group G which partitions the
- set of elements. This method returns a list of all these conjugacy
- classes of G.
- Examples
- ========
- >>> from sympy.combinatorics import SymmetricGroup
- >>> SymmetricGroup(3).conjugacy_classes()
- [{(2)}, {(0 1 2), (0 2 1)}, {(0 2), (1 2), (2)(0 1)}]
- """
- identity = _af_new(list(range(self.degree)))
- known_elements = {identity}
- classes = [known_elements.copy()]
- for x in self.generate():
- if x not in known_elements:
- new_class = self.conjugacy_class(x)
- classes.append(new_class)
- known_elements.update(new_class)
- return classes
- def normal_closure(self, other, k=10):
- r"""Return the normal closure of a subgroup/set of permutations.
- Explanation
- ===========
- If ``S`` is a subset of a group ``G``, the normal closure of ``A`` in ``G``
- is defined as the intersection of all normal subgroups of ``G`` that
- contain ``A`` ([1], p.14). Alternatively, it is the group generated by
- the conjugates ``x^{-1}yx`` for ``x`` a generator of ``G`` and ``y`` a
- generator of the subgroup ``\left\langle S\right\rangle`` generated by
- ``S`` (for some chosen generating set for ``\left\langle S\right\rangle``)
- ([1], p.73).
- Parameters
- ==========
- other
- a subgroup/list of permutations/single permutation
- k
- an implementation-specific parameter that determines the number
- of conjugates that are adjoined to ``other`` at once
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import (SymmetricGroup,
- ... CyclicGroup, AlternatingGroup)
- >>> S = SymmetricGroup(5)
- >>> C = CyclicGroup(5)
- >>> G = S.normal_closure(C)
- >>> G.order()
- 60
- >>> G.is_subgroup(AlternatingGroup(5))
- True
- See Also
- ========
- commutator, derived_subgroup, random_pr
- Notes
- =====
- The algorithm is described in [1], pp. 73-74; it makes use of the
- generation of random elements for permutation groups by the product
- replacement algorithm.
- """
- if hasattr(other, 'generators'):
- degree = self.degree
- identity = _af_new(list(range(degree)))
- if all(g == identity for g in other.generators):
- return other
- Z = PermutationGroup(other.generators[:])
- base, strong_gens = Z.schreier_sims_incremental()
- strong_gens_distr = _distribute_gens_by_base(base, strong_gens)
- basic_orbits, basic_transversals = \
- _orbits_transversals_from_bsgs(base, strong_gens_distr)
- self._random_pr_init(r=10, n=20)
- _loop = True
- while _loop:
- Z._random_pr_init(r=10, n=10)
- for _ in range(k):
- g = self.random_pr()
- h = Z.random_pr()
- conj = h^g
- res = _strip(conj, base, basic_orbits, basic_transversals)
- if res[0] != identity or res[1] != len(base) + 1:
- gens = Z.generators
- gens.append(conj)
- Z = PermutationGroup(gens)
- strong_gens.append(conj)
- temp_base, temp_strong_gens = \
- Z.schreier_sims_incremental(base, strong_gens)
- base, strong_gens = temp_base, temp_strong_gens
- strong_gens_distr = \
- _distribute_gens_by_base(base, strong_gens)
- basic_orbits, basic_transversals = \
- _orbits_transversals_from_bsgs(base,
- strong_gens_distr)
- _loop = False
- for g in self.generators:
- for h in Z.generators:
- conj = h^g
- res = _strip(conj, base, basic_orbits,
- basic_transversals)
- if res[0] != identity or res[1] != len(base) + 1:
- _loop = True
- break
- if _loop:
- break
- return Z
- elif hasattr(other, '__getitem__'):
- return self.normal_closure(PermutationGroup(other))
- elif hasattr(other, 'array_form'):
- return self.normal_closure(PermutationGroup([other]))
- def orbit(self, alpha, action='tuples'):
- r"""Compute the orbit of alpha `\{g(\alpha) | g \in G\}` as a set.
- Explanation
- ===========
- The time complexity of the algorithm used here is `O(|Orb|*r)` where
- `|Orb|` is the size of the orbit and ``r`` is the number of generators of
- the group. For a more detailed analysis, see [1], p.78, [2], pp. 19-21.
- Here alpha can be a single point, or a list of points.
- If alpha is a single point, the ordinary orbit is computed.
- if alpha is a list of points, there are three available options:
- 'union' - computes the union of the orbits of the points in the list
- 'tuples' - computes the orbit of the list interpreted as an ordered
- tuple under the group action ( i.e., g((1,2,3)) = (g(1), g(2), g(3)) )
- 'sets' - computes the orbit of the list interpreted as a sets
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation([1, 2, 0, 4, 5, 6, 3])
- >>> G = PermutationGroup([a])
- >>> G.orbit(0)
- {0, 1, 2}
- >>> G.orbit([0, 4], 'union')
- {0, 1, 2, 3, 4, 5, 6}
- See Also
- ========
- orbit_transversal
- """
- return _orbit(self.degree, self.generators, alpha, action)
- def orbit_rep(self, alpha, beta, schreier_vector=None):
- """Return a group element which sends ``alpha`` to ``beta``.
- Explanation
- ===========
- If ``beta`` is not in the orbit of ``alpha``, the function returns
- ``False``. This implementation makes use of the schreier vector.
- For a proof of correctness, see [1], p.80
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import AlternatingGroup
- >>> G = AlternatingGroup(5)
- >>> G.orbit_rep(0, 4)
- (0 4 1 2 3)
- See Also
- ========
- schreier_vector
- """
- if schreier_vector is None:
- schreier_vector = self.schreier_vector(alpha)
- if schreier_vector[beta] is None:
- return False
- k = schreier_vector[beta]
- gens = [x._array_form for x in self.generators]
- a = []
- while k != -1:
- a.append(gens[k])
- beta = gens[k].index(beta) # beta = (~gens[k])(beta)
- k = schreier_vector[beta]
- if a:
- return _af_new(_af_rmuln(*a))
- else:
- return _af_new(list(range(self._degree)))
- def orbit_transversal(self, alpha, pairs=False):
- r"""Computes a transversal for the orbit of ``alpha`` as a set.
- Explanation
- ===========
- For a permutation group `G`, a transversal for the orbit
- `Orb = \{g(\alpha) | g \in G\}` is a set
- `\{g_\beta | g_\beta(\alpha) = \beta\}` for `\beta \in Orb`.
- Note that there may be more than one possible transversal.
- If ``pairs`` is set to ``True``, it returns the list of pairs
- `(\beta, g_\beta)`. For a proof of correctness, see [1], p.79
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import DihedralGroup
- >>> G = DihedralGroup(6)
- >>> G.orbit_transversal(0)
- [(5), (0 1 2 3 4 5), (0 5)(1 4)(2 3), (0 2 4)(1 3 5), (5)(0 4)(1 3), (0 3)(1 4)(2 5)]
- See Also
- ========
- orbit
- """
- return _orbit_transversal(self._degree, self.generators, alpha, pairs)
- def orbits(self, rep=False):
- """Return the orbits of ``self``, ordered according to lowest element
- in each orbit.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation(1, 5)(2, 3)(4, 0, 6)
- >>> b = Permutation(1, 5)(3, 4)(2, 6, 0)
- >>> G = PermutationGroup([a, b])
- >>> G.orbits()
- [{0, 2, 3, 4, 6}, {1, 5}]
- """
- return _orbits(self._degree, self._generators)
- def order(self):
- """Return the order of the group: the number of permutations that
- can be generated from elements of the group.
- The number of permutations comprising the group is given by
- ``len(group)``; the length of each permutation in the group is
- given by ``group.size``.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation([1, 0, 2])
- >>> G = PermutationGroup([a])
- >>> G.degree
- 3
- >>> len(G)
- 1
- >>> G.order()
- 2
- >>> list(G.generate())
- [(2), (2)(0 1)]
- >>> a = Permutation([0, 2, 1])
- >>> b = Permutation([1, 0, 2])
- >>> G = PermutationGroup([a, b])
- >>> G.order()
- 6
- See Also
- ========
- degree
- """
- if self._order is not None:
- return self._order
- if self._is_sym:
- n = self._degree
- self._order = factorial(n)
- return self._order
- if self._is_alt:
- n = self._degree
- self._order = factorial(n)/2
- return self._order
- m = prod([len(x) for x in self.basic_transversals])
- self._order = m
- return m
- def index(self, H):
- """
- Returns the index of a permutation group.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation(1,2,3)
- >>> b =Permutation(3)
- >>> G = PermutationGroup([a])
- >>> H = PermutationGroup([b])
- >>> G.index(H)
- 3
- """
- if H.is_subgroup(self):
- return self.order()//H.order()
- @property
- def is_symmetric(self):
- """Return ``True`` if the group is symmetric.
- Examples
- ========
- >>> from sympy.combinatorics import SymmetricGroup
- >>> g = SymmetricGroup(5)
- >>> g.is_symmetric
- True
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> g = PermutationGroup(
- ... Permutation(0, 1, 2, 3, 4),
- ... Permutation(2, 3))
- >>> g.is_symmetric
- True
- Notes
- =====
- This uses a naive test involving the computation of the full
- group order.
- If you need more quicker taxonomy for large groups, you can use
- :meth:`PermutationGroup.is_alt_sym`.
- However, :meth:`PermutationGroup.is_alt_sym` may not be accurate
- and is not able to distinguish between an alternating group and
- a symmetric group.
- See Also
- ========
- is_alt_sym
- """
- _is_sym = self._is_sym
- if _is_sym is not None:
- return _is_sym
- n = self.degree
- if n >= 8:
- if self.is_transitive():
- _is_alt_sym = self._eval_is_alt_sym_monte_carlo()
- if _is_alt_sym:
- if any(g.is_odd for g in self.generators):
- self._is_sym, self._is_alt = True, False
- return True
- self._is_sym, self._is_alt = False, True
- return False
- return self._eval_is_alt_sym_naive(only_sym=True)
- self._is_sym, self._is_alt = False, False
- return False
- return self._eval_is_alt_sym_naive(only_sym=True)
- @property
- def is_alternating(self):
- """Return ``True`` if the group is alternating.
- Examples
- ========
- >>> from sympy.combinatorics import AlternatingGroup
- >>> g = AlternatingGroup(5)
- >>> g.is_alternating
- True
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> g = PermutationGroup(
- ... Permutation(0, 1, 2, 3, 4),
- ... Permutation(2, 3, 4))
- >>> g.is_alternating
- True
- Notes
- =====
- This uses a naive test involving the computation of the full
- group order.
- If you need more quicker taxonomy for large groups, you can use
- :meth:`PermutationGroup.is_alt_sym`.
- However, :meth:`PermutationGroup.is_alt_sym` may not be accurate
- and is not able to distinguish between an alternating group and
- a symmetric group.
- See Also
- ========
- is_alt_sym
- """
- _is_alt = self._is_alt
- if _is_alt is not None:
- return _is_alt
- n = self.degree
- if n >= 8:
- if self.is_transitive():
- _is_alt_sym = self._eval_is_alt_sym_monte_carlo()
- if _is_alt_sym:
- if all(g.is_even for g in self.generators):
- self._is_sym, self._is_alt = False, True
- return True
- self._is_sym, self._is_alt = True, False
- return False
- return self._eval_is_alt_sym_naive(only_alt=True)
- self._is_sym, self._is_alt = False, False
- return False
- return self._eval_is_alt_sym_naive(only_alt=True)
- @classmethod
- def _distinct_primes_lemma(cls, primes):
- """Subroutine to test if there is only one cyclic group for the
- order."""
- primes = sorted(primes)
- l = len(primes)
- for i in range(l):
- for j in range(i+1, l):
- if primes[j] % primes[i] == 1:
- return None
- return True
- @property
- def is_cyclic(self):
- r"""
- Return ``True`` if the group is Cyclic.
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import AbelianGroup
- >>> G = AbelianGroup(3, 4)
- >>> G.is_cyclic
- True
- >>> G = AbelianGroup(4, 4)
- >>> G.is_cyclic
- False
- Notes
- =====
- If the order of a group $n$ can be factored into the distinct
- primes $p_1, p_2, \dots , p_s$ and if
- .. math::
- \forall i, j \in \{1, 2, \dots, s \}:
- p_i \not \equiv 1 \pmod {p_j}
- holds true, there is only one group of the order $n$ which
- is a cyclic group [1]_. This is a generalization of the lemma
- that the group of order $15, 35, \dots$ are cyclic.
- And also, these additional lemmas can be used to test if a
- group is cyclic if the order of the group is already found.
- - If the group is abelian and the order of the group is
- square-free, the group is cyclic.
- - If the order of the group is less than $6$ and is not $4$, the
- group is cyclic.
- - If the order of the group is prime, the group is cyclic.
- References
- ==========
- .. [1] 1978: John S. Rose: A Course on Group Theory,
- Introduction to Finite Group Theory: 1.4
- """
- if self._is_cyclic is not None:
- return self._is_cyclic
- if len(self.generators) == 1:
- self._is_cyclic = True
- self._is_abelian = True
- return True
- if self._is_abelian is False:
- self._is_cyclic = False
- return False
- order = self.order()
- if order < 6:
- self._is_abelian = True
- if order != 4:
- self._is_cyclic = True
- return True
- factors = factorint(order)
- if all(v == 1 for v in factors.values()):
- if self._is_abelian:
- self._is_cyclic = True
- return True
- primes = list(factors.keys())
- if PermutationGroup._distinct_primes_lemma(primes) is True:
- self._is_cyclic = True
- self._is_abelian = True
- return True
- if not self.is_abelian:
- self._is_cyclic = False
- return False
- self._is_cyclic = all(
- any(g**(order//p) != self.identity for g in self.generators)
- for p, e in factors.items() if e > 1
- )
- return self._is_cyclic
- @property
- def is_dihedral(self):
- r"""
- Return ``True`` if the group is dihedral.
- Examples
- ========
- >>> from sympy.combinatorics.perm_groups import PermutationGroup
- >>> from sympy.combinatorics.permutations import Permutation
- >>> from sympy.combinatorics.named_groups import SymmetricGroup, CyclicGroup
- >>> G = PermutationGroup(Permutation(1, 6)(2, 5)(3, 4), Permutation(0, 1, 2, 3, 4, 5, 6))
- >>> G.is_dihedral
- True
- >>> G = SymmetricGroup(3)
- >>> G.is_dihedral
- True
- >>> G = CyclicGroup(6)
- >>> G.is_dihedral
- False
- References
- ==========
- .. [Di1] https://math.stackexchange.com/a/827273
- .. [Di2] https://kconrad.math.uconn.edu/blurbs/grouptheory/dihedral.pdf
- .. [Di3] https://kconrad.math.uconn.edu/blurbs/grouptheory/dihedral2.pdf
- .. [Di4] https://en.wikipedia.org/wiki/Dihedral_group
- """
- if self._is_dihedral is not None:
- return self._is_dihedral
- order = self.order()
- if order % 2 == 1:
- self._is_dihedral = False
- return False
- if order == 2:
- self._is_dihedral = True
- return True
- if order == 4:
- # The dihedral group of order 4 is the Klein 4-group.
- self._is_dihedral = not self.is_cyclic
- return self._is_dihedral
- if self.is_abelian:
- # The only abelian dihedral groups are the ones of orders 2 and 4.
- self._is_dihedral = False
- return False
- # Now we know the group is of even order >= 6, and nonabelian.
- n = order // 2
- # Handle special cases where there are exactly two generators.
- gens = self.generators
- if len(gens) == 2:
- x, y = gens
- a, b = x.order(), y.order()
- # Make a >= b
- if a < b:
- x, y, a, b = y, x, b, a
- # Using Theorem 2.1 of [Di3]:
- if a == 2 == b:
- self._is_dihedral = True
- return True
- # Using Theorem 1.1 of [Di3]:
- if a == n and b == 2 and y*x*y == ~x:
- self._is_dihedral = True
- return True
- # Proceed with algorithm of [Di1]
- # Find elements of orders 2 and n
- order_2, order_n = [], []
- for p in self.elements:
- k = p.order()
- if k == 2:
- order_2.append(p)
- elif k == n:
- order_n.append(p)
- if len(order_2) != n + 1 - (n % 2):
- self._is_dihedral = False
- return False
- if not order_n:
- self._is_dihedral = False
- return False
- x = order_n[0]
- # Want an element y of order 2 that is not a power of x
- # (i.e. that is not the 180-deg rotation, when n is even).
- y = order_2[0]
- if n % 2 == 0 and y == x**(n//2):
- y = order_2[1]
- self._is_dihedral = (y*x*y == ~x)
- return self._is_dihedral
- def pointwise_stabilizer(self, points, incremental=True):
- r"""Return the pointwise stabilizer for a set of points.
- Explanation
- ===========
- For a permutation group `G` and a set of points
- `\{p_1, p_2,\ldots, p_k\}`, the pointwise stabilizer of
- `p_1, p_2, \ldots, p_k` is defined as
- `G_{p_1,\ldots, p_k} =
- \{g\in G | g(p_i) = p_i \forall i\in\{1, 2,\ldots,k\}\}` ([1],p20).
- It is a subgroup of `G`.
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import SymmetricGroup
- >>> S = SymmetricGroup(7)
- >>> Stab = S.pointwise_stabilizer([2, 3, 5])
- >>> Stab.is_subgroup(S.stabilizer(2).stabilizer(3).stabilizer(5))
- True
- See Also
- ========
- stabilizer, schreier_sims_incremental
- Notes
- =====
- When incremental == True,
- rather than the obvious implementation using successive calls to
- ``.stabilizer()``, this uses the incremental Schreier-Sims algorithm
- to obtain a base with starting segment - the given points.
- """
- if incremental:
- base, strong_gens = self.schreier_sims_incremental(base=points)
- stab_gens = []
- degree = self.degree
- for gen in strong_gens:
- if [gen(point) for point in points] == points:
- stab_gens.append(gen)
- if not stab_gens:
- stab_gens = _af_new(list(range(degree)))
- return PermutationGroup(stab_gens)
- else:
- gens = self._generators
- degree = self.degree
- for x in points:
- gens = _stabilizer(degree, gens, x)
- return PermutationGroup(gens)
- def make_perm(self, n, seed=None):
- """
- Multiply ``n`` randomly selected permutations from
- pgroup together, starting with the identity
- permutation. If ``n`` is a list of integers, those
- integers will be used to select the permutations and they
- will be applied in L to R order: make_perm((A, B, C)) will
- give CBA(I) where I is the identity permutation.
- ``seed`` is used to set the seed for the random selection
- of permutations from pgroup. If this is a list of integers,
- the corresponding permutations from pgroup will be selected
- in the order give. This is mainly used for testing purposes.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a, b = [Permutation([1, 0, 3, 2]), Permutation([1, 3, 0, 2])]
- >>> G = PermutationGroup([a, b])
- >>> G.make_perm(1, [0])
- (0 1)(2 3)
- >>> G.make_perm(3, [0, 1, 0])
- (0 2 3 1)
- >>> G.make_perm([0, 1, 0])
- (0 2 3 1)
- See Also
- ========
- random
- """
- if is_sequence(n):
- if seed is not None:
- raise ValueError('If n is a sequence, seed should be None')
- n, seed = len(n), n
- else:
- try:
- n = int(n)
- except TypeError:
- raise ValueError('n must be an integer or a sequence.')
- randomrange = _randrange(seed)
- # start with the identity permutation
- result = Permutation(list(range(self.degree)))
- m = len(self)
- for _ in range(n):
- p = self[randomrange(m)]
- result = rmul(result, p)
- return result
- def random(self, af=False):
- """Return a random group element
- """
- rank = randrange(self.order())
- return self.coset_unrank(rank, af)
- def random_pr(self, gen_count=11, iterations=50, _random_prec=None):
- """Return a random group element using product replacement.
- Explanation
- ===========
- For the details of the product replacement algorithm, see
- ``_random_pr_init`` In ``random_pr`` the actual 'product replacement'
- is performed. Notice that if the attribute ``_random_gens``
- is empty, it needs to be initialized by ``_random_pr_init``.
- See Also
- ========
- _random_pr_init
- """
- if self._random_gens == []:
- self._random_pr_init(gen_count, iterations)
- random_gens = self._random_gens
- r = len(random_gens) - 1
- # handle randomized input for testing purposes
- if _random_prec is None:
- s = randrange(r)
- t = randrange(r - 1)
- if t == s:
- t = r - 1
- x = choice([1, 2])
- e = choice([-1, 1])
- else:
- s = _random_prec['s']
- t = _random_prec['t']
- if t == s:
- t = r - 1
- x = _random_prec['x']
- e = _random_prec['e']
- if x == 1:
- random_gens[s] = _af_rmul(random_gens[s], _af_pow(random_gens[t], e))
- random_gens[r] = _af_rmul(random_gens[r], random_gens[s])
- else:
- random_gens[s] = _af_rmul(_af_pow(random_gens[t], e), random_gens[s])
- random_gens[r] = _af_rmul(random_gens[s], random_gens[r])
- return _af_new(random_gens[r])
- def random_stab(self, alpha, schreier_vector=None, _random_prec=None):
- """Random element from the stabilizer of ``alpha``.
- The schreier vector for ``alpha`` is an optional argument used
- for speeding up repeated calls. The algorithm is described in [1], p.81
- See Also
- ========
- random_pr, orbit_rep
- """
- if schreier_vector is None:
- schreier_vector = self.schreier_vector(alpha)
- if _random_prec is None:
- rand = self.random_pr()
- else:
- rand = _random_prec['rand']
- beta = rand(alpha)
- h = self.orbit_rep(alpha, beta, schreier_vector)
- return rmul(~h, rand)
- def schreier_sims(self):
- """Schreier-Sims algorithm.
- Explanation
- ===========
- It computes the generators of the chain of stabilizers
- `G > G_{b_1} > .. > G_{b1,..,b_r} > 1`
- in which `G_{b_1,..,b_i}` stabilizes `b_1,..,b_i`,
- and the corresponding ``s`` cosets.
- An element of the group can be written as the product
- `h_1*..*h_s`.
- We use the incremental Schreier-Sims algorithm.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation([0, 2, 1])
- >>> b = Permutation([1, 0, 2])
- >>> G = PermutationGroup([a, b])
- >>> G.schreier_sims()
- >>> G.basic_transversals
- [{0: (2)(0 1), 1: (2), 2: (1 2)},
- {0: (2), 2: (0 2)}]
- """
- if self._transversals:
- return
- self._schreier_sims()
- return
- def _schreier_sims(self, base=None):
- schreier = self.schreier_sims_incremental(base=base, slp_dict=True)
- base, strong_gens = schreier[:2]
- self._base = base
- self._strong_gens = strong_gens
- self._strong_gens_slp = schreier[2]
- if not base:
- self._transversals = []
- self._basic_orbits = []
- return
- strong_gens_distr = _distribute_gens_by_base(base, strong_gens)
- basic_orbits, transversals, slps = _orbits_transversals_from_bsgs(base,\
- strong_gens_distr, slp=True)
- # rewrite the indices stored in slps in terms of strong_gens
- for i, slp in enumerate(slps):
- gens = strong_gens_distr[i]
- for k in slp:
- slp[k] = [strong_gens.index(gens[s]) for s in slp[k]]
- self._transversals = transversals
- self._basic_orbits = [sorted(x) for x in basic_orbits]
- self._transversal_slp = slps
- def schreier_sims_incremental(self, base=None, gens=None, slp_dict=False):
- """Extend a sequence of points and generating set to a base and strong
- generating set.
- Parameters
- ==========
- base
- The sequence of points to be extended to a base. Optional
- parameter with default value ``[]``.
- gens
- The generating set to be extended to a strong generating set
- relative to the base obtained. Optional parameter with default
- value ``self.generators``.
- slp_dict
- If `True`, return a dictionary `{g: gens}` for each strong
- generator `g` where `gens` is a list of strong generators
- coming before `g` in `strong_gens`, such that the product
- of the elements of `gens` is equal to `g`.
- Returns
- =======
- (base, strong_gens)
- ``base`` is the base obtained, and ``strong_gens`` is the strong
- generating set relative to it. The original parameters ``base``,
- ``gens`` remain unchanged.
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import AlternatingGroup
- >>> from sympy.combinatorics.testutil import _verify_bsgs
- >>> A = AlternatingGroup(7)
- >>> base = [2, 3]
- >>> seq = [2, 3]
- >>> base, strong_gens = A.schreier_sims_incremental(base=seq)
- >>> _verify_bsgs(A, base, strong_gens)
- True
- >>> base[:2]
- [2, 3]
- Notes
- =====
- This version of the Schreier-Sims algorithm runs in polynomial time.
- There are certain assumptions in the implementation - if the trivial
- group is provided, ``base`` and ``gens`` are returned immediately,
- as any sequence of points is a base for the trivial group. If the
- identity is present in the generators ``gens``, it is removed as
- it is a redundant generator.
- The implementation is described in [1], pp. 90-93.
- See Also
- ========
- schreier_sims, schreier_sims_random
- """
- if base is None:
- base = []
- if gens is None:
- gens = self.generators[:]
- degree = self.degree
- id_af = list(range(degree))
- # handle the trivial group
- if len(gens) == 1 and gens[0].is_Identity:
- if slp_dict:
- return base, gens, {gens[0]: [gens[0]]}
- return base, gens
- # prevent side effects
- _base, _gens = base[:], gens[:]
- # remove the identity as a generator
- _gens = [x for x in _gens if not x.is_Identity]
- # make sure no generator fixes all base points
- for gen in _gens:
- if all(x == gen._array_form[x] for x in _base):
- for new in id_af:
- if gen._array_form[new] != new:
- break
- else:
- assert None # can this ever happen?
- _base.append(new)
- # distribute generators according to basic stabilizers
- strong_gens_distr = _distribute_gens_by_base(_base, _gens)
- strong_gens_slp = []
- # initialize the basic stabilizers, basic orbits and basic transversals
- orbs = {}
- transversals = {}
- slps = {}
- base_len = len(_base)
- for i in range(base_len):
- transversals[i], slps[i] = _orbit_transversal(degree, strong_gens_distr[i],
- _base[i], pairs=True, af=True, slp=True)
- transversals[i] = dict(transversals[i])
- orbs[i] = list(transversals[i].keys())
- # main loop: amend the stabilizer chain until we have generators
- # for all stabilizers
- i = base_len - 1
- while i >= 0:
- # this flag is used to continue with the main loop from inside
- # a nested loop
- continue_i = False
- # test the generators for being a strong generating set
- db = {}
- for beta, u_beta in list(transversals[i].items()):
- for j, gen in enumerate(strong_gens_distr[i]):
- gb = gen._array_form[beta]
- u1 = transversals[i][gb]
- g1 = _af_rmul(gen._array_form, u_beta)
- slp = [(i, g) for g in slps[i][beta]]
- slp = [(i, j)] + slp
- if g1 != u1:
- # test if the schreier generator is in the i+1-th
- # would-be basic stabilizer
- y = True
- try:
- u1_inv = db[gb]
- except KeyError:
- u1_inv = db[gb] = _af_invert(u1)
- schreier_gen = _af_rmul(u1_inv, g1)
- u1_inv_slp = slps[i][gb][:]
- u1_inv_slp.reverse()
- u1_inv_slp = [(i, (g,)) for g in u1_inv_slp]
- slp = u1_inv_slp + slp
- h, j, slp = _strip_af(schreier_gen, _base, orbs, transversals, i, slp=slp, slps=slps)
- if j <= base_len:
- # new strong generator h at level j
- y = False
- elif h:
- # h fixes all base points
- y = False
- moved = 0
- while h[moved] == moved:
- moved += 1
- _base.append(moved)
- base_len += 1
- strong_gens_distr.append([])
- if y is False:
- # if a new strong generator is found, update the
- # data structures and start over
- h = _af_new(h)
- strong_gens_slp.append((h, slp))
- for l in range(i + 1, j):
- strong_gens_distr[l].append(h)
- transversals[l], slps[l] =\
- _orbit_transversal(degree, strong_gens_distr[l],
- _base[l], pairs=True, af=True, slp=True)
- transversals[l] = dict(transversals[l])
- orbs[l] = list(transversals[l].keys())
- i = j - 1
- # continue main loop using the flag
- continue_i = True
- if continue_i is True:
- break
- if continue_i is True:
- break
- if continue_i is True:
- continue
- i -= 1
- strong_gens = _gens[:]
- if slp_dict:
- # create the list of the strong generators strong_gens and
- # rewrite the indices of strong_gens_slp in terms of the
- # elements of strong_gens
- for k, slp in strong_gens_slp:
- strong_gens.append(k)
- for i in range(len(slp)):
- s = slp[i]
- if isinstance(s[1], tuple):
- slp[i] = strong_gens_distr[s[0]][s[1][0]]**-1
- else:
- slp[i] = strong_gens_distr[s[0]][s[1]]
- strong_gens_slp = dict(strong_gens_slp)
- # add the original generators
- for g in _gens:
- strong_gens_slp[g] = [g]
- return (_base, strong_gens, strong_gens_slp)
- strong_gens.extend([k for k, _ in strong_gens_slp])
- return _base, strong_gens
- def schreier_sims_random(self, base=None, gens=None, consec_succ=10,
- _random_prec=None):
- r"""Randomized Schreier-Sims algorithm.
- Explanation
- ===========
- The randomized Schreier-Sims algorithm takes the sequence ``base``
- and the generating set ``gens``, and extends ``base`` to a base, and
- ``gens`` to a strong generating set relative to that base with
- probability of a wrong answer at most `2^{-consec\_succ}`,
- provided the random generators are sufficiently random.
- Parameters
- ==========
- base
- The sequence to be extended to a base.
- gens
- The generating set to be extended to a strong generating set.
- consec_succ
- The parameter defining the probability of a wrong answer.
- _random_prec
- An internal parameter used for testing purposes.
- Returns
- =======
- (base, strong_gens)
- ``base`` is the base and ``strong_gens`` is the strong generating
- set relative to it.
- Examples
- ========
- >>> from sympy.combinatorics.testutil import _verify_bsgs
- >>> from sympy.combinatorics.named_groups import SymmetricGroup
- >>> S = SymmetricGroup(5)
- >>> base, strong_gens = S.schreier_sims_random(consec_succ=5)
- >>> _verify_bsgs(S, base, strong_gens) #doctest: +SKIP
- True
- Notes
- =====
- The algorithm is described in detail in [1], pp. 97-98. It extends
- the orbits ``orbs`` and the permutation groups ``stabs`` to
- basic orbits and basic stabilizers for the base and strong generating
- set produced in the end.
- The idea of the extension process
- is to "sift" random group elements through the stabilizer chain
- and amend the stabilizers/orbits along the way when a sift
- is not successful.
- The helper function ``_strip`` is used to attempt
- to decompose a random group element according to the current
- state of the stabilizer chain and report whether the element was
- fully decomposed (successful sift) or not (unsuccessful sift). In
- the latter case, the level at which the sift failed is reported and
- used to amend ``stabs``, ``base``, ``gens`` and ``orbs`` accordingly.
- The halting condition is for ``consec_succ`` consecutive successful
- sifts to pass. This makes sure that the current ``base`` and ``gens``
- form a BSGS with probability at least `1 - 1/\text{consec\_succ}`.
- See Also
- ========
- schreier_sims
- """
- if base is None:
- base = []
- if gens is None:
- gens = self.generators
- base_len = len(base)
- n = self.degree
- # make sure no generator fixes all base points
- for gen in gens:
- if all(gen(x) == x for x in base):
- new = 0
- while gen._array_form[new] == new:
- new += 1
- base.append(new)
- base_len += 1
- # distribute generators according to basic stabilizers
- strong_gens_distr = _distribute_gens_by_base(base, gens)
- # initialize the basic stabilizers, basic transversals and basic orbits
- transversals = {}
- orbs = {}
- for i in range(base_len):
- transversals[i] = dict(_orbit_transversal(n, strong_gens_distr[i],
- base[i], pairs=True))
- orbs[i] = list(transversals[i].keys())
- # initialize the number of consecutive elements sifted
- c = 0
- # start sifting random elements while the number of consecutive sifts
- # is less than consec_succ
- while c < consec_succ:
- if _random_prec is None:
- g = self.random_pr()
- else:
- g = _random_prec['g'].pop()
- h, j = _strip(g, base, orbs, transversals)
- y = True
- # determine whether a new base point is needed
- if j <= base_len:
- y = False
- elif not h.is_Identity:
- y = False
- moved = 0
- while h(moved) == moved:
- moved += 1
- base.append(moved)
- base_len += 1
- strong_gens_distr.append([])
- # if the element doesn't sift, amend the strong generators and
- # associated stabilizers and orbits
- if y is False:
- for l in range(1, j):
- strong_gens_distr[l].append(h)
- transversals[l] = dict(_orbit_transversal(n,
- strong_gens_distr[l], base[l], pairs=True))
- orbs[l] = list(transversals[l].keys())
- c = 0
- else:
- c += 1
- # build the strong generating set
- strong_gens = strong_gens_distr[0][:]
- for gen in strong_gens_distr[1]:
- if gen not in strong_gens:
- strong_gens.append(gen)
- return base, strong_gens
- def schreier_vector(self, alpha):
- """Computes the schreier vector for ``alpha``.
- Explanation
- ===========
- The Schreier vector efficiently stores information
- about the orbit of ``alpha``. It can later be used to quickly obtain
- elements of the group that send ``alpha`` to a particular element
- in the orbit. Notice that the Schreier vector depends on the order
- in which the group generators are listed. For a definition, see [3].
- Since list indices start from zero, we adopt the convention to use
- "None" instead of 0 to signify that an element does not belong
- to the orbit.
- For the algorithm and its correctness, see [2], pp.78-80.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation([2, 4, 6, 3, 1, 5, 0])
- >>> b = Permutation([0, 1, 3, 5, 4, 6, 2])
- >>> G = PermutationGroup([a, b])
- >>> G.schreier_vector(0)
- [-1, None, 0, 1, None, 1, 0]
- See Also
- ========
- orbit
- """
- n = self.degree
- v = [None]*n
- v[alpha] = -1
- orb = [alpha]
- used = [False]*n
- used[alpha] = True
- gens = self.generators
- r = len(gens)
- for b in orb:
- for i in range(r):
- temp = gens[i]._array_form[b]
- if used[temp] is False:
- orb.append(temp)
- used[temp] = True
- v[temp] = i
- return v
- def stabilizer(self, alpha):
- r"""Return the stabilizer subgroup of ``alpha``.
- Explanation
- ===========
- The stabilizer of `\alpha` is the group `G_\alpha =
- \{g \in G | g(\alpha) = \alpha\}`.
- For a proof of correctness, see [1], p.79.
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import DihedralGroup
- >>> G = DihedralGroup(6)
- >>> G.stabilizer(5)
- PermutationGroup([
- (5)(0 4)(1 3)])
- See Also
- ========
- orbit
- """
- return PermGroup(_stabilizer(self._degree, self._generators, alpha))
- @property
- def strong_gens(self):
- r"""Return a strong generating set from the Schreier-Sims algorithm.
- Explanation
- ===========
- A generating set `S = \{g_1, g_2, \dots, g_t\}` for a permutation group
- `G` is a strong generating set relative to the sequence of points
- (referred to as a "base") `(b_1, b_2, \dots, b_k)` if, for
- `1 \leq i \leq k` we have that the intersection of the pointwise
- stabilizer `G^{(i+1)} := G_{b_1, b_2, \dots, b_i}` with `S` generates
- the pointwise stabilizer `G^{(i+1)}`. The concepts of a base and
- strong generating set and their applications are discussed in depth
- in [1], pp. 87-89 and [2], pp. 55-57.
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import DihedralGroup
- >>> D = DihedralGroup(4)
- >>> D.strong_gens
- [(0 1 2 3), (0 3)(1 2), (1 3)]
- >>> D.base
- [0, 1]
- See Also
- ========
- base, basic_transversals, basic_orbits, basic_stabilizers
- """
- if self._strong_gens == []:
- self.schreier_sims()
- return self._strong_gens
- def subgroup(self, gens):
- """
- Return the subgroup generated by `gens` which is a list of
- elements of the group
- """
- if not all(g in self for g in gens):
- raise ValueError("The group does not contain the supplied generators")
- G = PermutationGroup(gens)
- return G
- def subgroup_search(self, prop, base=None, strong_gens=None, tests=None,
- init_subgroup=None):
- """Find the subgroup of all elements satisfying the property ``prop``.
- Explanation
- ===========
- This is done by a depth-first search with respect to base images that
- uses several tests to prune the search tree.
- Parameters
- ==========
- prop
- The property to be used. Has to be callable on group elements
- and always return ``True`` or ``False``. It is assumed that
- all group elements satisfying ``prop`` indeed form a subgroup.
- base
- A base for the supergroup.
- strong_gens
- A strong generating set for the supergroup.
- tests
- A list of callables of length equal to the length of ``base``.
- These are used to rule out group elements by partial base images,
- so that ``tests[l](g)`` returns False if the element ``g`` is known
- not to satisfy prop base on where g sends the first ``l + 1`` base
- points.
- init_subgroup
- if a subgroup of the sought group is
- known in advance, it can be passed to the function as this
- parameter.
- Returns
- =======
- res
- The subgroup of all elements satisfying ``prop``. The generating
- set for this group is guaranteed to be a strong generating set
- relative to the base ``base``.
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import (SymmetricGroup,
- ... AlternatingGroup)
- >>> from sympy.combinatorics.testutil import _verify_bsgs
- >>> S = SymmetricGroup(7)
- >>> prop_even = lambda x: x.is_even
- >>> base, strong_gens = S.schreier_sims_incremental()
- >>> G = S.subgroup_search(prop_even, base=base, strong_gens=strong_gens)
- >>> G.is_subgroup(AlternatingGroup(7))
- True
- >>> _verify_bsgs(G, base, G.generators)
- True
- Notes
- =====
- This function is extremely lengthy and complicated and will require
- some careful attention. The implementation is described in
- [1], pp. 114-117, and the comments for the code here follow the lines
- of the pseudocode in the book for clarity.
- The complexity is exponential in general, since the search process by
- itself visits all members of the supergroup. However, there are a lot
- of tests which are used to prune the search tree, and users can define
- their own tests via the ``tests`` parameter, so in practice, and for
- some computations, it's not terrible.
- A crucial part in the procedure is the frequent base change performed
- (this is line 11 in the pseudocode) in order to obtain a new basic
- stabilizer. The book mentiones that this can be done by using
- ``.baseswap(...)``, however the current implementation uses a more
- straightforward way to find the next basic stabilizer - calling the
- function ``.stabilizer(...)`` on the previous basic stabilizer.
- """
- # initialize BSGS and basic group properties
- def get_reps(orbits):
- # get the minimal element in the base ordering
- return [min(orbit, key = lambda x: base_ordering[x]) \
- for orbit in orbits]
- def update_nu(l):
- temp_index = len(basic_orbits[l]) + 1 -\
- len(res_basic_orbits_init_base[l])
- # this corresponds to the element larger than all points
- if temp_index >= len(sorted_orbits[l]):
- nu[l] = base_ordering[degree]
- else:
- nu[l] = sorted_orbits[l][temp_index]
- if base is None:
- base, strong_gens = self.schreier_sims_incremental()
- base_len = len(base)
- degree = self.degree
- identity = _af_new(list(range(degree)))
- base_ordering = _base_ordering(base, degree)
- # add an element larger than all points
- base_ordering.append(degree)
- # add an element smaller than all points
- base_ordering.append(-1)
- # compute BSGS-related structures
- strong_gens_distr = _distribute_gens_by_base(base, strong_gens)
- basic_orbits, transversals = _orbits_transversals_from_bsgs(base,
- strong_gens_distr)
- # handle subgroup initialization and tests
- if init_subgroup is None:
- init_subgroup = PermutationGroup([identity])
- if tests is None:
- trivial_test = lambda x: True
- tests = []
- for i in range(base_len):
- tests.append(trivial_test)
- # line 1: more initializations.
- res = init_subgroup
- f = base_len - 1
- l = base_len - 1
- # line 2: set the base for K to the base for G
- res_base = base[:]
- # line 3: compute BSGS and related structures for K
- res_base, res_strong_gens = res.schreier_sims_incremental(
- base=res_base)
- res_strong_gens_distr = _distribute_gens_by_base(res_base,
- res_strong_gens)
- res_generators = res.generators
- res_basic_orbits_init_base = \
- [_orbit(degree, res_strong_gens_distr[i], res_base[i])\
- for i in range(base_len)]
- # initialize orbit representatives
- orbit_reps = [None]*base_len
- # line 4: orbit representatives for f-th basic stabilizer of K
- orbits = _orbits(degree, res_strong_gens_distr[f])
- orbit_reps[f] = get_reps(orbits)
- # line 5: remove the base point from the representatives to avoid
- # getting the identity element as a generator for K
- orbit_reps[f].remove(base[f])
- # line 6: more initializations
- c = [0]*base_len
- u = [identity]*base_len
- sorted_orbits = [None]*base_len
- for i in range(base_len):
- sorted_orbits[i] = basic_orbits[i][:]
- sorted_orbits[i].sort(key=lambda point: base_ordering[point])
- # line 7: initializations
- mu = [None]*base_len
- nu = [None]*base_len
- # this corresponds to the element smaller than all points
- mu[l] = degree + 1
- update_nu(l)
- # initialize computed words
- computed_words = [identity]*base_len
- # line 8: main loop
- while True:
- # apply all the tests
- while l < base_len - 1 and \
- computed_words[l](base[l]) in orbit_reps[l] and \
- base_ordering[mu[l]] < \
- base_ordering[computed_words[l](base[l])] < \
- base_ordering[nu[l]] and \
- tests[l](computed_words):
- # line 11: change the (partial) base of K
- new_point = computed_words[l](base[l])
- res_base[l] = new_point
- new_stab_gens = _stabilizer(degree, res_strong_gens_distr[l],
- new_point)
- res_strong_gens_distr[l + 1] = new_stab_gens
- # line 12: calculate minimal orbit representatives for the
- # l+1-th basic stabilizer
- orbits = _orbits(degree, new_stab_gens)
- orbit_reps[l + 1] = get_reps(orbits)
- # line 13: amend sorted orbits
- l += 1
- temp_orbit = [computed_words[l - 1](point) for point
- in basic_orbits[l]]
- temp_orbit.sort(key=lambda point: base_ordering[point])
- sorted_orbits[l] = temp_orbit
- # lines 14 and 15: update variables used minimality tests
- new_mu = degree + 1
- for i in range(l):
- if base[l] in res_basic_orbits_init_base[i]:
- candidate = computed_words[i](base[i])
- if base_ordering[candidate] > base_ordering[new_mu]:
- new_mu = candidate
- mu[l] = new_mu
- update_nu(l)
- # line 16: determine the new transversal element
- c[l] = 0
- temp_point = sorted_orbits[l][c[l]]
- gamma = computed_words[l - 1]._array_form.index(temp_point)
- u[l] = transversals[l][gamma]
- # update computed words
- computed_words[l] = rmul(computed_words[l - 1], u[l])
- # lines 17 & 18: apply the tests to the group element found
- g = computed_words[l]
- temp_point = g(base[l])
- if l == base_len - 1 and \
- base_ordering[mu[l]] < \
- base_ordering[temp_point] < base_ordering[nu[l]] and \
- temp_point in orbit_reps[l] and \
- tests[l](computed_words) and \
- prop(g):
- # line 19: reset the base of K
- res_generators.append(g)
- res_base = base[:]
- # line 20: recalculate basic orbits (and transversals)
- res_strong_gens.append(g)
- res_strong_gens_distr = _distribute_gens_by_base(res_base,
- res_strong_gens)
- res_basic_orbits_init_base = \
- [_orbit(degree, res_strong_gens_distr[i], res_base[i]) \
- for i in range(base_len)]
- # line 21: recalculate orbit representatives
- # line 22: reset the search depth
- orbit_reps[f] = get_reps(orbits)
- l = f
- # line 23: go up the tree until in the first branch not fully
- # searched
- while l >= 0 and c[l] == len(basic_orbits[l]) - 1:
- l = l - 1
- # line 24: if the entire tree is traversed, return K
- if l == -1:
- return PermutationGroup(res_generators)
- # lines 25-27: update orbit representatives
- if l < f:
- # line 26
- f = l
- c[l] = 0
- # line 27
- temp_orbits = _orbits(degree, res_strong_gens_distr[f])
- orbit_reps[f] = get_reps(temp_orbits)
- # line 28: update variables used for minimality testing
- mu[l] = degree + 1
- temp_index = len(basic_orbits[l]) + 1 - \
- len(res_basic_orbits_init_base[l])
- if temp_index >= len(sorted_orbits[l]):
- nu[l] = base_ordering[degree]
- else:
- nu[l] = sorted_orbits[l][temp_index]
- # line 29: set the next element from the current branch and update
- # accordingly
- c[l] += 1
- if l == 0:
- gamma = sorted_orbits[l][c[l]]
- else:
- gamma = computed_words[l - 1]._array_form.index(sorted_orbits[l][c[l]])
- u[l] = transversals[l][gamma]
- if l == 0:
- computed_words[l] = u[l]
- else:
- computed_words[l] = rmul(computed_words[l - 1], u[l])
- @property
- def transitivity_degree(self):
- r"""Compute the degree of transitivity of the group.
- Explanation
- ===========
- A permutation group `G` acting on `\Omega = \{0, 1, \dots, n-1\}` is
- ``k``-fold transitive, if, for any `k` points
- `(a_1, a_2, \dots, a_k) \in \Omega` and any `k` points
- `(b_1, b_2, \dots, b_k) \in \Omega` there exists `g \in G` such that
- `g(a_1) = b_1, g(a_2) = b_2, \dots, g(a_k) = b_k`
- The degree of transitivity of `G` is the maximum ``k`` such that
- `G` is ``k``-fold transitive. ([8])
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> a = Permutation([1, 2, 0])
- >>> b = Permutation([1, 0, 2])
- >>> G = PermutationGroup([a, b])
- >>> G.transitivity_degree
- 3
- See Also
- ========
- is_transitive, orbit
- """
- if self._transitivity_degree is None:
- n = self.degree
- G = self
- # if G is k-transitive, a tuple (a_0,..,a_k)
- # can be brought to (b_0,...,b_(k-1), b_k)
- # where b_0,...,b_(k-1) are fixed points;
- # consider the group G_k which stabilizes b_0,...,b_(k-1)
- # if G_k is transitive on the subset excluding b_0,...,b_(k-1)
- # then G is (k+1)-transitive
- for i in range(n):
- orb = G.orbit(i)
- if len(orb) != n - i:
- self._transitivity_degree = i
- return i
- G = G.stabilizer(i)
- self._transitivity_degree = n
- return n
- else:
- return self._transitivity_degree
- def _p_elements_group(self, p):
- '''
- For an abelian p-group, return the subgroup consisting of
- all elements of order p (and the identity)
- '''
- gens = self.generators[:]
- gens = sorted(gens, key=lambda x: x.order(), reverse=True)
- gens_p = [g**(g.order()/p) for g in gens]
- gens_r = []
- for i in range(len(gens)):
- x = gens[i]
- x_order = x.order()
- # x_p has order p
- x_p = x**(x_order/p)
- if i > 0:
- P = PermutationGroup(gens_p[:i])
- else:
- P = PermutationGroup(self.identity)
- if x**(x_order/p) not in P:
- gens_r.append(x**(x_order/p))
- else:
- # replace x by an element of order (x.order()/p)
- # so that gens still generates G
- g = P.generator_product(x_p, original=True)
- for s in g:
- x = x*s**-1
- x_order = x_order/p
- # insert x to gens so that the sorting is preserved
- del gens[i]
- del gens_p[i]
- j = i - 1
- while j < len(gens) and gens[j].order() >= x_order:
- j += 1
- gens = gens[:j] + [x] + gens[j:]
- gens_p = gens_p[:j] + [x] + gens_p[j:]
- return PermutationGroup(gens_r)
- def _sylow_alt_sym(self, p):
- '''
- Return a p-Sylow subgroup of a symmetric or an
- alternating group.
- Explanation
- ===========
- The algorithm for this is hinted at in [1], Chapter 4,
- Exercise 4.
- For Sym(n) with n = p^i, the idea is as follows. Partition
- the interval [0..n-1] into p equal parts, each of length p^(i-1):
- [0..p^(i-1)-1], [p^(i-1)..2*p^(i-1)-1]...[(p-1)*p^(i-1)..p^i-1].
- Find a p-Sylow subgroup of Sym(p^(i-1)) (treated as a subgroup
- of ``self``) acting on each of the parts. Call the subgroups
- P_1, P_2...P_p. The generators for the subgroups P_2...P_p
- can be obtained from those of P_1 by applying a "shifting"
- permutation to them, that is, a permutation mapping [0..p^(i-1)-1]
- to the second part (the other parts are obtained by using the shift
- multiple times). The union of this permutation and the generators
- of P_1 is a p-Sylow subgroup of ``self``.
- For n not equal to a power of p, partition
- [0..n-1] in accordance with how n would be written in base p.
- E.g. for p=2 and n=11, 11 = 2^3 + 2^2 + 1 so the partition
- is [[0..7], [8..9], {10}]. To generate a p-Sylow subgroup,
- take the union of the generators for each of the parts.
- For the above example, {(0 1), (0 2)(1 3), (0 4), (1 5)(2 7)}
- from the first part, {(8 9)} from the second part and
- nothing from the third. This gives 4 generators in total, and
- the subgroup they generate is p-Sylow.
- Alternating groups are treated the same except when p=2. In this
- case, (0 1)(s s+1) should be added for an appropriate s (the start
- of a part) for each part in the partitions.
- See Also
- ========
- sylow_subgroup, is_alt_sym
- '''
- n = self.degree
- gens = []
- identity = Permutation(n-1)
- # the case of 2-sylow subgroups of alternating groups
- # needs special treatment
- alt = p == 2 and all(g.is_even for g in self.generators)
- # find the presentation of n in base p
- coeffs = []
- m = n
- while m > 0:
- coeffs.append(m % p)
- m = m // p
- power = len(coeffs)-1
- # for a symmetric group, gens[:i] is the generating
- # set for a p-Sylow subgroup on [0..p**(i-1)-1]. For
- # alternating groups, the same is given by gens[:2*(i-1)]
- for i in range(1, power+1):
- if i == 1 and alt:
- # (0 1) shouldn't be added for alternating groups
- continue
- gen = Permutation([(j + p**(i-1)) % p**i for j in range(p**i)])
- gens.append(identity*gen)
- if alt:
- gen = Permutation(0, 1)*gen*Permutation(0, 1)*gen
- gens.append(gen)
- # the first point in the current part (see the algorithm
- # description in the docstring)
- start = 0
- while power > 0:
- a = coeffs[power]
- # make the permutation shifting the start of the first
- # part ([0..p^i-1] for some i) to the current one
- for _ in range(a):
- shift = Permutation()
- if start > 0:
- for i in range(p**power):
- shift = shift(i, start + i)
- if alt:
- gen = Permutation(0, 1)*shift*Permutation(0, 1)*shift
- gens.append(gen)
- j = 2*(power - 1)
- else:
- j = power
- for i, gen in enumerate(gens[:j]):
- if alt and i % 2 == 1:
- continue
- # shift the generator to the start of the
- # partition part
- gen = shift*gen*shift
- gens.append(gen)
- start += p**power
- power = power-1
- return gens
- def sylow_subgroup(self, p):
- '''
- Return a p-Sylow subgroup of the group.
- The algorithm is described in [1], Chapter 4, Section 7
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import DihedralGroup
- >>> from sympy.combinatorics.named_groups import SymmetricGroup
- >>> from sympy.combinatorics.named_groups import AlternatingGroup
- >>> D = DihedralGroup(6)
- >>> S = D.sylow_subgroup(2)
- >>> S.order()
- 4
- >>> G = SymmetricGroup(6)
- >>> S = G.sylow_subgroup(5)
- >>> S.order()
- 5
- >>> G1 = AlternatingGroup(3)
- >>> G2 = AlternatingGroup(5)
- >>> G3 = AlternatingGroup(9)
- >>> S1 = G1.sylow_subgroup(3)
- >>> S2 = G2.sylow_subgroup(3)
- >>> S3 = G3.sylow_subgroup(3)
- >>> len1 = len(S1.lower_central_series())
- >>> len2 = len(S2.lower_central_series())
- >>> len3 = len(S3.lower_central_series())
- >>> len1 == len2
- True
- >>> len1 < len3
- True
- '''
- from sympy.combinatorics.homomorphisms import (
- orbit_homomorphism, block_homomorphism)
- if not isprime(p):
- raise ValueError("p must be a prime")
- def is_p_group(G):
- # check if the order of G is a power of p
- # and return the power
- m = G.order()
- n = 0
- while m % p == 0:
- m = m/p
- n += 1
- if m == 1:
- return True, n
- return False, n
- def _sylow_reduce(mu, nu):
- # reduction based on two homomorphisms
- # mu and nu with trivially intersecting
- # kernels
- Q = mu.image().sylow_subgroup(p)
- Q = mu.invert_subgroup(Q)
- nu = nu.restrict_to(Q)
- R = nu.image().sylow_subgroup(p)
- return nu.invert_subgroup(R)
- order = self.order()
- if order % p != 0:
- return PermutationGroup([self.identity])
- p_group, n = is_p_group(self)
- if p_group:
- return self
- if self.is_alt_sym():
- return PermutationGroup(self._sylow_alt_sym(p))
- # if there is a non-trivial orbit with size not divisible
- # by p, the sylow subgroup is contained in its stabilizer
- # (by orbit-stabilizer theorem)
- orbits = self.orbits()
- non_p_orbits = [o for o in orbits if len(o) % p != 0 and len(o) != 1]
- if non_p_orbits:
- G = self.stabilizer(list(non_p_orbits[0]).pop())
- return G.sylow_subgroup(p)
- if not self.is_transitive():
- # apply _sylow_reduce to orbit actions
- orbits = sorted(orbits, key=len)
- omega1 = orbits.pop()
- omega2 = orbits[0].union(*orbits)
- mu = orbit_homomorphism(self, omega1)
- nu = orbit_homomorphism(self, omega2)
- return _sylow_reduce(mu, nu)
- blocks = self.minimal_blocks()
- if len(blocks) > 1:
- # apply _sylow_reduce to block system actions
- mu = block_homomorphism(self, blocks[0])
- nu = block_homomorphism(self, blocks[1])
- return _sylow_reduce(mu, nu)
- elif len(blocks) == 1:
- block = list(blocks)[0]
- if any(e != 0 for e in block):
- # self is imprimitive
- mu = block_homomorphism(self, block)
- if not is_p_group(mu.image())[0]:
- S = mu.image().sylow_subgroup(p)
- return mu.invert_subgroup(S).sylow_subgroup(p)
- # find an element of order p
- g = self.random()
- g_order = g.order()
- while g_order % p != 0 or g_order == 0:
- g = self.random()
- g_order = g.order()
- g = g**(g_order // p)
- if order % p**2 != 0:
- return PermutationGroup(g)
- C = self.centralizer(g)
- while C.order() % p**n != 0:
- S = C.sylow_subgroup(p)
- s_order = S.order()
- Z = S.center()
- P = Z._p_elements_group(p)
- h = P.random()
- C_h = self.centralizer(h)
- while C_h.order() % p*s_order != 0:
- h = P.random()
- C_h = self.centralizer(h)
- C = C_h
- return C.sylow_subgroup(p)
- def _block_verify(self, L, alpha):
- delta = sorted(self.orbit(alpha))
- # p[i] will be the number of the block
- # delta[i] belongs to
- p = [-1]*len(delta)
- blocks = [-1]*len(delta)
- B = [[]] # future list of blocks
- u = [0]*len(delta) # u[i] in L s.t. alpha^u[i] = B[0][i]
- t = L.orbit_transversal(alpha, pairs=True)
- for a, beta in t:
- B[0].append(a)
- i_a = delta.index(a)
- p[i_a] = 0
- blocks[i_a] = alpha
- u[i_a] = beta
- rho = 0
- m = 0 # number of blocks - 1
- while rho <= m:
- beta = B[rho][0]
- for g in self.generators:
- d = beta^g
- i_d = delta.index(d)
- sigma = p[i_d]
- if sigma < 0:
- # define a new block
- m += 1
- sigma = m
- u[i_d] = u[delta.index(beta)]*g
- p[i_d] = sigma
- rep = d
- blocks[i_d] = rep
- newb = [rep]
- for gamma in B[rho][1:]:
- i_gamma = delta.index(gamma)
- d = gamma^g
- i_d = delta.index(d)
- if p[i_d] < 0:
- u[i_d] = u[i_gamma]*g
- p[i_d] = sigma
- blocks[i_d] = rep
- newb.append(d)
- else:
- # B[rho] is not a block
- s = u[i_gamma]*g*u[i_d]**(-1)
- return False, s
- B.append(newb)
- else:
- for h in B[rho][1:]:
- if h^g not in B[sigma]:
- # B[rho] is not a block
- s = u[delta.index(beta)]*g*u[i_d]**(-1)
- return False, s
- rho += 1
- return True, blocks
- def _verify(H, K, phi, z, alpha):
- '''
- Return a list of relators ``rels`` in generators ``gens`_h` that
- are mapped to ``H.generators`` by ``phi`` so that given a finite
- presentation <gens_k | rels_k> of ``K`` on a subset of ``gens_h``
- <gens_h | rels_k + rels> is a finite presentation of ``H``.
- Explanation
- ===========
- ``H`` should be generated by the union of ``K.generators`` and ``z``
- (a single generator), and ``H.stabilizer(alpha) == K``; ``phi`` is a
- canonical injection from a free group into a permutation group
- containing ``H``.
- The algorithm is described in [1], Chapter 6.
- Examples
- ========
- >>> from sympy.combinatorics import free_group, Permutation, PermutationGroup
- >>> from sympy.combinatorics.homomorphisms import homomorphism
- >>> from sympy.combinatorics.fp_groups import FpGroup
- >>> H = PermutationGroup(Permutation(0, 2), Permutation (1, 5))
- >>> K = PermutationGroup(Permutation(5)(0, 2))
- >>> F = free_group("x_0 x_1")[0]
- >>> gens = F.generators
- >>> phi = homomorphism(F, H, F.generators, H.generators)
- >>> rels_k = [gens[0]**2] # relators for presentation of K
- >>> z= Permutation(1, 5)
- >>> check, rels_h = H._verify(K, phi, z, 1)
- >>> check
- True
- >>> rels = rels_k + rels_h
- >>> G = FpGroup(F, rels) # presentation of H
- >>> G.order() == H.order()
- True
- See also
- ========
- strong_presentation, presentation, stabilizer
- '''
- orbit = H.orbit(alpha)
- beta = alpha^(z**-1)
- K_beta = K.stabilizer(beta)
- # orbit representatives of K_beta
- gammas = [alpha, beta]
- orbits = list({tuple(K_beta.orbit(o)) for o in orbit})
- orbit_reps = [orb[0] for orb in orbits]
- for rep in orbit_reps:
- if rep not in gammas:
- gammas.append(rep)
- # orbit transversal of K
- betas = [alpha, beta]
- transversal = {alpha: phi.invert(H.identity), beta: phi.invert(z**-1)}
- for s, g in K.orbit_transversal(beta, pairs=True):
- if s not in transversal:
- transversal[s] = transversal[beta]*phi.invert(g)
- union = K.orbit(alpha).union(K.orbit(beta))
- while (len(union) < len(orbit)):
- for gamma in gammas:
- if gamma in union:
- r = gamma^z
- if r not in union:
- betas.append(r)
- transversal[r] = transversal[gamma]*phi.invert(z)
- for s, g in K.orbit_transversal(r, pairs=True):
- if s not in transversal:
- transversal[s] = transversal[r]*phi.invert(g)
- union = union.union(K.orbit(r))
- break
- # compute relators
- rels = []
- for b in betas:
- k_gens = K.stabilizer(b).generators
- for y in k_gens:
- new_rel = transversal[b]
- gens = K.generator_product(y, original=True)
- for g in gens[::-1]:
- new_rel = new_rel*phi.invert(g)
- new_rel = new_rel*transversal[b]**-1
- perm = phi(new_rel)
- try:
- gens = K.generator_product(perm, original=True)
- except ValueError:
- return False, perm
- for g in gens:
- new_rel = new_rel*phi.invert(g)**-1
- if new_rel not in rels:
- rels.append(new_rel)
- for gamma in gammas:
- new_rel = transversal[gamma]*phi.invert(z)*transversal[gamma^z]**-1
- perm = phi(new_rel)
- try:
- gens = K.generator_product(perm, original=True)
- except ValueError:
- return False, perm
- for g in gens:
- new_rel = new_rel*phi.invert(g)**-1
- if new_rel not in rels:
- rels.append(new_rel)
- return True, rels
- def strong_presentation(self):
- '''
- Return a strong finite presentation of group. The generators
- of the returned group are in the same order as the strong
- generators of group.
- The algorithm is based on Sims' Verify algorithm described
- in [1], Chapter 6.
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import DihedralGroup
- >>> P = DihedralGroup(4)
- >>> G = P.strong_presentation()
- >>> P.order() == G.order()
- True
- See Also
- ========
- presentation, _verify
- '''
- from sympy.combinatorics.fp_groups import (FpGroup,
- simplify_presentation)
- from sympy.combinatorics.free_groups import free_group
- from sympy.combinatorics.homomorphisms import (block_homomorphism,
- homomorphism, GroupHomomorphism)
- strong_gens = self.strong_gens[:]
- stabs = self.basic_stabilizers[:]
- base = self.base[:]
- # injection from a free group on len(strong_gens)
- # generators into G
- gen_syms = [('x_%d'%i) for i in range(len(strong_gens))]
- F = free_group(', '.join(gen_syms))[0]
- phi = homomorphism(F, self, F.generators, strong_gens)
- H = PermutationGroup(self.identity)
- while stabs:
- alpha = base.pop()
- K = H
- H = stabs.pop()
- new_gens = [g for g in H.generators if g not in K]
- if K.order() == 1:
- z = new_gens.pop()
- rels = [F.generators[-1]**z.order()]
- intermediate_gens = [z]
- K = PermutationGroup(intermediate_gens)
- # add generators one at a time building up from K to H
- while new_gens:
- z = new_gens.pop()
- intermediate_gens = [z] + intermediate_gens
- K_s = PermutationGroup(intermediate_gens)
- orbit = K_s.orbit(alpha)
- orbit_k = K.orbit(alpha)
- # split into cases based on the orbit of K_s
- if orbit_k == orbit:
- if z in K:
- rel = phi.invert(z)
- perm = z
- else:
- t = K.orbit_rep(alpha, alpha^z)
- rel = phi.invert(z)*phi.invert(t)**-1
- perm = z*t**-1
- for g in K.generator_product(perm, original=True):
- rel = rel*phi.invert(g)**-1
- new_rels = [rel]
- elif len(orbit_k) == 1:
- # `success` is always true because `strong_gens`
- # and `base` are already a verified BSGS. Later
- # this could be changed to start with a randomly
- # generated (potential) BSGS, and then new elements
- # would have to be appended to it when `success`
- # is false.
- success, new_rels = K_s._verify(K, phi, z, alpha)
- else:
- # K.orbit(alpha) should be a block
- # under the action of K_s on K_s.orbit(alpha)
- check, block = K_s._block_verify(K, alpha)
- if check:
- # apply _verify to the action of K_s
- # on the block system; for convenience,
- # add the blocks as additional points
- # that K_s should act on
- t = block_homomorphism(K_s, block)
- m = t.codomain.degree # number of blocks
- d = K_s.degree
- # conjugating with p will shift
- # permutations in t.image() to
- # higher numbers, e.g.
- # p*(0 1)*p = (m m+1)
- p = Permutation()
- for i in range(m):
- p *= Permutation(i, i+d)
- t_img = t.images
- # combine generators of K_s with their
- # action on the block system
- images = {g: g*p*t_img[g]*p for g in t_img}
- for g in self.strong_gens[:-len(K_s.generators)]:
- images[g] = g
- K_s_act = PermutationGroup(list(images.values()))
- f = GroupHomomorphism(self, K_s_act, images)
- K_act = PermutationGroup([f(g) for g in K.generators])
- success, new_rels = K_s_act._verify(K_act, f.compose(phi), f(z), d)
- for n in new_rels:
- if n not in rels:
- rels.append(n)
- K = K_s
- group = FpGroup(F, rels)
- return simplify_presentation(group)
- def presentation(self, eliminate_gens=True):
- '''
- Return an `FpGroup` presentation of the group.
- The algorithm is described in [1], Chapter 6.1.
- '''
- from sympy.combinatorics.fp_groups import (FpGroup,
- simplify_presentation)
- from sympy.combinatorics.coset_table import CosetTable
- from sympy.combinatorics.free_groups import free_group
- from sympy.combinatorics.homomorphisms import homomorphism
- if self._fp_presentation:
- return self._fp_presentation
- def _factor_group_by_rels(G, rels):
- if isinstance(G, FpGroup):
- rels.extend(G.relators)
- return FpGroup(G.free_group, list(set(rels)))
- return FpGroup(G, rels)
- gens = self.generators
- len_g = len(gens)
- if len_g == 1:
- order = gens[0].order()
- # handle the trivial group
- if order == 1:
- return free_group([])[0]
- F, x = free_group('x')
- return FpGroup(F, [x**order])
- if self.order() > 20:
- half_gens = self.generators[0:(len_g+1)//2]
- else:
- half_gens = []
- H = PermutationGroup(half_gens)
- H_p = H.presentation()
- len_h = len(H_p.generators)
- C = self.coset_table(H)
- n = len(C) # subgroup index
- gen_syms = [('x_%d'%i) for i in range(len(gens))]
- F = free_group(', '.join(gen_syms))[0]
- # mapping generators of H_p to those of F
- images = [F.generators[i] for i in range(len_h)]
- R = homomorphism(H_p, F, H_p.generators, images, check=False)
- # rewrite relators
- rels = R(H_p.relators)
- G_p = FpGroup(F, rels)
- # injective homomorphism from G_p into self
- T = homomorphism(G_p, self, G_p.generators, gens)
- C_p = CosetTable(G_p, [])
- C_p.table = [[None]*(2*len_g) for i in range(n)]
- # initiate the coset transversal
- transversal = [None]*n
- transversal[0] = G_p.identity
- # fill in the coset table as much as possible
- for i in range(2*len_h):
- C_p.table[0][i] = 0
- gamma = 1
- for alpha, x in product(range(n), range(2*len_g)):
- beta = C[alpha][x]
- if beta == gamma:
- gen = G_p.generators[x//2]**((-1)**(x % 2))
- transversal[beta] = transversal[alpha]*gen
- C_p.table[alpha][x] = beta
- C_p.table[beta][x + (-1)**(x % 2)] = alpha
- gamma += 1
- if gamma == n:
- break
- C_p.p = list(range(n))
- beta = x = 0
- while not C_p.is_complete():
- # find the first undefined entry
- while C_p.table[beta][x] == C[beta][x]:
- x = (x + 1) % (2*len_g)
- if x == 0:
- beta = (beta + 1) % n
- # define a new relator
- gen = G_p.generators[x//2]**((-1)**(x % 2))
- new_rel = transversal[beta]*gen*transversal[C[beta][x]]**-1
- perm = T(new_rel)
- nxt = G_p.identity
- for s in H.generator_product(perm, original=True):
- nxt = nxt*T.invert(s)**-1
- new_rel = new_rel*nxt
- # continue coset enumeration
- G_p = _factor_group_by_rels(G_p, [new_rel])
- C_p.scan_and_fill(0, new_rel)
- C_p = G_p.coset_enumeration([], strategy="coset_table",
- draft=C_p, max_cosets=n, incomplete=True)
- self._fp_presentation = simplify_presentation(G_p)
- return self._fp_presentation
- def polycyclic_group(self):
- """
- Return the PolycyclicGroup instance with below parameters:
- Explanation
- ===========
- * pc_sequence : Polycyclic sequence is formed by collecting all
- the missing generators between the adjacent groups in the
- derived series of given permutation group.
- * pc_series : Polycyclic series is formed by adding all the missing
- generators of ``der[i+1]`` in ``der[i]``, where ``der`` represents
- the derived series.
- * relative_order : A list, computed by the ratio of adjacent groups in
- pc_series.
- """
- from sympy.combinatorics.pc_groups import PolycyclicGroup
- if not self.is_polycyclic:
- raise ValueError("The group must be solvable")
- der = self.derived_series()
- pc_series = []
- pc_sequence = []
- relative_order = []
- pc_series.append(der[-1])
- der.reverse()
- for i in range(len(der)-1):
- H = der[i]
- for g in der[i+1].generators:
- if g not in H:
- H = PermutationGroup([g] + H.generators)
- pc_series.insert(0, H)
- pc_sequence.insert(0, g)
- G1 = pc_series[0].order()
- G2 = pc_series[1].order()
- relative_order.insert(0, G1 // G2)
- return PolycyclicGroup(pc_sequence, pc_series, relative_order, collector=None)
- def _orbit(degree, generators, alpha, action='tuples'):
- r"""Compute the orbit of alpha `\{g(\alpha) | g \in G\}` as a set.
- Explanation
- ===========
- The time complexity of the algorithm used here is `O(|Orb|*r)` where
- `|Orb|` is the size of the orbit and ``r`` is the number of generators of
- the group. For a more detailed analysis, see [1], p.78, [2], pp. 19-21.
- Here alpha can be a single point, or a list of points.
- If alpha is a single point, the ordinary orbit is computed.
- if alpha is a list of points, there are three available options:
- 'union' - computes the union of the orbits of the points in the list
- 'tuples' - computes the orbit of the list interpreted as an ordered
- tuple under the group action ( i.e., g((1, 2, 3)) = (g(1), g(2), g(3)) )
- 'sets' - computes the orbit of the list interpreted as a sets
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup
- >>> from sympy.combinatorics.perm_groups import _orbit
- >>> a = Permutation([1, 2, 0, 4, 5, 6, 3])
- >>> G = PermutationGroup([a])
- >>> _orbit(G.degree, G.generators, 0)
- {0, 1, 2}
- >>> _orbit(G.degree, G.generators, [0, 4], 'union')
- {0, 1, 2, 3, 4, 5, 6}
- See Also
- ========
- orbit, orbit_transversal
- """
- if not hasattr(alpha, '__getitem__'):
- alpha = [alpha]
- gens = [x._array_form for x in generators]
- if len(alpha) == 1 or action == 'union':
- orb = alpha
- used = [False]*degree
- for el in alpha:
- used[el] = True
- for b in orb:
- for gen in gens:
- temp = gen[b]
- if used[temp] == False:
- orb.append(temp)
- used[temp] = True
- return set(orb)
- elif action == 'tuples':
- alpha = tuple(alpha)
- orb = [alpha]
- used = {alpha}
- for b in orb:
- for gen in gens:
- temp = tuple([gen[x] for x in b])
- if temp not in used:
- orb.append(temp)
- used.add(temp)
- return set(orb)
- elif action == 'sets':
- alpha = frozenset(alpha)
- orb = [alpha]
- used = {alpha}
- for b in orb:
- for gen in gens:
- temp = frozenset([gen[x] for x in b])
- if temp not in used:
- orb.append(temp)
- used.add(temp)
- return {tuple(x) for x in orb}
- def _orbits(degree, generators):
- """Compute the orbits of G.
- If ``rep=False`` it returns a list of sets else it returns a list of
- representatives of the orbits
- Examples
- ========
- >>> from sympy.combinatorics import Permutation
- >>> from sympy.combinatorics.perm_groups import _orbits
- >>> a = Permutation([0, 2, 1])
- >>> b = Permutation([1, 0, 2])
- >>> _orbits(a.size, [a, b])
- [{0, 1, 2}]
- """
- orbs = []
- sorted_I = list(range(degree))
- I = set(sorted_I)
- while I:
- i = sorted_I[0]
- orb = _orbit(degree, generators, i)
- orbs.append(orb)
- # remove all indices that are in this orbit
- I -= orb
- sorted_I = [i for i in sorted_I if i not in orb]
- return orbs
- def _orbit_transversal(degree, generators, alpha, pairs, af=False, slp=False):
- r"""Computes a transversal for the orbit of ``alpha`` as a set.
- Explanation
- ===========
- generators generators of the group ``G``
- For a permutation group ``G``, a transversal for the orbit
- `Orb = \{g(\alpha) | g \in G\}` is a set
- `\{g_\beta | g_\beta(\alpha) = \beta\}` for `\beta \in Orb`.
- Note that there may be more than one possible transversal.
- If ``pairs`` is set to ``True``, it returns the list of pairs
- `(\beta, g_\beta)`. For a proof of correctness, see [1], p.79
- if ``af`` is ``True``, the transversal elements are given in
- array form.
- If `slp` is `True`, a dictionary `{beta: slp_beta}` is returned
- for `\beta \in Orb` where `slp_beta` is a list of indices of the
- generators in `generators` s.t. if `slp_beta = [i_1 \dots i_n]`
- `g_\beta = generators[i_n] \times \dots \times generators[i_1]`.
- Examples
- ========
- >>> from sympy.combinatorics.named_groups import DihedralGroup
- >>> from sympy.combinatorics.perm_groups import _orbit_transversal
- >>> G = DihedralGroup(6)
- >>> _orbit_transversal(G.degree, G.generators, 0, False)
- [(5), (0 1 2 3 4 5), (0 5)(1 4)(2 3), (0 2 4)(1 3 5), (5)(0 4)(1 3), (0 3)(1 4)(2 5)]
- """
- tr = [(alpha, list(range(degree)))]
- slp_dict = {alpha: []}
- used = [False]*degree
- used[alpha] = True
- gens = [x._array_form for x in generators]
- for x, px in tr:
- px_slp = slp_dict[x]
- for gen in gens:
- temp = gen[x]
- if used[temp] == False:
- slp_dict[temp] = [gens.index(gen)] + px_slp
- tr.append((temp, _af_rmul(gen, px)))
- used[temp] = True
- if pairs:
- if not af:
- tr = [(x, _af_new(y)) for x, y in tr]
- if not slp:
- return tr
- return tr, slp_dict
- if af:
- tr = [y for _, y in tr]
- if not slp:
- return tr
- return tr, slp_dict
- tr = [_af_new(y) for _, y in tr]
- if not slp:
- return tr
- return tr, slp_dict
- def _stabilizer(degree, generators, alpha):
- r"""Return the stabilizer subgroup of ``alpha``.
- Explanation
- ===========
- The stabilizer of `\alpha` is the group `G_\alpha =
- \{g \in G | g(\alpha) = \alpha\}`.
- For a proof of correctness, see [1], p.79.
- degree : degree of G
- generators : generators of G
- Examples
- ========
- >>> from sympy.combinatorics.perm_groups import _stabilizer
- >>> from sympy.combinatorics.named_groups import DihedralGroup
- >>> G = DihedralGroup(6)
- >>> _stabilizer(G.degree, G.generators, 5)
- [(5)(0 4)(1 3), (5)]
- See Also
- ========
- orbit
- """
- orb = [alpha]
- table = {alpha: list(range(degree))}
- table_inv = {alpha: list(range(degree))}
- used = [False]*degree
- used[alpha] = True
- gens = [x._array_form for x in generators]
- stab_gens = []
- for b in orb:
- for gen in gens:
- temp = gen[b]
- if used[temp] is False:
- gen_temp = _af_rmul(gen, table[b])
- orb.append(temp)
- table[temp] = gen_temp
- table_inv[temp] = _af_invert(gen_temp)
- used[temp] = True
- else:
- schreier_gen = _af_rmuln(table_inv[temp], gen, table[b])
- if schreier_gen not in stab_gens:
- stab_gens.append(schreier_gen)
- return [_af_new(x) for x in stab_gens]
- PermGroup = PermutationGroup
- class SymmetricPermutationGroup(Basic):
- """
- The class defining the lazy form of SymmetricGroup.
- deg : int
- """
- def __new__(cls, deg):
- deg = _sympify(deg)
- obj = Basic.__new__(cls, deg)
- return obj
- def __init__(self, *args, **kwargs):
- self._deg = self.args[0]
- self._order = None
- def __contains__(self, i):
- """Return ``True`` if *i* is contained in SymmetricPermutationGroup.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, SymmetricPermutationGroup
- >>> G = SymmetricPermutationGroup(4)
- >>> Permutation(1, 2, 3) in G
- True
- """
- if not isinstance(i, Permutation):
- raise TypeError("A SymmetricPermutationGroup contains only Permutations as "
- "elements, not elements of type %s" % type(i))
- return i.size == self.degree
- def order(self):
- """
- Return the order of the SymmetricPermutationGroup.
- Examples
- ========
- >>> from sympy.combinatorics import SymmetricPermutationGroup
- >>> G = SymmetricPermutationGroup(4)
- >>> G.order()
- 24
- """
- if self._order is not None:
- return self._order
- n = self._deg
- self._order = factorial(n)
- return self._order
- @property
- def degree(self):
- """
- Return the degree of the SymmetricPermutationGroup.
- Examples
- ========
- >>> from sympy.combinatorics import SymmetricPermutationGroup
- >>> G = SymmetricPermutationGroup(4)
- >>> G.degree
- 4
- """
- return self._deg
- @property
- def identity(self):
- '''
- Return the identity element of the SymmetricPermutationGroup.
- Examples
- ========
- >>> from sympy.combinatorics import SymmetricPermutationGroup
- >>> G = SymmetricPermutationGroup(4)
- >>> G.identity()
- (3)
- '''
- return _af_new(list(range(self._deg)))
- class Coset(Basic):
- """A left coset of a permutation group with respect to an element.
- Parameters
- ==========
- g : Permutation
- H : PermutationGroup
- dir : "+" or "-", If not specified by default it will be "+"
- here ``dir`` specified the type of coset "+" represent the
- right coset and "-" represent the left coset.
- G : PermutationGroup, optional
- The group which contains *H* as its subgroup and *g* as its
- element.
- If not specified, it would automatically become a symmetric
- group ``SymmetricPermutationGroup(g.size)`` and
- ``SymmetricPermutationGroup(H.degree)`` if ``g.size`` and ``H.degree``
- are matching.``SymmetricPermutationGroup`` is a lazy form of SymmetricGroup
- used for representation purpose.
- """
- def __new__(cls, g, H, G=None, dir="+"):
- g = _sympify(g)
- if not isinstance(g, Permutation):
- raise NotImplementedError
- H = _sympify(H)
- if not isinstance(H, PermutationGroup):
- raise NotImplementedError
- if G is not None:
- G = _sympify(G)
- if not isinstance(G, (PermutationGroup, SymmetricPermutationGroup)):
- raise NotImplementedError
- if not H.is_subgroup(G):
- raise ValueError("{} must be a subgroup of {}.".format(H, G))
- if g not in G:
- raise ValueError("{} must be an element of {}.".format(g, G))
- else:
- g_size = g.size
- h_degree = H.degree
- if g_size != h_degree:
- raise ValueError(
- "The size of the permutation {} and the degree of "
- "the permutation group {} should be matching "
- .format(g, H))
- G = SymmetricPermutationGroup(g.size)
- if isinstance(dir, str):
- dir = Symbol(dir)
- elif not isinstance(dir, Symbol):
- raise TypeError("dir must be of type basestring or "
- "Symbol, not %s" % type(dir))
- if str(dir) not in ('+', '-'):
- raise ValueError("dir must be one of '+' or '-' not %s" % dir)
- obj = Basic.__new__(cls, g, H, G, dir)
- return obj
- def __init__(self, *args, **kwargs):
- self._dir = self.args[3]
- @property
- def is_left_coset(self):
- """
- Check if the coset is left coset that is ``gH``.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup, Coset
- >>> a = Permutation(1, 2)
- >>> b = Permutation(0, 1)
- >>> G = PermutationGroup([a, b])
- >>> cst = Coset(a, G, dir="-")
- >>> cst.is_left_coset
- True
- """
- return str(self._dir) == '-'
- @property
- def is_right_coset(self):
- """
- Check if the coset is right coset that is ``Hg``.
- Examples
- ========
- >>> from sympy.combinatorics import Permutation, PermutationGroup, Coset
- >>> a = Permutation(1, 2)
- >>> b = Permutation(0, 1)
- >>> G = PermutationGroup([a, b])
- >>> cst = Coset(a, G, dir="+")
- >>> cst.is_right_coset
- True
- """
- return str(self._dir) == '+'
- def as_list(self):
- """
- Return all the elements of coset in the form of list.
- """
- g = self.args[0]
- H = self.args[1]
- cst = []
- if str(self._dir) == '+':
- for h in H.elements:
- cst.append(h*g)
- else:
- for h in H.elements:
- cst.append(g*h)
- return cst
|