# HG changeset patch
# User Rob Beezer <beezer@ups.edu>
# Date 1283400941 25200
# Node ID b8b209f74df25e4f730c7bf2c0fab68367fe74e6
# Parent  5b338f2e484f2065d3d30d47bc204d6e9ed13d12
#9773: Implement abelian group classes

diff -r 5b338f2e484f -r b8b209f74df2 sage/groups/all.py
--- a/sage/groups/all.py	Thu Aug 05 03:35:44 2010 -0700
+++ b/sage/groups/all.py	Wed Sep 01 21:15:41 2010 -0700
@@ -12,3 +12,6 @@
 from class_function import ClassFunction
 
 from additive_abelian.all import *
+
+from fg_abelian.all import *
+
diff -r 5b338f2e484f -r b8b209f74df2 sage/groups/fg_abelian/__init__.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sage/groups/fg_abelian/__init__.py	Wed Sep 01 21:15:41 2010 -0700
@@ -0,0 +1,1 @@
+#
\ No newline at end of file
diff -r 5b338f2e484f -r b8b209f74df2 sage/groups/fg_abelian/all.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sage/groups/fg_abelian/all.py	Wed Sep 01 21:15:41 2010 -0700
@@ -0,0 +1,2 @@
+from fg_abelian_group import AdditiveAbelianFGGroup, MultiplicativeAbelianFGGroup
+from units_modn import UnitsModnGroup
\ No newline at end of file
diff -r 5b338f2e484f -r b8b209f74df2 sage/groups/fg_abelian/fg_abelian_group.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sage/groups/fg_abelian/fg_abelian_group.py	Wed Sep 01 21:15:41 2010 -0700
@@ -0,0 +1,621 @@
+r"""
+Finitely-Generated Abelian Groups
+---------------------------------
+
+Abstract class, built on FG modules, in both additive and multiplicative versions.
+
+"""
+from sage.modules.fg_pid.fgp_module import FGP_Module_class, FGP_Element
+from sage.structure.parent_gens import ParentWithGens
+from sage.rings.all import ZZ
+from sage.misc.cachefunc import cached_method
+
+def _cover_and_relations_from_moduli(mods):
+    r"""
+    Utility function to construct modules for a quotient construction.
+
+    INPUT:
+
+    A list of integers, designating the modulus of each term in
+    a product of cyclic groups.  A zero implies the term is infinite,
+    i.e. all of `\ZZ`.
+
+    OUTPUT:
+
+    Two additive FGP_Modules over `\ZZ`, such that their quotient
+    is naturally isomorphic to the corresponding product of cyclic
+    groups with given modulus.
+
+    This routine provides the necessary minimum input to create
+    finitely-generated modules over `\ZZ`.  Classes in this module
+    will augment this input to create objects tat behave as
+    finitely-generated groups.  the FGP module class will perform
+    optimizations on these moduli to achieve a minimal set of generators
+    and an internal representation that is more economical for certain
+    computations.
+
+    EXAMPLE::
+
+        sage: from sage.groups.fg_abelian.fg_abelian_group import _cover_and_relations_from_moduli
+        sage: _cover_and_relations_from_moduli([0,2,3])
+        (Ambient free module of rank 3 over the principal ideal domain Integer Ring, Free module of degree 3 and rank 2 over Integer Ring
+        Echelon basis matrix:
+        [0 2 0]
+        [0 0 3])
+
+    TESTS::
+
+        sage: _cover_and_relations_from_moduli([5,2.3,0])
+        Traceback (most recent call last):
+        ...
+        ValueError: List of moduli must be non-negative integers, not [5, 2.30000000000000, 0]
+        sage: _cover_and_relations_from_moduli([0,4,-5,2])
+        Traceback (most recent call last):
+        ...
+        ValueError: List of moduli must be non-negative integers, not [0, 4, -5, 2]
+    """
+    # We need some generators so the generator-parent routine returns
+    #   something, and we need at least one modulus to get default generators
+    #   but in the case of trivial groups, the optimized modules will do the right thing
+    if mods == []:
+        mods = [1]
+    if not all([x in ZZ for x in mods]) or not all([x >= 0 for x in mods]):
+        raise ValueError('List of moduli must be non-negative integers, not %s' % mods)
+    n = len(mods)
+    A = ZZ**n
+    B = A.span([A.gen(i) * mods[i] for i in range(n)])
+    return A, B
+
+class AbelianFGGroup_class(FGP_Module_class):
+    r"""
+    This is the abstract base class, has desired functionality, as much as we can push up here.
+
+    Lots of abstract methods to get info from additive/multiplicative behavior
+    """
+
+    def __init__(self, cover, relations, generators=None, ambient=None):
+        r"""
+        (goes at class level, really)
+        INPUT:
+
+        - cover - module - copies of ZZ, from cover/relations
+
+        - relations - module - mod out by moduli to create cyclic groups
+
+        - generators - arbitrary elements of some structure, defaults come from derived classes
+
+        - ambient - if non-None, indicates a subggroup of some other fg abelian group
+
+        """
+        #sage: S=TestGroup([3,3])
+        #sage: TestSuite(S).run(verbose = True)
+        #
+        # same initial format as constructor of FGP class to allow subquotient, subclass, subgroup
+        if generators != None and ambient != None:
+            raise ValueError('cannot specify both generators and an ambient group when creating a finitely generated abelian group')
+        # degree needs to be nonzero to ensure at least one generator
+        self._degree = cover.degree()
+        # ambient=None is a flag, group is "self-ambient", needs generators of some kind though
+        # otherwise creating a subgroup, so get ambient group from construction, no gens required
+        # non-None ambient values come from subgroup/quotient construction, so we don't test them here
+        self._ambient = ambient
+        if self._ambient is None:   # creating from scratch, store original genes, mods/orders (just in ambient group)
+            self._ambient = self
+            if generators is None:
+                self._orig_gens = tuple(self._default_gens(self._degree))
+            else:
+                if not isinstance(generators, (list,tuple)):
+                    raise TypeError('generators of a finitely generated abelian group must be a list or tuple, not %s' % generators)
+                if len(generators) != self._degree:
+                    raise ValueError('the number of generators must equal the number of moduli')
+                self._orig_gens = tuple(generators)  
+            # self._orig_gens will be non-empty now
+            # the relations module has the orders passed to the cover/relation generator
+            #   so we can recover/extract them here, to use for reporting on ambient's construction
+            m = relations.basis_matrix()
+            mods = [0]*m.ncols()
+            for row,col in zip(m.pivot_rows(), m.pivots()):
+                mods[col]=m[row,col]
+            self._orig_mods = tuple(mods)
+        # parentage of generators comes from a cached method
+        # could sanity check/examine generators versus apparent orders, common parent
+        # test for integer multiples in additive groups, exponentiation in multiplicative for efficiency in discrete_exp
+        #   belongs in derived classes?
+        # Derived classes will set their category via Parent w/ gens initialization
+        #   but the gens are the optimized ones from the module class, not the "original" ones here
+        # We build the quotient module structure now
+        FGP_Module_class.__init__(self, cover, relations)
+
+    def __call__(self, x, check=None):
+        from sage.structure.element import is_Vector
+        from sage.rings.all import QQ
+        # check for lists posing as module elements, else complain | delegate
+        # send some things up to FGP module
+        # trap others and test
+        #
+        # if parent is this group, then return it
+        if hasattr(x,'parent') and x.parent() is self:
+            # print "returning own element"
+            return x
+        sanitized = None
+        # naked lists are scalars for minimal generators, not module elements
+        if isinstance(x, (list,tuple)):
+            # print "handling a list"
+            if len(x) == len(self.invariants()):
+                sanitized = x
+            else:
+                raise ValueError('%s is the wrong length to provide a linear combination of minimal generators' % x)
+        # an element of the right class gets passed up to be treated as an FGP_Element
+        elif isinstance(x, self._element_class()):
+            # print "handling own class"
+            sanitized = x
+            ###  CHECK type of elements here?
+        # vectors are linear combos of original generators
+        elif is_Vector(x):
+            # print "Handling a vector"
+            if len(x) == self._degree:
+                sanitized = x
+            else:
+                raise ValueError('%s is the wrong length to provide a linear combination of original generators' % x)
+        if sanitized != None:
+            # print "calling super __call__"
+            return FGP_Module_class.__call__(self, sanitized, check=check)
+        else:
+            # print "Coercing into generator parent"
+            # first coerce into parent, raiase Type error
+            # then see if we can log it
+            try:
+                x = self.generator_parent()(x)
+                return self._discrete_log(x)
+            except:
+                raise ValueError('cannot interpret %s as an element of %s' % (x, self))
+
+
+    def __contains__(self, x):
+        r"""
+        Return true if x is contained in self.
+        """
+        if not self.is_finite():
+            raise TypeError('Unable to decide if %s is in an infinite group: %s' % (x, self))
+        if not isinstance(x, self._element_class()):
+            try:
+                x = self.generator_parent()(x)
+            except TypeError:
+                print "gen parent problem"
+                return False
+        # have something of the right element class or right generator type
+        try:
+            self(x)
+            return True
+        except:
+            return False
+
+
+    # comparison, see module class for __eq__
+
+
+    def _subquotient_class(self):
+        return self.__class__
+
+    def _repr_(self):
+        return self._ascii_name()
+
+
+    @cached_method
+    def generator_parent(self):
+        # Mostly to get 0 or 1 for empty sum/product in discrete exp
+        return self.ambient_generators()[0].parent()
+
+    def _discrete_log(self, element):
+        # Generic  method
+        # element is posing as a possible element of the group generated by the generators
+        # We look to see if it is a (linear) combination, and return the combination as vector over ZZ
+        # This will be computationally expensive usually, it is a decomposition
+        # Input: anything that __call__ does not recognize (based on class types)
+        # Output: a specific element with the "right" discrete exponential
+        #
+        if not self.is_finite():
+            raise TypeError("naive brute-force discrete log is not possible for infinite group")
+        # self.list() is cached automatically, and discrete_exp is cached purposely
+        #   subsequent calls become more like lookups
+        #   first call will be slow, implement speed-ups in discrete_exp
+        #   or overide this method in a derived class
+        matches = filter(lambda x: x.discrete_exp() == element, self.list())
+        if not matches:
+            raise ValueError("%s is not an element of %s" % (element, self))
+        if len(matches) > 1:
+            print "Warning: %s has several representations in %s" % (element, self)
+        return matches[0]
+
+
+    def ambient_group(self):
+        # always returns a group with some _orig_gens and _orig_mods
+        return self._ambient
+
+    def ambient_generators(self):
+        return self.ambient_group()._orig_gens
+
+    def ambient_generator_orders(self):
+        return self.ambient_group()._orig_mods
+
+    def is_abelian(self):
+        return True
+
+    def is_ambient(self):
+        return self.ambient_group() == self
+
+    def is_subgroup(self, other):
+        # self subgroup of other?
+        # .is_submodule() for FGP requires common relations, tests submodule for covers
+        # We add check on ambient group too (ensuring common relations)
+        # modules could be equal, while built with different generators,
+        #   thus strictness about ambient group check
+        return self.ambient_group() == other.ambient_group() and self.is_submodule(other)
+
+    def is_isomorphic(self, other):
+        # if other is not abelian class, convert to permutation group
+        # if other is permutation group, promote self to a permutation group and then test
+        # as is, just for abelian groups
+        return self.invariants() == other.invariants()
+
+    def is_cyclic(self):
+        return len(self.smith_form_gens()) < 2
+
+    def order(self):
+        r"""
+        Return the order of this group (an integer or infinity)
+
+        EXAMPLES::
+
+            sage: AdditiveAbelianGroup([2,4]).order()
+            8
+            sage: AdditiveAbelianGroup([0, 2,4]).order()
+            +Infinity
+            sage: AdditiveAbelianGroup([]).order()
+            1
+        """
+        return self.cardinality()
+
+    def exponent(self):
+        r"""
+        Return the exponent of this group (the smallest positive integer `N`
+        such that `Nx = 0` for all `x` in the group). If there is no such
+        integer, return 0.
+
+        EXAMPLES::
+
+            sage: AdditiveAbelianGroup([2,4]).exponent()
+            4
+            sage: AdditiveAbelianGroup([0, 2,4]).exponent()
+            0
+            sage: AdditiveAbelianGroup([]).exponent()
+            1
+        """
+        if not self.invariants():
+            return 1
+        else:
+            ann =  self.annihilator().gen()
+            if ann:
+                return ann
+            return ZZ(0)
+
+    def cyclic_generator(self):
+        gens = self.smith_form_gens()
+        if self.is_cyclic():
+            if len(gens) == 1:
+                return(gens[0])
+            else:
+                return self.identity()
+        else:
+            raise ValueError('No cyclic generator for the non-cyclic %s' % self)
+
+    def identity(self):
+        # Build 0 and 1 from this in derived classes
+        from sage.modules.free_module_element import vector
+        trivial = vector(ZZ, [0]*self._degree)
+        return self(trivial, check=True)
+
+    def subgroup(self, gens):
+        if not isinstance(gens, (list, tuple)):
+            raise TypeError, "Generators of a subgroup of an abelian group must be in a list or tuple"
+        gen_vectors = [self(v).lift() for v in gens]
+        relations = self.relations()  # preserved across subgroups
+        newcover = self.cover().submodule(gen_vectors) + relations
+        return self._subquotient_class()(newcover, relations, ambient=self.ambient_group())
+
+    def intersection(self, other):
+        if self.ambient_group() != other.ambient_group():
+            raise ValueError('cannot intersect subgroups of different groups: %s and %s' % (self, other))
+        # subgroups preserve relations, so just intersect covers, also preserve ambient group
+        newcover = self.cover().intersection(other.cover())
+        return self._subquotient_class()(newcover, self.relations(), ambient=self.ambient_group())
+
+
+    def _cyclic_product_name(self, orders):
+        r"""
+        Helper formatting function, direct sum of cyclic groups
+        INPUT: orders = list of positive integers
+        OUTPUT: ASCII string
+        """
+        # Optimized trivial group can have no generators, so no orders
+        if orders==() or orders==[]:
+            orders = [1]
+        terms = []
+        for order in orders:
+            if order:
+                terms.append('Z_' + str(order))
+            else:
+                terms.append('Z')
+        return ' + '.join(terms)
+
+    def _ascii_name(self):
+        # uses isomorphism class no matter what
+        # build an ambient name class, direct product, etc.
+        # finite|infinite
+        # multiplicative|additive
+        # group|subgroup(w/ generators)
+        # {isomorphism class}
+        rep=[]
+        if self.is_multiplicative():
+            rep.append('Multiplicative')
+        else:
+            rep.append('Additive')
+        rep.append('abelian group')
+        if self.is_finite():
+            rep.extend(['of order', repr(self.cardinality())+','])
+        else:
+            rep.append('of infinite order,')
+        rep.append('isomorphic to')
+        rep.append(self._cyclic_product_name(self.invariants()))
+        rep.append("with generator(s):")
+        rep.append(', '.join([repr(x) for x in self.gens()]))
+        return " ".join(rep)
+
+
+    def ambient_name(self):
+        rep =[]
+        if self.is_multiplicative():
+            rep.append('Multiplicative')
+        else:
+            rep.append('Additive')
+        rep.append('abelian group isomorphic to')
+        rep.append(self._cyclic_product_name(self.ambient_generator_orders()))
+        rep.append("with generator(s):")
+        rep.append(', '.join([repr(x) for x in self.ambient_generators()]))
+        return " ".join(rep)
+
+
+
+
+    def info(self):
+        print "Finite:", self.is_finite()
+        print "Multiplicative:", self.is_multiplicative()
+        print "Ambient:", self.is_ambient()
+        print "Generators:", self.gens()
+        print "Ambient Gens:", self.ambient_group()._orig_gens
+        print "Ambient Mods:", self.ambient_group()._orig_mods
+
+class AbelianFGGroupElement(FGP_Element):
+
+    def __init__(self, parent, x, check=None):
+        FGP_Element.__init__(self, parent, x, check)
+
+    def _discrete_exp(self):
+        # Generic method
+        # Assumes just sum or product possible
+        # Used by the _repr_ and _latex_ and ... routines
+        # Convert from internal to human representation
+        # Override in derived classes for better performance
+        # Input:  an (optimized, enhanced) group element represenation
+        # Output: an element of the real group being represented, store as side efffect
+        parent = self.parent()
+        terms=[]
+        # replace blunt copies by multiple or power, if that corresponding operation is available
+        for gen, copies in zip(parent.ambient_group()._orig_gens, self._hermite_lift()):
+            terms.extend([gen]*copies)
+        # n.b. terms is empty for identity element of group
+        return parent._listop()(terms, parent._identity_gen())
+
+    @cached_method
+    def discrete_exp(self):
+        # Do not override, instead override _discrete_exp
+        # result is cached with element, so not recomputed
+        return self._discrete_exp()
+
+    # make ascii versions with operators, generators (?)
+    # define term-format-function and operator/seperator
+    #    functions in derived additive/multiplicative classes (?)
+    def representation(self, generators='minimal'):
+        # Result can differ for an ambient group
+        #   where minimal generators could be just a re-ordering
+        if generators=='minimal':
+            # from module routines, cleaned-up ('reduced')
+            return self.parent().coordinate_vector(self, reduce=True)
+        elif generators=='ambient':
+            # up in the ambient group
+            return self._hermite_lift()
+        else:
+            raise ValueError('"generators" keyword must be "minimal" or "ambient", not %s' % generators)
+
+
+    def _hermite_lift(self):
+        r"""
+        This gives a certain canonical lifting of elements of this group
+        (represented as a quotient `G/H` of free abelian groups) to `G`, using
+        the Hermite normal form of the matrix of relations.
+
+        Mainly used by the ``_repr_`` method.
+
+        EXAMPLES::
+
+            sage: A = AdditiveAbelianGroup([2, 3])
+            sage: v = 3000001 * A.0
+            sage: v.lift()
+            (3000001, 0)
+            sage: v._hermite_lift()
+            (1, 0)
+        """
+        y = self.lift()
+        H = self.parent().W().basis_matrix()
+
+        for i in xrange(H.nrows()):
+            if i in H.pivot_rows():
+                j = H.pivots()[i]
+                N = H[i,j]
+                a = (y[j] - (y[j] % N)) // N
+                y = y - a*H.row(i)
+        return y.change_ring(ZZ)
+
+    def _repr_(self):
+        return self.discrete_exp()._repr_()
+
+
+#~~~~~~~~~~~~~~~~~~~~~~  Additive  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+from sage.categories.commutative_additive_groups import CommutativeAdditiveGroups
+
+class AdditiveAbelianFGGroup_class(AbelianFGGroup_class):
+
+    def __init__(self, cover, relations, generators=None, ambient=None):
+        ParentWithGens.__init__(self, self, category=CommutativeAdditiveGroups())
+        AbelianFGGroup_class.__init__(self, cover, relations, generators=generators, ambient=ambient)
+
+    def _element_class(self):
+        return AdditiveAbelianFGGroupElement
+
+    def _listop(self):
+        from sage.all import sum
+        return sum
+
+    def _identity_gen(self):
+        #return self.generator_parent().zero()
+        return self.generator_parent()(0)
+
+    def _default_gens(self, deg):
+        # returns a tuple
+        from sage.modules.free_module_element import vector
+        basis = []
+        for i in range(deg):
+            zero = vector(ZZ, [0]*deg)
+            zero[i] = 1
+            basis.append(zero)
+        return tuple(basis)
+
+    def is_multiplicative(self):
+        return False
+
+
+class AdditiveAbelianFGGroupElement(AbelianFGGroupElement):
+
+    def __init__(self, parent, x, check=None):
+        AbelianFGGroupElement.__init__(self, parent, x, check)
+
+#~~~~~~~~~~~~~~~~~~~~~~  Multiplicative  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+from sage.categories.groups import Groups
+
+class MultiplicativeAbelianFGGroup_class(AbelianFGGroup_class):
+
+    def __init__(self, cover, relations, generators=None, ambient=None):
+        # nee commutative?
+        ParentWithGens.__init__(self, self, category=Groups())
+        AbelianFGGroup_class.__init__(self, cover, relations, generators=generators, ambient=ambient)
+
+    def _element_class(self):
+        return MultiplicativeAbelianFGGroupElement
+
+    def _listop(self):
+        from sage.all import prod
+        return prod
+
+    def _identity_gen(self):
+        # return self.generator_parent().one()
+        return self.generator_parent()(1)
+
+    def _default_gens(self, deg):
+        # returns a tuple
+        from sage.symbolic.ring import SymbolicRing
+        SR=SymbolicRing()
+        return tuple([SR('f'+str(i+1)) for i in range(deg)])
+
+    def is_multiplicative(self):
+        return True
+
+
+class MultiplicativeAbelianFGGroupElement(AbelianFGGroupElement):
+
+    def __init__(self, parent, elt, check=None):
+        AbelianFGGroupElement.__init__(self, parent, elt, check)
+
+    def __mul__(self, other):
+        return self+other
+
+    def __pow__(self, other):
+        return other*self
+
+    # make sum return not implemented
+    # Also integer multiples
+
+#~~~~~~~~~~~~~~~~~~~~~~  Builders  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+def AdditiveAbelianFGGroup(orders, generators=None):
+    r"""
+    Creates a finitely-generated additive abelian group.
+
+    INPUT:
+
+    - ``orders`` - a list of integers giving the orders of the
+      generators (in the same order).  If no generators are
+      given these are theorders of the default generators.
+      Use a zero to specify an infinite order generator.
+
+    - ``generators`` - a list of elements with a common parent.
+      The only other requirement is that it must be possible to
+      add two elements.  When no generators are given, the defaults
+      provided are dense vectors (module elements) over the integers.
+      We do not presume the generators are independent, but performance
+      and results will be better if they are.
+
+    OUTPUT:
+
+    An element of the XXX class that is an additive abelian group.
+
+    EXAMPLES:
+
+    A finite group with default generators. ::
+
+        sage: G = AdditiveAbelianFGGroup([2,4]); G
+        Additive abelian group of order 8, isomorphic to Z_2 + Z_4 with generator(s): (1, 0), (0, 1)
+        sage: G.list()
+        [(0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2), (1, 3)]
+
+    Notice that generators generally get replaced by a smaller set.  The
+    generators used to create the group (or if a subgroup, the generators
+    used to create the universal, or ambient, group) can be requested. ::
+
+        sage: G = AdditiveAbelianFGGroup([2,3]); G
+        Additive abelian group of order 6, isomorphic to Z_6 with generator(s): (1, 2)
+        sage: G.gens()
+        ((1, 2),)
+        sage: G.ambient_generators()
+        ((1, 0), (0, 1))
+        sage: G.generator_parent()
+        Ambient free module of rank 2 over the principal ideal domain Integer Ring
+
+    Generators can be any element with an addition defined.
+
+        sage: E = EllipticCurve('30a2')
+        sage: points = [E(4,-7,1), E(7/4, -11/8, 1), E(3, -2, 1)]
+        sage: M = AdditiveAbelianFGGroup([3, 2, 2], points)
+        sage: M
+        Additive abelian group of order 12, isomorphic to Z_2 + Z_6 with generator(s): (7/4 : -11/8 : 1), (13 : -52 : 1)
+        sage: M.ambient_generators()
+        ((4 : -7 : 1), (7/4 : -11/8 : 1), (3 : -2 : 1))
+        sage: M.list()
+        [(0 : 1 : 0), (13 : -52 : 1), (4 : -7 : 1), (3 : -2 : 1), (4 : 2 : 1), (13 : 38 : 1), (7/4 : -11/8 : 1), (1 : -4 : 1), (-2 : -7 : 1), (-5 : 2 : 1), (-2 : 8 : 1), (1 : 2 : 1)]
+    """
+    cover, relations = _cover_and_relations_from_moduli(orders)
+    return AdditiveAbelianFGGroup_class(cover, relations, generators=generators)
+
+
+def MultiplicativeAbelianFGGroup(orders, generators=None):
+    cover, relations = _cover_and_relations_from_moduli(orders)
+    return MultiplicativeAbelianFGGroup_class(cover, relations, generators=generators)
diff -r 5b338f2e484f -r b8b209f74df2 sage/groups/fg_abelian/units_modn.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sage/groups/fg_abelian/units_modn.py	Wed Sep 01 21:15:41 2010 -0700
@@ -0,0 +1,111 @@
+def UnitsModnGroup(n):
+    r"""
+    Creates a group with the units (invertible elements) under multiplication mod n.
+
+    INPUT:
+
+    - ``n`` - a positive non-zero integer
+
+    OUTPUT:
+
+    A multiplicative abelian group containing all the invertible elements (units)
+    when the operation is multiplication mod n.
+
+    EXAMPLES:
+
+    A nontrivial example, illustrating the possible operations. ::
+
+        sage: n = 2^3 * 3^2 * 7^2
+        sage: U = UnitsModnGroup(n); U
+        Multiplicative abelian group of order 1008, isomorphic to Z_2 + Z_2 + Z_6 + Z_42 with generator(s): 2647, 685, 785, 2053
+        sage: U.order() == euler_phi(n)
+        True
+
+    The original generators are all elements of prime-power order,
+    while the computed generators have orders equal to the
+    invariants of the abelian group.  ::
+
+        sage: U.ambient_generators()
+        (2647, 1765, 1961, 2353, 2449, 3313, 1513)
+        sage: U.ambient_generator_orders()
+        (2, 2, 2, 3, 2, 3, 7)
+        sage: U.gens()
+        (2647, 685, 785, 2053)
+        sage: [x.order() for x in U.gens()]
+        [2, 2, 6, 42]
+        sage: U.invariants()
+        (2, 2, 6, 42)
+
+    Subgroups, intersections and quotients are all possible.  ::
+
+        sage: a = U(3331); b = U(757); c = U(785); d = U(2377);
+        sage: X=U.subgroup([a,b])
+        sage: X
+        Multiplicative abelian group of order 28, isomorphic to Z_2 + Z_14 with generator(s): 1567, 757
+        sage: Y=U.subgroup([c,d])
+        sage: Y
+        Multiplicative abelian group of order 126, isomorphic to Z_3 + Z_42 with generator(s): 2353, 2249
+        sage: A=X.intersection(Y)
+        sage: A
+        Multiplicative abelian group of order 7, isomorphic to Z_7 with generator(s): 1513
+        sage: A.is_cyclic()
+        True
+        sage: A.list()
+        [1, 1513, 3025, 1009, 2521, 505, 2017]
+        sage: A.0 in X and A.0 in Y
+        True
+        sage: b^2
+        1513
+        sage: d^3
+        1513
+        sage: X.is_subgroup(U)
+        True
+        sage: U.is_subgroup(Y)
+        False
+        sage: U/X
+        Multiplicative abelian group of order 36, isomorphic to Z_6 + Z_6 with generator(s): f3*f6^2, f4^2*f5
+        sage: Y/A
+        Multiplicative abelian group of order 18, isomorphic to Z_3 + Z_6 with generator(s): f6, f3*f4^2
+
+    A Cayley table for a group of units. ::
+
+        sage: U = UnitsModnGroup(28); U
+        Multiplicative abelian group of order 12, isomorphic to Z_2 + Z_6 with generator(s): 15, 17
+        sage: U.cayley_table(names='elements')
+         *   1 17  9 13 25  5 15  3 23 27 11 19
+          +------------------------------------
+         1|  1 17  9 13 25  5 15  3 23 27 11 19
+        17| 17  9 13 25  5  1  3 23 27 11 19 15
+         9|  9 13 25  5  1 17 23 27 11 19 15  3
+        13| 13 25  5  1 17  9 27 11 19 15  3 23
+        25| 25  5  1 17  9 13 11 19 15  3 23 27
+         5|  5  1 17  9 13 25 19 15  3 23 27 11
+        15| 15  3 23 27 11 19  1 17  9 13 25  5
+         3|  3 23 27 11 19 15 17  9 13 25  5  1
+        23| 23 27 11 19 15  3  9 13 25  5  1 17
+        27| 27 11 19 15  3 23 13 25  5  1 17  9
+        11| 11 19 15  3 23 27 25  5  1 17  9 13
+        19| 19 15  3 23 27 11  5  1 17  9 13 25
+    """
+    from sage.rings.finite_rings.integer_mod_ring import IntegerModRing
+    from sage.groups.fg_abelian.fg_abelian_group import _cover_and_relations_from_moduli, MultiplicativeAbelianFGGroup_class
+    # sanity check n
+    R=IntegerModRing(n)
+    # Build with elements strictly of prime-power order
+    # Optimized module code will consolidate to minimal generators
+    newgens = []
+    for gen in R.unit_gens():
+        order = gen.multiplicative_order()
+        prime_power = [gen**(order/(base**exponent)) for base,exponent in gen.multiplicative_order().factor()]
+        newgens.extend(prime_power)
+    #gens = R.unit_gens()
+    #newgens = []
+    #for gen in gens:
+        #order = gen.multiplicative_order()
+        #for base, exponent in order.factor():
+            #newgen = gen**(order/(base**exponent))
+            #newgens.append(newgen)
+    orders = [x.multiplicative_order() for x in newgens]
+    cover, relations = _cover_and_relations_from_moduli(orders)
+    return MultiplicativeAbelianFGGroup_class(cover, relations, generators=newgens)
+
diff -r 5b338f2e484f -r b8b209f74df2 setup.py
--- a/setup.py	Thu Aug 05 03:35:44 2010 -0700
+++ b/setup.py	Wed Sep 01 21:15:41 2010 -0700
@@ -829,6 +829,7 @@
                      'sage.groups',
                      'sage.groups.abelian_gps',
                      'sage.groups.additive_abelian',
+                     'sage.groups.fg_abelian',
                      'sage.groups.matrix_gps',
                      'sage.groups.perm_gps',
                      'sage.groups.perm_gps.partn_ref',
