Repository: oscar-system/Oscar.jl
Pull request #4661: Add `dicyclic_group`, `is_dicyclic_group` and have `quaternion_group` and `is_quaternion_group` be aliases of those
Author: login: jamesnohilly
Labels: name: enhancement
description: New feature or request
name: topic: groups
name: release notes: use title
description: For PRs: the title of this PR is suitable for direct use in the release notes
Created: 2025-02-27T13:38:13Z | Merged: 2025-04-30T09:34:01Z

This PR adds a group constructor for Dicyclic Groups (`dicyclic_group`) and changes the constructor for Quaternion Groups to be an alias of `dicyclic_group`.

This PR closes #1630

Linked issue #1630: Dicyclic groups
I think it would be a good idea to have `dicyclic_group` similar to `quaternion_group`. 

(Regarding the parameter, I personally would favour `dicyclic_group(n)` yielding a group of order `4n`, but since we don't do this for quaternion or dihedral groups, I guess this wish will not be fulfilled.)
Linked issue comments: user: login: fingolfin
created_at: 2022-10-16T15:29:36Z
body: Actually you can already get these via `quaternion_group`.

This is due to a historic quirk (?) in GAP. It used to call all these groups "quaternion groups". This is still available for backwards compatibility. From the GAP manual:

> DicyclicGroup  constructs  the dicyclic group of size n in the category given by the filter filt.  Here,  n  must  be a multiple of 4. The synonym QuaternionGroup for DicyclicGroup is provided  for backward compatibility, but will print a warning if n is not a power of 2.

But I agree, we should have `dicyclic_group` and `is_dicyclic_group`. Question then is what to do about `quaternion_group`?
1. remove it
2. keep it as a synonym / alias
3. keep it but restrict it to arguments which are powers of 2
4. keep it but remove the argument (as historically "Quaternion group" only refers to the group of order 8) 

Additional, one can wonder about having `generalized_quaternion_group` ...

(Oh and of course the various `is_*` functions should be made to match)
author_association: MEMBER
user: login: thofma
created_at: 2022-10-16T16:29:30Z
body: Ah, I never actually tried `quaternion_group(20)`, since the docstring says that `n` must be a power of 2.

I don't mind too much, but would lean towards 2. There are some mathematical areas where this is only known as quaternion group of order `4n` (and no one would ever have the idea of looking it up under 'dicyclic group').
author_association: COLLABORATOR

Diff:
diff --git a/src/Groups/group_constructors.jl b/src/Groups/group_constructors.jl
index d4fc8967c682..7b7fbd19b737 100644
--- a/src/Groups/group_constructors.jl
+++ b/src/Groups/group_constructors.jl
@@ -205,11 +205,11 @@ end
 # Delegating to the GAP constructor via `_gap_filter` does not work here.
 function abelian_group(::Type{TG}, v::Vector{T}) where TG <: Union{PcGroup, SubPcGroup} where T <: IntegerUnion
   if 0 in v
-# if 0 in v || (TG == PcGroup && any(!is_prime, v))
-#TODO: Currently GAP's IsPcpGroup groups run into problems
-#      already in the available Oscar tests,
-#      see https://github.com/gap-packages/polycyclic/issues/88,
-#      so we keep the code from the master branch here.
+    # if 0 in v || (TG == PcGroup && any(!is_prime, v))
+    #TODO: Currently GAP's IsPcpGroup groups run into problems
+    #      already in the available Oscar tests,
+    #      see https://github.com/gap-packages/polycyclic/issues/88,
+    #      so we keep the code from the master branch here.
     # We cannot construct an `IsPcGroup` group if some generator shall have
     # order infinity or 1 or a composed number.
     return TG(GAP.Globals.AbelianPcpGroup(length(v), GapObj(v; recursive = true)))
@@ -327,8 +327,8 @@ julia> order(g)
 ```
 """
 function projective_general_linear_group(n::Int, q::Int)
-   @req is_prime_power_with_data(q)[1] "The field size must be a prime power"
-   return PermGroup(GAP.Globals.PGL(n, q))
+  @req is_prime_power_with_data(q)[1] "The field size must be a prime power"
+  return PermGroup(GAP.Globals.PGL(n, q))
 end
 
 
@@ -350,8 +350,8 @@ julia> order(g)
 ```
 """
 function projective_special_linear_group(n::Int, q::Int)
-   @req is_prime_power_with_data(q)[1] "The field size must be a prime power"
-   return PermGroup(GAP.Globals.PSL(n, q))
+  @req is_prime_power_with_data(q)[1] "The field size must be a prime power"
+  return PermGroup(GAP.Globals.PSL(n, q))
 end
 
 
@@ -373,9 +373,9 @@ julia> order(g)
 ```
 """
 function projective_symplectic_group(n::Int, q::Int)
-   @req is_prime_power_with_data(q)[1] "The field size must be a prime power"
-   @req iseven(n) "The dimension must be even"
-   return PermGroup(GAP.Globals.PSp(n, q))
+  @req is_prime_power_with_data(q)[1] "The field size must be a prime power"
+  @req iseven(n) "The dimension must be even"
+  return PermGroup(GAP.Globals.PSp(n, q))
 end
 
 
@@ -397,8 +397,8 @@ julia> order(g)
 ```
 """
 function projective_unitary_group(n::Int, q::Int)
-   @req is_prime_power_with_data(q)[1] "The field size must be a prime power"
-   return PermGroup(GAP.Globals.PGU(n, q))
+  @req is_prime_power_with_data(q)[1] "The field size must be a prime power"
+  return PermGroup(GAP.Globals.PGU(n, q))
 end
 
 
@@ -420,8 +420,8 @@ julia> order(g)
 ```
 """
 function projective_special_unitary_group(n::Int, q::Int)
-   @req is_prime_power_with_data(q)[1] "The field size must be a prime power"
-   return PermGroup(GAP.Globals.PSU(n, q))
+  @req is_prime_power_with_data(q)[1] "The field size must be a prime power"
+  return PermGroup(GAP.Globals.PSU(n, q))
 end
 
 
@@ -444,15 +444,15 @@ julia> g = projective_orthogonal_group(3, 3);  order(g)
 ```
 """
 function projective_orthogonal_group(e::Int, n::Int, q::Int)
-   @req is_prime_power_with_data(q)[1] "The field size must be a prime power"
-   if e == 1 || e == -1
-      @req iseven(n) "The dimension must be even"
-   elseif e == 0
-      @req isodd(n) "The dimension must be odd"
-   else
-      throw(ArgumentError("Invalid description of projective orthogonal group"))
-   end
-   return PermGroup(GAP.Globals.PGO(e, n, q))
+  @req is_prime_power_with_data(q)[1] "The field size must be a prime power"
+  if e == 1 || e == -1
+    @req iseven(n) "The dimension must be even"
+  elseif e == 0
+    @req isodd(n) "The dimension must be odd"
+  else
+    throw(ArgumentError("Invalid description of projective orthogonal group"))
+  end
+  return PermGroup(GAP.Globals.PGO(e, n, q))
 end
 
 projective_orthogonal_group(n::Int, q::Int) = projective_orthogonal_group(0, n, q)
@@ -477,15 +477,15 @@ julia> g = projective_special_orthogonal_group(3, 3);  order(g)
 ```
 """
 function projective_special_orthogonal_group(e::Int, n::Int, q::Int)
-   @req is_prime_power_with_data(q)[1] "The field size must be a prime power"
-   if e == 1 || e == -1
-      @req iseven(n) "The dimension must be even"
-   elseif e == 0
-      @req isodd(n) "The dimension must be odd"
-   else
-      throw(ArgumentError("Invalid description of projective special orthogonal group"))
-   end
-   return PermGroup(GAP.Globals.PSO(e, n, q))
+  @req is_prime_power_with_data(q)[1] "The field size must be a prime power"
+  if e == 1 || e == -1
+    @req iseven(n) "The dimension must be even"
+  elseif e == 0
+    @req isodd(n) "The dimension must be odd"
+  else
+    throw(ArgumentError("Invalid description of projective special orthogonal group"))
+  end
+  return PermGroup(GAP.Globals.PSO(e, n, q))
 end
 
 projective_special_orthogonal_group(n::Int, q::Int) = projective_special_orthogonal_group(0, n, q)
@@ -510,15 +510,15 @@ julia> g = projective_omega_group(3, 3);  order(g)
 ```
 """
 function projective_omega_group(e::Int, n::Int, q::Int)
-   @req is_prime_power_with_data(q)[1] "The field size must be a prime power"
-   if e == 1 || e == -1
-      @req iseven(n) "The dimension must be even"
-   elseif e == 0
-      @req isodd(n) "The dimension must be odd"
-   else
-      throw(ArgumentError("Invalid description of projective orthogonal group"))
-   end
-   return PermGroup(GAP.Globals.POmega(e, n, q))
+  @req is_prime_power_with_data(q)[1] "The field size must be a prime power"
+  if e == 1 || e == -1
+    @req iseven(n) "The dimension must be even"
+  elseif e == 0
+    @req isodd(n) "The dimension must be odd"
+  else
+    throw(ArgumentError("Invalid description of projective orthogonal group"))
+  end
+  return PermGroup(GAP.Globals.POmega(e, n, q))
 end
 
 projective_omega_group(n::Int, q::Int) = projective_omega_group(0, n, q)
@@ -611,17 +611,17 @@ julia> gens(free_group([:a, :b], "x" => 1:2, 'y' => (1:2, 1:2)))
 ```
 """
 function free_group(L::Vector{<:Symbol}; eltype::Symbol = :letter)
-   @req allunique(L) "generator names must be unique"
-   J = GapObj(L, recursive = true)
-   if eltype == :syllable
-     G = FPGroup(GAP.Globals.FreeGroup(J; FreeGroupFamilyType = GapObj("syllable"))::GapObj)
-   elseif eltype == :letter
-     G = FPGroup(GAP.Globals.FreeGroup(J)::GapObj)
-   else
-     error("eltype must be :letter or :syllable, not ", eltype)
-   end
-   GAP.Globals.SetRankOfFreeGroup(GapObj(G), length(J))
-   return G
+  @req allunique(L) "generator names must be unique"
+  J = GapObj(L, recursive = true)
+  if eltype == :syllable
+    G = FPGroup(GAP.Globals.FreeGroup(J; FreeGroupFamilyType = GapObj("syllable"))::GapObj)
+  elseif eltype == :letter
+    G = FPGroup(GAP.Globals.FreeGroup(J)::GapObj)
+  else
+    error("eltype must be :letter or :syllable, not ", eltype)
+  end
+  GAP.Globals.SetRankOfFreeGroup(GapObj(G), length(J))
+  return G
 end
 
 # HACK: we want to use `AbstractAlgebra.@varnames_interface` for free groups,
@@ -636,8 +636,8 @@ end
 # `@free_group` by delegating to the `@_free_group` macros (plus some extra
 # shenigans).
 function _free_group(L::Vector{<:Symbol}; eltype::Symbol = :letter)
-   G = free_group(L; eltype)
-   return G, gens(G)
+  G = free_group(L; eltype)
+  return G, gens(G)
 end
 
 AbstractAlgebra.@varnames_interface _free_group(s)
@@ -649,7 +649,7 @@ free_group(; kw...) = _free_group(0; kw...)[1]
 # HACK to get the default variable name stem `:f` instead of `:x`
 # but also to insert validation for `n`.
 function free_group(n::Int, s::VarName = :f; kw...)
-   @req n >= 0 "n must be a non-negative integer"
+  @req n >= 0 "n must be a non-negative integer"
   _free_group(n, s; kw...)[1]
 end
 
@@ -693,12 +693,12 @@ macro free_group(args...)
     # if the arguments are varnames, put them into a vector before delegating
     # to @_free_group
     esc(quote
-      Oscar.@_free_group([$(args...)])
+        Oscar.@_free_group([$(args...)])
     end)
   else
     # by default just delegate to `@_free_group`
     esc(quote
-      Oscar.@_free_group($(args...))
+        Oscar.@_free_group($(args...))
     end)
   end
 end
@@ -721,7 +721,7 @@ end
 #end
 
 function free_abelian_group(::Type{FPGroup}, n::Int)
- return FPGroup(GAPWrap.FreeAbelianGroup(n)::GapObj)
+  return FPGroup(GAPWrap.FreeAbelianGroup(n)::GapObj)
 end
 
 
@@ -821,22 +821,28 @@ false
 @gapattribute is_dihedral_group(G::GAPGroup) = GAP.Globals.IsDihedralGroup(GapObj(G))::Bool
 
 """
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
     quaternion_group(::Type{T} = PcGroup, n::IntegerUnion)
 
-Return the (generalized) quaternion group of order `n`,
-as an instance of `T`,
-where `n` is a power of 2 and `T` is in
-{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
+Return the dicyclic group of order `n`,
+as an instance of `T`, where `n` is a multiple of 4
+and `T` is a suitable group type such as
+`PcGroup`, `SubPcGroup`, `PermGroup`, `FPGroup`, `SubFPGroup`.
+
+!!! note
+    In the literature, these groups are sometimes called dicyclic groups and
+    sometimes (generalized) quaternion groups. For user convenience we support
+    both names. However, the two functions are fully identical. 
 
 # Examples
 ```jldoctest
-julia> g = quaternion_group(8)
+julia> g = dicyclic_group(8)
 Pc group of order 8
 
-julia> quaternion_group(PermGroup, 8)
+julia> dicyclic_group(PermGroup, 8)
 Permutation group of degree 8
 
-julia> g = quaternion_group(FPGroup, 8)
+julia> g = dicyclic_group(FPGroup, 8)
 Finitely presented group of order 8
 
 julia> relators(g)
@@ -844,38 +850,81 @@ julia> relators(g)
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
-quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
+dicyclic_group(n::IntegerUnion) = dicyclic_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+function dicyclic_group(::Type{T}, n::IntegerUnion) where {T<:GAPGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(_gap_filter(T), n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(_gap_filter(T), n)::GapObj)
 end
 
 # Delegating to the GAP constructor via `_gap_filter` does not work here.
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
+function dicyclic_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(GAP.Globals.IsPcGroup, n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(GAP.Globals.IsPcGroup, n)::GapObj)
 end
 
 @doc raw"""
+    is_dicyclic_group(G::GAPGroup)
     is_quaternion_group(G::GAPGroup)
 
-Return `true` if `G` is isomorphic to a (generalized) quaternion group
-of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+Return `true` if `G` is isomorphic to a dicyclic group
+of order $4k, k > 1$, and `false` otherwise.
+
+!!! note
+    In the literature, these groups are sometimes called dicyclic groups and
+    sometimes (generalized) quaternion groups. For user convenience we support
+    both names. However, the two functions are fully identical.
+    
+    If you wish to check that `G` is a quaternion group in a stricter sense,
+    e.g. that its order exactly 8 resp. a power of two, you can check
+    check the order explicitly resp. use `is_pgroup`.
 
 # Examples
 ```jldoctest
-julia> is_quaternion_group(small_group(8, 3))
+julia> is_dicyclic_group(small_group(8, 3))
 false
 
-julia> is_quaternion_group(small_group(8, 4))
+julia> is_dicyclic_group(transitive_group(8, 5))
 true
 ```
 """
-@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool
+is_dicyclic_group(G::GAPGroup) = _is_dicyclic_group(G)
+
+function _is_dicyclic_group(G::GAPGroup)
+  N = order(G)
+  !iszero(mod(N, 4)) && return false
+  
+  n = div(N, 2)
+  a = div(n, 2)
+
+  G1, _ = derived_subgroup(G)
+  (is_cyclic(G1) && order(G1) == a) || return false
+
+  local Zn
+  T = right_transversal(G, G1)
+  @assert length(T) == 4
+  for x in T
+    H = GAP.Globals.ClosureGroup(GapObj(G1), GapObj(x))
+    Zn, _ = Oscar._as_subgroup(G1, H)
+    if is_cyclic(Zn) && order(Zn) == n
+      break
+    end
+  end
+
+  (is_cyclic(Zn) && order(Zn) == n) || return false
+
+  local t = one(G)
+  while t in Zn
+    t = rand(G)
+  end
+
+  !(order(t) == 4 && all(s -> s^t*s == s^0, gens(Zn))) && return false
+
+  # Different from GAP code, here we skip finding other generator.
+  return true
+end
+
+@alias quaternion_group dicyclic_group
+@alias is_quaternion_group is_dicyclic_group
diff --git a/src/exports.jl b/src/exports.jl
index 4c9bb819aec3..6ebc0ddc4864 100644
--- a/src/exports.jl
+++ b/src/exports.jl
@@ -510,6 +510,7 @@ export describe
 export desimulate_valuation
 export det
 export diameter
+export dicyclic_group
 export dihedral_group
 export dim
 export dim_of_torusfactor
@@ -845,6 +846,7 @@ export is_coroot_with_index
 export is_cyclic, has_is_cyclic, set_is_cyclic
 export is_degenerate
 export is_dense
+export is_dicyclic_group
 export is_dihedral_group, has_is_dihedral_group, set_is_dihedral_group
 export is_dominant
 export is_du_val_singularity
@@ -945,7 +947,7 @@ export is_pure
 export is_q_cartier
 export is_q_gorenstein
 export is_quasisimple, has_is_quasisimple, set_is_quasisimple
-export is_quaternion_group, has_is_quaternion_group, set_is_quaternion_group
+export is_quaternion_group
 export is_quotient
 export is_radical
 export is_ready
diff --git a/test/Groups/constructors.jl b/test/Groups/constructors.jl
index af2212361bd5..4c5e2e5fe641 100644
--- a/test/Groups/constructors.jl
+++ b/test/Groups/constructors.jl
@@ -65,6 +65,10 @@ end
   @test isa(dihedral_group(PermGroup, 6), PermGroup)
 
   @test is_quaternion_group(small_group(8, 4))
+  @test ! is_quaternion_group(small_group(12, 3))
+  @test is_dicyclic_group(small_group(8, 4))
+  @test ! is_dicyclic_group(small_group(13, 1))
+
   @test small_group_identification(small_group(8, 4)) == (8, 4)
   @test isa(small_group(8, 4), PcGroup)
   @test isa(small_group(60, 5), PermGroup)
@@ -170,6 +174,9 @@ end
 
   Q8 = quaternion_group(8)
   @test isa(Q8, PcGroup)
+
+  Dic12 = dicyclic_group(12)
+  @test isa(Dic12, PcGroup)
   
   gl = GL(2, 3)
   @test isa(gl, MatrixGroup)


Reviews:
user: login: jamesnohilly
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T13:41:09Z
user: login: jamesnohilly
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T13:44:19Z
user: login: thofma
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T13:59:48Z
user: login: thofma
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T15:18:35Z
user: login: lgoettgens
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T15:44:00Z
user: login: thofma
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T16:14:50Z
user: login: fingolfin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-27T21:30:53Z
user: login: thofma
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-01T10:30:30Z
user: login: thofma
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-01T10:30:57Z
user: login: jamesnohilly
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T11:17:20Z
user: login: jamesnohilly
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T11:18:21Z
user: login: thofma
body: Very nice, thanks. But some group theory person should have a look too.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-12T16:25:57Z
user: login: fingolfin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T23:58:27Z
user: login: fingolfin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T23:59:28Z
user: login: thofma
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-21T06:57:54Z
user: login: ThomasBreuer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T10:02:01Z
user: login: ThomasBreuer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T10:16:41Z
user: login: ThomasBreuer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T10:19:12Z
user: login: ThomasBreuer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T13:35:49Z
user: login: fingolfin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T13:28:51Z
user: login: fingolfin
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-02T13:50:06Z
user: login: fingolfin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T14:06:29Z
user: login: fingolfin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T14:08:04Z
user: login: lgoettgens
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-03T10:12:46Z
user: login: ThomasBreuer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T11:24:56Z
user: login: ThomasBreuer
body: Looks good. Thanks.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-16T12:37:47Z
user: login: lgoettgens
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T17:28:55Z
user: login: jamesnohilly
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T11:14:09Z
user: login: fingolfin
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-30T08:33:05Z

Review comments:
diff_hunk: @@ -843,38 +845,81 @@ julia> relators(g)
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
 quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup = dicyclic_group(T, n)
+quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup} = dicyclic_group(T, n)
+
+@doc raw"""
+    is_quaternion_group(G::GAPGroup)
+
+Return `true` if `G` is isomorphic to a (generalized) quaternion group
+of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+
+# Examples
+```jldoctest
+julia> is_quaternion_group(small_group(8, 3))
+false
+
+julia> is_quaternion_group(small_group(8, 4))
+true
+```
+"""
+@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsGeneralisedQuaternionGroup(GapObj(G))::Bool
path: src/Groups/group_constructors.jl
user: login: jamesnohilly
body: This has not been aliased as we specifically want it to be isomorphic to a generalized quaternion group, however I am unsure if this is correct and could be changed.
created_at: 2025-02-27T13:41:08Z
author_association: COLLABORATOR
diff_hunk: @@ -843,38 +845,81 @@ julia> relators(g)
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
 quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup = dicyclic_group(T, n)
+quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup} = dicyclic_group(T, n)
+
+@doc raw"""
+    is_quaternion_group(G::GAPGroup)
+
+Return `true` if `G` is isomorphic to a (generalized) quaternion group
+of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+
+# Examples
path: src/Groups/group_constructors.jl
user: login: jamesnohilly
body: I am unsure if a better example could be used to reflect specifically what we check (and same for `is_dicyclic_group`).
created_at: 2025-02-27T13:44:19Z
author_association: COLLABORATOR
diff_hunk: @@ -827,6 +827,8 @@ as an instance of `T`,
 where `n` is a power of 2 and `T` is in
path: src/Groups/group_constructors.jl
user: login: thofma
body: ```suggestion
where `n` is a multiple of 4 and `T` is in
```

created_at: 2025-02-27T13:59:47Z
author_association: COLLABORATOR
diff_hunk: @@ -843,38 +845,81 @@ julia> relators(g)
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
 quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup = dicyclic_group(T, n)
+quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup} = dicyclic_group(T, n)
+
+@doc raw"""
+    is_quaternion_group(G::GAPGroup)
+
+Return `true` if `G` is isomorphic to a (generalized) quaternion group
+of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+
+# Examples
path: src/Groups/group_constructors.jl
user: login: thofma
body: ```
is_quaternion_group(transitive_group(8, 5))
```
maybe?
created_at: 2025-02-27T15:18:35Z
author_association: COLLABORATOR
diff_hunk: @@ -843,38 +845,81 @@ julia> relators(g)
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
 quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup = dicyclic_group(T, n)
+quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup} = dicyclic_group(T, n)
path: src/Groups/group_constructors.jl
user: login: lgoettgens
body: ```suggestion
quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup = dicyclic_group(T, n)
```
Is there a reason that these two are identical? If not, I would suggest to unify them
created_at: 2025-02-27T15:39:37Z
author_association: MEMBER
diff_hunk: @@ -827,6 +827,8 @@ as an instance of `T`,
 where `n` is a power of 2 and `T` is in
path: src/Groups/group_constructors.jl
user: login: lgoettgens
body: I think "power of 2" is what we want here, as that is how (generalized) quaternion groups are defined. For the function that does the exact same construction for "multiple of 4", there is `dicyclic_group` below. In particular, this distinction in the docstring matches to how `is_quaternion_group` works
created_at: 2025-02-27T15:43:08Z
author_association: MEMBER
diff_hunk: @@ -843,38 +845,81 @@ julia> relators(g)
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
 quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup = dicyclic_group(T, n)
+quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup} = dicyclic_group(T, n)
+
+@doc raw"""
+    is_quaternion_group(G::GAPGroup)
+
+Return `true` if `G` is isomorphic to a (generalized) quaternion group
+of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+
+# Examples
+```jldoctest
+julia> is_quaternion_group(small_group(8, 3))
+false
+
+julia> is_quaternion_group(small_group(8, 4))
+true
+```
+"""
+@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsGeneralisedQuaternionGroup(GapObj(G))::Bool
+
+"""
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
+
+Return the dicyclic group of order `n`, as an instance of `T`,
+where `n` is a multiple of 4 and `T` is in
+{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
+
+# Examples
+```jldoctest
+julia> g = dicyclic_group(12)
+Pc group of order 12
+
+julia> dicyclic_group(PermGroup, 12)
+Permutation group of degree 12
+
+julia> g = dicyclic_group(FPGroup, 12)
+Finitely presented group of order 12
+
+julia> relators(g)
+3-element Vector{FPGroupElem}:
+ r^2*s^-3
+ s^6
+ r^-1*s*r*s
+```
+"""
+dicyclic_group(n::IntegerUnion) = dicyclic_group(PcGroup, n)
+
+function dicyclic_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(_gap_filter(T), n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(_gap_filter(T), n)::GapObj)
 end
 
 # Delegating to the GAP constructor via `_gap_filter` does not work here.
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
+function dicyclic_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(GAP.Globals.IsPcGroup, n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(GAP.Globals.IsPcGroup, n)::GapObj)
 end
 
 @doc raw"""
-    is_quaternion_group(G::GAPGroup)
+    is_dicyclic_group(G::GAPGroup)
 
-Return `true` if `G` is isomorphic to a (generalized) quaternion group
-of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+Return `true` if `G` is isomorphic to a dicyclic group
+of order $4n, n > 1$, and `false` otherwise.
 
 # Examples
 ```jldoctest
-julia> is_quaternion_group(small_group(8, 3))
+julia> is_dicyclic_group(small_group(8, 3))
 false
 
-julia> is_quaternion_group(small_group(8, 4))
+julia> is_dicyclic_group(small_group(8, 4))
 true
 ```
 """
-@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool
+@gapattribute is_dicyclic_group(G::GAPGroup) = GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool
path: src/Groups/group_constructors.jl
user: login: lgoettgens
body: ```suggestion
@gapattribute is_dicyclic_group(G::GAPGroup) = GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool

```
created_at: 2025-02-27T15:43:56Z
author_association: MEMBER
diff_hunk: @@ -827,6 +827,8 @@ as an instance of `T`,
 where `n` is a power of 2 and `T` is in
path: src/Groups/group_constructors.jl
user: login: thofma
body: Hm, I think that the consensus in https://github.com/oscar-system/Oscar.jl/issues/1630 is that both should be the same. Here are some reasons why I think this is the case:
1. The definition with 2-powers is unnecessarily restrictive. There are plenty of references that construct quaternion groups of order 4n.
2. There is now already code out there that is using the most general definition (that the order is any number $4n$), because this is what people were being told in #1630
3. Since we don't do any quaternion_group(k) -> group of order 2^k or 2^(k+2) shenanigans, there is no reason to restrict to 2-powers.

created_at: 2025-02-27T16:14:49Z
author_association: COLLABORATOR
diff_hunk: @@ -827,6 +827,8 @@ as an instance of `T`,
 where `n` is a power of 2 and `T` is in
path: src/Groups/group_constructors.jl
user: login: fingolfin
body: Yeah I decided to just treat both commands as exact equals, any artificial restriction benefits nobody but can cause frustration
created_at: 2025-02-27T21:30:52Z
author_association: MEMBER
diff_hunk: @@ -65,6 +65,7 @@ end
   @test isa(dihedral_group(PermGroup, 6), PermGroup)
 
   @test is_quaternion_group(small_group(8, 4))
+  @test is_dicyclic_group(small_group(8, 4))
path: test/Groups/constructors.jl
user: login: thofma
body: While you are at it, could you also add tests where it returns false? Something like `abelian_group([8])` or `small_group(13, 1)`. That would be nice.
created_at: 2025-03-01T10:30:30Z
author_association: COLLABORATOR
diff_hunk: @@ -843,38 +845,83 @@ julia> relators(g)
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
 quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+quaternion_group(::Type{T}, n::IntegerUnion) where {T<:Union{GAPGroup,PcGroup,SubPcGroup}} =
+  dicyclic_group(T, n)
+
+@doc raw"""
+    is_quaternion_group(G::GAPGroup)
+
+Return `true` if `G` is isomorphic to a (generalized) quaternion group
+of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+
+# Examples
+```jldoctest
+julia> is_quaternion_group(small_group(8, 3))
+false
+
+julia> is_quaternion_group(small_group(8, 4))
+true
+```
+"""
+@gapattribute is_quaternion_group(G::GAPGroup) =
+  GAP.Globals.IsGeneralisedQuaternionGroup(GapObj(G))::Bool
+
+"""
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
+
+Return the dicyclic group of order `n`, as an instance of `T`,
+where `n` is a multiple of 4 and `T` is in
+{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
+
+# Examples
+```jldoctest
+julia> g = dicyclic_group(12)
+Pc group of order 12
+
+julia> dicyclic_group(PermGroup, 12)
+Permutation group of degree 12
+
+julia> g = dicyclic_group(FPGroup, 12)
+Finitely presented group of order 12
+
+julia> relators(g)
+3-element Vector{FPGroupElem}:
+ r^2*s^-3
+ s^6
+ r^-1*s*r*s
+```
+"""
+dicyclic_group(n::IntegerUnion) = dicyclic_group(PcGroup, n)
+
+function dicyclic_group(::Type{T}, n::IntegerUnion) where {T<:GAPGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(_gap_filter(T), n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(_gap_filter(T), n)::GapObj)
 end
 
 # Delegating to the GAP constructor via `_gap_filter` does not work here.
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
+function dicyclic_group(::Type{T}, n::IntegerUnion) where {T<:Union{PcGroup,SubPcGroup}}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(GAP.Globals.IsPcGroup, n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(GAP.Globals.IsPcGroup, n)::GapObj)
 end
 
 @doc raw"""
-    is_quaternion_group(G::GAPGroup)
+    is_dicyclic_group(G::GAPGroup)
 
-Return `true` if `G` is isomorphic to a (generalized) quaternion group
-of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+Return `true` if `G` is isomorphic to a dicyclic group
+of order $4n, n > 1$, and `false` otherwise.
 
 # Examples
 ```jldoctest
-julia> is_quaternion_group(small_group(8, 3))
+julia> is_dicyclic_group(small_group(8, 3))
 false
 
-julia> is_quaternion_group(small_group(8, 4))
+julia> is_dicyclic_group(small_group(8, 4))
 true
 ```
 """
-@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool
+@gapattribute is_dicyclic_group(G::GAPGroup) =
+  GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool
path: src/Groups/group_constructors.jl
user: login: thofma
body: newline should not be removed from end of file
created_at: 2025-03-01T10:30:57Z
author_association: COLLABORATOR
diff_hunk: @@ -843,38 +845,83 @@ julia> relators(g)
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
 quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+quaternion_group(::Type{T}, n::IntegerUnion) where {T<:Union{GAPGroup,PcGroup,SubPcGroup}} =
+  dicyclic_group(T, n)
+
+@doc raw"""
+    is_quaternion_group(G::GAPGroup)
+
+Return `true` if `G` is isomorphic to a (generalized) quaternion group
+of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+
+# Examples
+```jldoctest
+julia> is_quaternion_group(small_group(8, 3))
+false
+
+julia> is_quaternion_group(small_group(8, 4))
+true
+```
+"""
+@gapattribute is_quaternion_group(G::GAPGroup) =
+  GAP.Globals.IsGeneralisedQuaternionGroup(GapObj(G))::Bool
+
+"""
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
+
+Return the dicyclic group of order `n`, as an instance of `T`,
+where `n` is a multiple of 4 and `T` is in
+{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
+
+# Examples
+```jldoctest
+julia> g = dicyclic_group(12)
+Pc group of order 12
+
+julia> dicyclic_group(PermGroup, 12)
+Permutation group of degree 12
+
+julia> g = dicyclic_group(FPGroup, 12)
+Finitely presented group of order 12
+
+julia> relators(g)
+3-element Vector{FPGroupElem}:
+ r^2*s^-3
+ s^6
+ r^-1*s*r*s
+```
+"""
+dicyclic_group(n::IntegerUnion) = dicyclic_group(PcGroup, n)
+
+function dicyclic_group(::Type{T}, n::IntegerUnion) where {T<:GAPGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(_gap_filter(T), n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(_gap_filter(T), n)::GapObj)
 end
 
 # Delegating to the GAP constructor via `_gap_filter` does not work here.
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
+function dicyclic_group(::Type{T}, n::IntegerUnion) where {T<:Union{PcGroup,SubPcGroup}}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(GAP.Globals.IsPcGroup, n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(GAP.Globals.IsPcGroup, n)::GapObj)
 end
 
 @doc raw"""
-    is_quaternion_group(G::GAPGroup)
+    is_dicyclic_group(G::GAPGroup)
 
-Return `true` if `G` is isomorphic to a (generalized) quaternion group
-of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+Return `true` if `G` is isomorphic to a dicyclic group
+of order $4n, n > 1$, and `false` otherwise.
 
 # Examples
 ```jldoctest
-julia> is_quaternion_group(small_group(8, 3))
+julia> is_dicyclic_group(small_group(8, 3))
 false
 
-julia> is_quaternion_group(small_group(8, 4))
+julia> is_dicyclic_group(small_group(8, 4))
 true
 ```
 """
-@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool
+@gapattribute is_dicyclic_group(G::GAPGroup) =
+  GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool
path: src/Groups/group_constructors.jl
user: login: jamesnohilly
body: Thanks! Didn't catch this after formatting
created_at: 2025-03-03T11:17:20Z
author_association: COLLABORATOR
diff_hunk: @@ -65,6 +65,7 @@ end
   @test isa(dihedral_group(PermGroup, 6), PermGroup)
 
   @test is_quaternion_group(small_group(8, 4))
+  @test is_dicyclic_group(small_group(8, 4))
path: test/Groups/constructors.jl
user: login: jamesnohilly
body: Will do! I have added one for both `is_quaternion_group` and `is_dicyclic_group`.
created_at: 2025-03-03T11:18:21Z
author_association: COLLABORATOR
diff_hunk: @@ -825,9 +825,11 @@ false
 
 Return the (generalized) quaternion group of order `n`,
 as an instance of `T`,
-where `n` is a power of 2 and `T` is in
+where `n` is a multiple of 4 and `T` is in
 {`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
 
+This is an alias of `dicyclic_group`.
path: src/Groups/group_constructors.jl
user: login: fingolfin
body: ```suggestion
This is an alias of [`dicyclic_group`](@ref).
```
created_at: 2025-03-20T23:42:15Z
author_association: MEMBER
diff_hunk: @@ -844,38 +846,83 @@ julia> relators(g)
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
 quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+quaternion_group(::Type{T}, n::IntegerUnion) where {T<:Union{GAPGroup,PcGroup,SubPcGroup}} =
path: src/Groups/group_constructors.jl
user: login: fingolfin
body: `PcGroupa` and `SubPcGroup` are subtypes of `GAPGroup`, so you can just write
```suggestion
quaternion_group(::Type{T}, n::IntegerUnion) where {T<:GAPGroup} =
```
created_at: 2025-03-20T23:43:23Z
author_association: MEMBER
diff_hunk: @@ -825,9 +825,11 @@ false
 
 Return the (generalized) quaternion group of order `n`,
 as an instance of `T`,
-where `n` is a power of 2 and `T` is in
+where `n` is a multiple of 4 and `T` is in
 {`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
path: src/Groups/group_constructors.jl
user: login: fingolfin
body: ```suggestion
where `n` is a multiple of 4 and `T` is a suitable group type such as
`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`.
```
created_at: 2025-03-20T23:44:26Z
author_association: MEMBER
diff_hunk: @@ -844,38 +846,83 @@ julia> relators(g)
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
 quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+quaternion_group(::Type{T}, n::IntegerUnion) where {T<:Union{GAPGroup,PcGroup,SubPcGroup}} =
+  dicyclic_group(T, n)
+
+@doc raw"""
+    is_quaternion_group(G::GAPGroup)
+
+Return `true` if `G` is isomorphic to a (generalized) quaternion group
+of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+
+# Examples
+```jldoctest
+julia> is_quaternion_group(small_group(8, 3))
+false
+
+julia> is_quaternion_group(transitive_group(8, 5))
+true
+```
+"""
+@gapattribute is_quaternion_group(G::GAPGroup) =
+  GAP.Globals.IsGeneralisedQuaternionGroup(GapObj(G))::Bool
+
+"""
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
+
+Return the dicyclic group of order `n`, as an instance of `T`,
+where `n` is a multiple of 4 and `T` is in
+{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
path: src/Groups/group_constructors.jl
user: login: fingolfin
body: Adjust this to fully mirror the `quaternion_group` docstring (including a `@ref` link in the other direction).


... or... perhaps we should instead really merge the two, like GAP does: show a single docstring that starts with
```
"""
    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
    quaternion_group(::Type{T} = PcGroup, n::IntegerUnion)

...
```

Then attach that to, say, `quaternion_group`, and turn `dicyclic_group` into a "true" alias by doing `const dicyclic_group = quaternion_group` (I think it'll then get the same docstring). Merge `is_quaternion_group and `is_dicyclic_group` similarly.

In the examples use only one (quaternion). 

Then instead of `This is an alias of `dicyclic_group`.` write something more like this:


```
!!! note
    For historical reasons and backwards compatibility, `dicyclic_group` is an alias
    of `quaternion_group`. The two functions are fully identical. We recommend always
    using `quaternion_group`.
```
and then similar for the `is_*` function.

Thoughts @ThomasBreuer @thofma ?
created_at: 2025-03-20T23:55:51Z
author_association: MEMBER
diff_hunk: @@ -844,38 +846,83 @@ julia> relators(g)
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
 quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+quaternion_group(::Type{T}, n::IntegerUnion) where {T<:Union{GAPGroup,PcGroup,SubPcGroup}} =
+  dicyclic_group(T, n)
+
+@doc raw"""
+    is_quaternion_group(G::GAPGroup)
+
+Return `true` if `G` is isomorphic to a (generalized) quaternion group
+of order $2^{k+1}, k \geq 2$, and `false` otherwise.
path: src/Groups/group_constructors.jl
user: login: fingolfin
body: this should also allow order `4k` (if we merge the docstrings as I proposed, this is of course taken care of).
created_at: 2025-03-20T23:57:54Z
author_association: MEMBER
diff_hunk: @@ -827,6 +827,8 @@ as an instance of `T`,
 where `n` is a power of 2 and `T` is in
path: src/Groups/group_constructors.jl
user: login: fingolfin
body: Note that e.g. [Wikipedia](https://en.wikipedia.org/wiki/Quaternion_group#Generalized_quaternion_group) allows "generalized quaternion groups" to have order $4k$.
created_at: 2025-03-20T23:59:28Z
author_association: MEMBER
diff_hunk: @@ -844,38 +846,83 @@ julia> relators(g)
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
 quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+quaternion_group(::Type{T}, n::IntegerUnion) where {T<:Union{GAPGroup,PcGroup,SubPcGroup}} =
+  dicyclic_group(T, n)
+
+@doc raw"""
+    is_quaternion_group(G::GAPGroup)
+
+Return `true` if `G` is isomorphic to a (generalized) quaternion group
+of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+
+# Examples
+```jldoctest
+julia> is_quaternion_group(small_group(8, 3))
+false
+
+julia> is_quaternion_group(transitive_group(8, 5))
+true
+```
+"""
+@gapattribute is_quaternion_group(G::GAPGroup) =
+  GAP.Globals.IsGeneralisedQuaternionGroup(GapObj(G))::Bool
+
+"""
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
+
+Return the dicyclic group of order `n`, as an instance of `T`,
+where `n` is a multiple of 4 and `T` is in
+{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
path: src/Groups/group_constructors.jl
user: login: thofma
body: If it is just an alias, as a user I would not quite understand why you provide an alias that I am happy to find (because "dicyclic group" is the right name in my domain) and use, but then tell me not to use it.

But I don't have a strong opinion on this.
created_at: 2025-03-21T06:57:54Z
author_association: COLLABORATOR
diff_hunk: @@ -611,17 +611,17 @@ julia> gens(free_group([:a, :b], "x" => 1:2, 'y' => (1:2, 1:2)))
 ```
 """
 function free_group(L::Vector{<:Symbol}; eltype::Symbol = :letter)
-   @req allunique(L) "generator names must be unique"
+  @req allunique(L) "generator names must be unique"
    J = GapObj(L, recursive = true)
-   if eltype == :syllable
+  if eltype == :syllable
      G = FPGroup(GAP.Globals.FreeGroup(J; FreeGroupFamilyType = GapObj("syllable"))::GapObj)
path: src/Groups/group_constructors.jl
user: login: ThomasBreuer
body: If the indentation gets changed from 3 to 2 spaces in a lot of places, why not also fo the lines that define `J` and `G`?
created_at: 2025-03-21T10:02:01Z
author_association: MEMBER
diff_hunk: @@ -844,38 +846,83 @@ julia> relators(g)
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
 quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+quaternion_group(::Type{T}, n::IntegerUnion) where {T<:Union{GAPGroup,PcGroup,SubPcGroup}} =
+  dicyclic_group(T, n)
+
+@doc raw"""
+    is_quaternion_group(G::GAPGroup)
+
+Return `true` if `G` is isomorphic to a (generalized) quaternion group
+of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+
+# Examples
+```jldoctest
+julia> is_quaternion_group(small_group(8, 3))
+false
+
+julia> is_quaternion_group(transitive_group(8, 5))
+true
+```
+"""
+@gapattribute is_quaternion_group(G::GAPGroup) =
+  GAP.Globals.IsGeneralisedQuaternionGroup(GapObj(G))::Bool
+
+"""
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
+
+Return the dicyclic group of order `n`, as an instance of `T`,
+where `n` is a multiple of 4 and `T` is in
+{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
+
+# Examples
+```jldoctest
+julia> g = dicyclic_group(12)
+Pc group of order 12
+
+julia> dicyclic_group(PermGroup, 12)
+Permutation group of degree 12
+
+julia> g = dicyclic_group(FPGroup, 12)
+Finitely presented group of order 12
+
+julia> relators(g)
+3-element Vector{FPGroupElem}:
+ r^2*s^-3
+ s^6
+ r^-1*s*r*s
+```
+"""
+dicyclic_group(n::IntegerUnion) = dicyclic_group(PcGroup, n)
+
+function dicyclic_group(::Type{T}, n::IntegerUnion) where {T<:GAPGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(_gap_filter(T), n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(_gap_filter(T), n)::GapObj)
 end
 
 # Delegating to the GAP constructor via `_gap_filter` does not work here.
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
+function dicyclic_group(::Type{T}, n::IntegerUnion) where {T<:Union{PcGroup,SubPcGroup}}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(GAP.Globals.IsPcGroup, n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(GAP.Globals.IsPcGroup, n)::GapObj)
 end
 
 @doc raw"""
-    is_quaternion_group(G::GAPGroup)
+    is_dicyclic_group(G::GAPGroup)
 
-Return `true` if `G` is isomorphic to a (generalized) quaternion group
-of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+Return `true` if `G` is isomorphic to a dicyclic group
+of order $4n, n > 1$, and `false` otherwise.
 
 # Examples
 ```jldoctest
-julia> is_quaternion_group(small_group(8, 3))
+julia> is_dicyclic_group(small_group(8, 3))
 false
 
-julia> is_quaternion_group(small_group(8, 4))
+julia> is_dicyclic_group(small_group(8, 4))
 true
 ```
 """
-@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool
+@gapattribute is_dicyclic_group(G::GAPGroup) =
+  GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool
path: src/Groups/group_constructors.jl
user: login: ThomasBreuer
body: I am getting confused.
GAP does the following:
```
gap> G:= DicyclicGroup(24);;
gap> IsQuaternionGroup(G);
false
gap> G:= DicyclicGroup(32);;
gap> IsQuaternionGroup(G);
true
```
That is, both `QuaternionGroup` and `DicyclicGroup` can be used to create the groups in question,
but `IsQuaternionGroup` regards only the dicyclic groups of 2-power order as (generalized) quaternion groups.
In fact, GAP seems to have no function that checks whether a group is dicyclic.
created_at: 2025-03-21T10:16:41Z
author_association: MEMBER
diff_hunk: @@ -844,38 +846,83 @@ julia> relators(g)
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
 quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+quaternion_group(::Type{T}, n::IntegerUnion) where {T<:Union{GAPGroup,PcGroup,SubPcGroup}} =
+  dicyclic_group(T, n)
+
+@doc raw"""
+    is_quaternion_group(G::GAPGroup)
+
+Return `true` if `G` is isomorphic to a (generalized) quaternion group
+of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+
+# Examples
+```jldoctest
+julia> is_quaternion_group(small_group(8, 3))
+false
+
+julia> is_quaternion_group(transitive_group(8, 5))
+true
+```
+"""
+@gapattribute is_quaternion_group(G::GAPGroup) =
+  GAP.Globals.IsGeneralisedQuaternionGroup(GapObj(G))::Bool
+
+"""
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
+
+Return the dicyclic group of order `n`, as an instance of `T`,
+where `n` is a multiple of 4 and `T` is in
+{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
path: src/Groups/group_constructors.jl
user: login: ThomasBreuer
body: My understanding of the discussion from issue #1630 is that it should be the other way round, that is, we recommend always using `dicyclic_group`.

And yes, I think it makes sense to have just one docstring for both names.
created_at: 2025-03-21T10:19:12Z
author_association: MEMBER
diff_hunk: @@ -844,38 +846,83 @@ julia> relators(g)
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
 quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+quaternion_group(::Type{T}, n::IntegerUnion) where {T<:Union{GAPGroup,PcGroup,SubPcGroup}} =
+  dicyclic_group(T, n)
+
+@doc raw"""
+    is_quaternion_group(G::GAPGroup)
+
+Return `true` if `G` is isomorphic to a (generalized) quaternion group
+of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+
+# Examples
+```jldoctest
+julia> is_quaternion_group(small_group(8, 3))
+false
+
+julia> is_quaternion_group(transitive_group(8, 5))
+true
+```
+"""
+@gapattribute is_quaternion_group(G::GAPGroup) =
+  GAP.Globals.IsGeneralisedQuaternionGroup(GapObj(G))::Bool
+
+"""
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
+
+Return the dicyclic group of order `n`, as an instance of `T`,
+where `n` is a multiple of 4 and `T` is in
+{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
+
+# Examples
+```jldoctest
+julia> g = dicyclic_group(12)
+Pc group of order 12
+
+julia> dicyclic_group(PermGroup, 12)
+Permutation group of degree 12
+
+julia> g = dicyclic_group(FPGroup, 12)
+Finitely presented group of order 12
+
+julia> relators(g)
+3-element Vector{FPGroupElem}:
+ r^2*s^-3
+ s^6
+ r^-1*s*r*s
+```
+"""
+dicyclic_group(n::IntegerUnion) = dicyclic_group(PcGroup, n)
+
+function dicyclic_group(::Type{T}, n::IntegerUnion) where {T<:GAPGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(_gap_filter(T), n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(_gap_filter(T), n)::GapObj)
 end
 
 # Delegating to the GAP constructor via `_gap_filter` does not work here.
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
+function dicyclic_group(::Type{T}, n::IntegerUnion) where {T<:Union{PcGroup,SubPcGroup}}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(GAP.Globals.IsPcGroup, n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(GAP.Globals.IsPcGroup, n)::GapObj)
 end
 
 @doc raw"""
-    is_quaternion_group(G::GAPGroup)
+    is_dicyclic_group(G::GAPGroup)
 
-Return `true` if `G` is isomorphic to a (generalized) quaternion group
-of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+Return `true` if `G` is isomorphic to a dicyclic group
+of order $4n, n > 1$, and `false` otherwise.
 
 # Examples
 ```jldoctest
-julia> is_quaternion_group(small_group(8, 3))
+julia> is_dicyclic_group(small_group(8, 3))
 false
 
-julia> is_quaternion_group(small_group(8, 4))
+julia> is_dicyclic_group(small_group(8, 4))
 true
 ```
 """
-@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool
+@gapattribute is_dicyclic_group(G::GAPGroup) =
+  GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool
path: src/Groups/group_constructors.jl
user: login: ThomasBreuer
body: As far as I see, the GAP function `DoComputeGeneralisedQuaternionGenerators` (which is called by `IsQuaternionGroup`) would work as an `is_dicyclic` check if one just omits the check for 2-power order.
created_at: 2025-03-21T13:35:49Z
author_association: MEMBER
diff_hunk: @@ -821,61 +821,107 @@ false
 @gapattribute is_dihedral_group(G::GAPGroup) = GAP.Globals.IsDihedralGroup(GapObj(G))::Bool
 
 """
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
     quaternion_group(::Type{T} = PcGroup, n::IntegerUnion)
 
-Return the (generalized) quaternion group of order `n`,
-as an instance of `T`,
-where `n` is a power of 2 and `T` is in
-{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
+Return the dicyclic group of order `n`,
+as an instance of `T`, where `n` is a multiple of 4
+and `T` is a suitable group type such as
+`PcGroup`, `SubPcGroup`, `PermGroup`, `FPGroup`, `SubFPGroup`.
+
+!!! note
+    For historical reasons and backwards compatibility, `quaternion_group` is an alias
+    of `dicyclic_group`. The two functions are fully identical. We recommend always
+    using `dicyclic_group`.
 
 # Examples
 ```jldoctest
-julia> g = quaternion_group(8)
+julia> g = dicyclic_group(8)
 Pc group of order 8
 
-julia> quaternion_group(PermGroup, 8)
+julia> dicyclic_group(PermGroup, 8)
 Permutation group of degree 8
 
-julia> g = quaternion_group(FPGroup, 8)
+julia> g = dicyclic_group(FPGroup, 8)
 Finitely presented group of order 8
 
 julia> relators(g)
 3-element Vector{FPGroupElem}:
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
-quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
+dicyclic_group(n::IntegerUnion) = dicyclic_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+function dicyclic_group(::Type{T}, n::IntegerUnion) where {T<:GAPGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(_gap_filter(T), n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(_gap_filter(T), n)::GapObj)
 end
 
 # Delegating to the GAP constructor via `_gap_filter` does not work here.
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
+function dicyclic_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(GAP.Globals.IsPcGroup, n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(GAP.Globals.IsPcGroup, n)::GapObj)
 end
 
 @doc raw"""
-    is_quaternion_group(G::GAPGroup)
+    is_dicyclic_group(G::GAPGroup)
 
-Return `true` if `G` is isomorphic to a (generalized) quaternion group
-of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+Return `true` if `G` is isomorphic to a dicyclic group
+of order $4k, k > 1$, and `false` otherwise.
 
 # Examples
 ```jldoctest
-julia> is_quaternion_group(small_group(8, 3))
+julia> is_dicyclic_group(small_group(8, 3))
 false
 
-julia> is_quaternion_group(small_group(8, 4))
+julia> is_dicyclic_group(transitive_group(8, 5))
 true
 ```
 """
-@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool
+is_dicyclic_group(G::GAPGroup) = _is_dicyclic_group(G)
+
+@doc raw"""
+    is_quaternion_group(G::GAPGroup)
+
+Return `true` if `G` is isomorphic to a generalised quaternion group
+of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+"""
+@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsGeneralisedQuaternionGroup(GapObj(G))
+
+# TODO: Remove once IsDicyclicGroup from GAP is available.
+function _is_dicyclic_group(G::GAPGroup)
+  N = order(G)
+  !iszero(mod(N, 4)) && return false
+  
+  n = N//2
+  a = n//2
path: src/Groups/group_constructors.jl
user: login: fingolfin
body: This creates rationals but since the numbers are even we are allowed to divide, so we should be able to do this:
```suggestion
  n = quo(N,2)
  a = quo(n,2)
```
created_at: 2025-04-02T13:28:51Z
author_association: MEMBER
diff_hunk: @@ -821,61 +821,107 @@ false
 @gapattribute is_dihedral_group(G::GAPGroup) = GAP.Globals.IsDihedralGroup(GapObj(G))::Bool
 
 """
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
     quaternion_group(::Type{T} = PcGroup, n::IntegerUnion)
 
-Return the (generalized) quaternion group of order `n`,
-as an instance of `T`,
-where `n` is a power of 2 and `T` is in
-{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
+Return the dicyclic group of order `n`,
+as an instance of `T`, where `n` is a multiple of 4
+and `T` is a suitable group type such as
+`PcGroup`, `SubPcGroup`, `PermGroup`, `FPGroup`, `SubFPGroup`.
+
+!!! note
+    For historical reasons and backwards compatibility, `quaternion_group` is an alias
+    of `dicyclic_group`. The two functions are fully identical. We recommend always
+    using `dicyclic_group`.
 
 # Examples
 ```jldoctest
-julia> g = quaternion_group(8)
+julia> g = dicyclic_group(8)
 Pc group of order 8
 
-julia> quaternion_group(PermGroup, 8)
+julia> dicyclic_group(PermGroup, 8)
 Permutation group of degree 8
 
-julia> g = quaternion_group(FPGroup, 8)
+julia> g = dicyclic_group(FPGroup, 8)
 Finitely presented group of order 8
 
 julia> relators(g)
 3-element Vector{FPGroupElem}:
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
-quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
+dicyclic_group(n::IntegerUnion) = dicyclic_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+function dicyclic_group(::Type{T}, n::IntegerUnion) where {T<:GAPGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(_gap_filter(T), n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(_gap_filter(T), n)::GapObj)
 end
 
 # Delegating to the GAP constructor via `_gap_filter` does not work here.
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
+function dicyclic_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(GAP.Globals.IsPcGroup, n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(GAP.Globals.IsPcGroup, n)::GapObj)
 end
 
 @doc raw"""
-    is_quaternion_group(G::GAPGroup)
+    is_dicyclic_group(G::GAPGroup)
 
-Return `true` if `G` is isomorphic to a (generalized) quaternion group
-of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+Return `true` if `G` is isomorphic to a dicyclic group
+of order $4k, k > 1$, and `false` otherwise.
 
 # Examples
 ```jldoctest
-julia> is_quaternion_group(small_group(8, 3))
+julia> is_dicyclic_group(small_group(8, 3))
 false
 
-julia> is_quaternion_group(small_group(8, 4))
+julia> is_dicyclic_group(transitive_group(8, 5))
 true
 ```
 """
-@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool
+is_dicyclic_group(G::GAPGroup) = _is_dicyclic_group(G)
+
+@doc raw"""
+    is_quaternion_group(G::GAPGroup)
+
+Return `true` if `G` is isomorphic to a generalised quaternion group
+of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+"""
+@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsGeneralisedQuaternionGroup(GapObj(G))
+
+# TODO: Remove once IsDicyclicGroup from GAP is available.
+function _is_dicyclic_group(G::GAPGroup)
+  N = order(G)
+  !iszero(mod(N, 4)) && return false
+  
+  n = N//2
+  a = n//2
+
+  G1, _ = derived_subgroup(G)
+  !(is_cyclic(G1) && order(G1) == a) && return false
path: src/Groups/group_constructors.jl
user: login: fingolfin
body: This may be a matter of taste, so take with a grain of salt; but I find this easier to understand:
```suggestion
  (is_cyclic(G1) && order(G1) == a) || return false
```
I read this as "either G1 is cyclic and has order a, or else we return `false`

Feelfree to ignore this!
created_at: 2025-04-02T13:30:34Z
author_association: MEMBER
diff_hunk: @@ -821,61 +821,107 @@ false
 @gapattribute is_dihedral_group(G::GAPGroup) = GAP.Globals.IsDihedralGroup(GapObj(G))::Bool
 
 """
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
     quaternion_group(::Type{T} = PcGroup, n::IntegerUnion)
 
-Return the (generalized) quaternion group of order `n`,
-as an instance of `T`,
-where `n` is a power of 2 and `T` is in
-{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
+Return the dicyclic group of order `n`,
+as an instance of `T`, where `n` is a multiple of 4
+and `T` is a suitable group type such as
+`PcGroup`, `SubPcGroup`, `PermGroup`, `FPGroup`, `SubFPGroup`.
+
+!!! note
+    For historical reasons and backwards compatibility, `quaternion_group` is an alias
+    of `dicyclic_group`. The two functions are fully identical. We recommend always
+    using `dicyclic_group`.
 
 # Examples
 ```jldoctest
-julia> g = quaternion_group(8)
+julia> g = dicyclic_group(8)
 Pc group of order 8
 
-julia> quaternion_group(PermGroup, 8)
+julia> dicyclic_group(PermGroup, 8)
 Permutation group of degree 8
 
-julia> g = quaternion_group(FPGroup, 8)
+julia> g = dicyclic_group(FPGroup, 8)
 Finitely presented group of order 8
 
 julia> relators(g)
 3-element Vector{FPGroupElem}:
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
-quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
+dicyclic_group(n::IntegerUnion) = dicyclic_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+function dicyclic_group(::Type{T}, n::IntegerUnion) where {T<:GAPGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(_gap_filter(T), n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(_gap_filter(T), n)::GapObj)
 end
 
 # Delegating to the GAP constructor via `_gap_filter` does not work here.
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
+function dicyclic_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(GAP.Globals.IsPcGroup, n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(GAP.Globals.IsPcGroup, n)::GapObj)
 end
 
 @doc raw"""
-    is_quaternion_group(G::GAPGroup)
+    is_dicyclic_group(G::GAPGroup)
 
-Return `true` if `G` is isomorphic to a (generalized) quaternion group
-of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+Return `true` if `G` is isomorphic to a dicyclic group
+of order $4k, k > 1$, and `false` otherwise.
 
 # Examples
 ```jldoctest
-julia> is_quaternion_group(small_group(8, 3))
+julia> is_dicyclic_group(small_group(8, 3))
 false
 
-julia> is_quaternion_group(small_group(8, 4))
+julia> is_dicyclic_group(transitive_group(8, 5))
 true
 ```
 """
-@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool
+is_dicyclic_group(G::GAPGroup) = _is_dicyclic_group(G)
+
+@doc raw"""
+    is_quaternion_group(G::GAPGroup)
+
+Return `true` if `G` is isomorphic to a generalised quaternion group
+of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+"""
+@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsGeneralisedQuaternionGroup(GapObj(G))
+
+# TODO: Remove once IsDicyclicGroup from GAP is available.
+function _is_dicyclic_group(G::GAPGroup)
+  N = order(G)
+  !iszero(mod(N, 4)) && return false
+  
+  n = N//2
+  a = n//2
+
+  G1, _ = derived_subgroup(G)
+  !(is_cyclic(G1) && order(G1) == a) && return false
+
+  local Zn
+  T = right_transversal(G, G1)
+  i = 1
+  while true
+    H = GAP.Globals.ClosureGroup(GapObj(G1), GapObj(T[i]))
path: src/Groups/group_constructors.jl
user: login: fingolfin
body: This line motivated me to write issue #4777 ;-)
created_at: 2025-04-02T13:43:40Z
author_association: MEMBER
diff_hunk: @@ -821,61 +821,107 @@ false
 @gapattribute is_dihedral_group(G::GAPGroup) = GAP.Globals.IsDihedralGroup(GapObj(G))::Bool
 
 """
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
     quaternion_group(::Type{T} = PcGroup, n::IntegerUnion)
 
-Return the (generalized) quaternion group of order `n`,
-as an instance of `T`,
-where `n` is a power of 2 and `T` is in
-{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
+Return the dicyclic group of order `n`,
+as an instance of `T`, where `n` is a multiple of 4
+and `T` is a suitable group type such as
+`PcGroup`, `SubPcGroup`, `PermGroup`, `FPGroup`, `SubFPGroup`.
+
+!!! note
+    For historical reasons and backwards compatibility, `quaternion_group` is an alias
+    of `dicyclic_group`. The two functions are fully identical. We recommend always
+    using `dicyclic_group`.
 
 # Examples
 ```jldoctest
-julia> g = quaternion_group(8)
+julia> g = dicyclic_group(8)
 Pc group of order 8
 
-julia> quaternion_group(PermGroup, 8)
+julia> dicyclic_group(PermGroup, 8)
 Permutation group of degree 8
 
-julia> g = quaternion_group(FPGroup, 8)
+julia> g = dicyclic_group(FPGroup, 8)
 Finitely presented group of order 8
 
 julia> relators(g)
 3-element Vector{FPGroupElem}:
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
-quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
+dicyclic_group(n::IntegerUnion) = dicyclic_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+function dicyclic_group(::Type{T}, n::IntegerUnion) where {T<:GAPGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(_gap_filter(T), n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(_gap_filter(T), n)::GapObj)
 end
 
 # Delegating to the GAP constructor via `_gap_filter` does not work here.
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
+function dicyclic_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(GAP.Globals.IsPcGroup, n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(GAP.Globals.IsPcGroup, n)::GapObj)
 end
 
 @doc raw"""
-    is_quaternion_group(G::GAPGroup)
+    is_dicyclic_group(G::GAPGroup)
 
-Return `true` if `G` is isomorphic to a (generalized) quaternion group
-of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+Return `true` if `G` is isomorphic to a dicyclic group
+of order $4k, k > 1$, and `false` otherwise.
 
 # Examples
 ```jldoctest
-julia> is_quaternion_group(small_group(8, 3))
+julia> is_dicyclic_group(small_group(8, 3))
 false
 
-julia> is_quaternion_group(small_group(8, 4))
+julia> is_dicyclic_group(transitive_group(8, 5))
 true
 ```
 """
-@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool
+is_dicyclic_group(G::GAPGroup) = _is_dicyclic_group(G)
+
+@doc raw"""
+    is_quaternion_group(G::GAPGroup)
+
+Return `true` if `G` is isomorphic to a generalised quaternion group
+of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+"""
+@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsGeneralisedQuaternionGroup(GapObj(G))
+
+# TODO: Remove once IsDicyclicGroup from GAP is available.
+function _is_dicyclic_group(G::GAPGroup)
+  N = order(G)
+  !iszero(mod(N, 4)) && return false
+  
+  n = N//2
+  a = n//2
+
+  G1, _ = derived_subgroup(G)
+  !(is_cyclic(G1) && order(G1) == a) && return false
+
+  local Zn
+  T = right_transversal(G, G1)
+  i = 1
+  while true
+    H = GAP.Globals.ClosureGroup(GapObj(G1), GapObj(T[i]))
+    Zn, _ = Oscar._as_subgroup(G1, H)
+    i = i + 1
+    if (i > 4) || (is_cyclic(Zn) && order(Zn) == n)
path: src/Groups/group_constructors.jl
user: login: fingolfin
body: I guess we could do away with `i` and just do this? But it would also make the code differ more from the GAP version, so perhaps not worth it anyway
```suggestion
  @assert length(T) == 4
  for x in T
    H = GAP.Globals.ClosureGroup(GapObj(G1), GapObj(x))
    Zn, _ = Oscar._as_subgroup(G1, H)
    if is_cyclic(Zn) && order(Zn) == n
```
created_at: 2025-04-02T13:47:33Z
author_association: MEMBER
diff_hunk: @@ -844,38 +846,83 @@ julia> relators(g)
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
 quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+quaternion_group(::Type{T}, n::IntegerUnion) where {T<:Union{GAPGroup,PcGroup,SubPcGroup}} =
+  dicyclic_group(T, n)
+
+@doc raw"""
+    is_quaternion_group(G::GAPGroup)
+
+Return `true` if `G` is isomorphic to a (generalized) quaternion group
+of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+
+# Examples
+```jldoctest
+julia> is_quaternion_group(small_group(8, 3))
+false
+
+julia> is_quaternion_group(transitive_group(8, 5))
+true
+```
+"""
+@gapattribute is_quaternion_group(G::GAPGroup) =
+  GAP.Globals.IsGeneralisedQuaternionGroup(GapObj(G))::Bool
+
+"""
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
+
+Return the dicyclic group of order `n`, as an instance of `T`,
+where `n` is a multiple of 4 and `T` is in
+{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
path: src/Groups/group_constructors.jl
user: login: fingolfin
body: I am fine with not making a recommendation, my main concern is that I think it'd be nice to mention why we have two names.
created_at: 2025-04-02T14:06:29Z
author_association: MEMBER
diff_hunk: @@ -821,61 +821,107 @@ false
 @gapattribute is_dihedral_group(G::GAPGroup) = GAP.Globals.IsDihedralGroup(GapObj(G))::Bool
 
 """
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
     quaternion_group(::Type{T} = PcGroup, n::IntegerUnion)
 
-Return the (generalized) quaternion group of order `n`,
-as an instance of `T`,
-where `n` is a power of 2 and `T` is in
-{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
+Return the dicyclic group of order `n`,
+as an instance of `T`, where `n` is a multiple of 4
+and `T` is a suitable group type such as
+`PcGroup`, `SubPcGroup`, `PermGroup`, `FPGroup`, `SubFPGroup`.
+
+!!! note
+    For historical reasons and backwards compatibility, `quaternion_group` is an alias
+    of `dicyclic_group`. The two functions are fully identical. We recommend always
+    using `dicyclic_group`.
path: src/Groups/group_constructors.jl
user: login: fingolfin
body: ```suggestion
    of `dicyclic_group`. The two functions are fully identical.
```
created_at: 2025-04-02T14:08:03Z
author_association: MEMBER
diff_hunk: @@ -821,61 +821,107 @@ false
 @gapattribute is_dihedral_group(G::GAPGroup) = GAP.Globals.IsDihedralGroup(GapObj(G))::Bool
 
 """
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
     quaternion_group(::Type{T} = PcGroup, n::IntegerUnion)
 
-Return the (generalized) quaternion group of order `n`,
-as an instance of `T`,
-where `n` is a power of 2 and `T` is in
-{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
+Return the dicyclic group of order `n`,
+as an instance of `T`, where `n` is a multiple of 4
+and `T` is a suitable group type such as
+`PcGroup`, `SubPcGroup`, `PermGroup`, `FPGroup`, `SubFPGroup`.
+
+!!! note
+    For historical reasons and backwards compatibility, `quaternion_group` is an alias
+    of `dicyclic_group`. The two functions are fully identical. We recommend always
+    using `dicyclic_group`.
 
 # Examples
 ```jldoctest
-julia> g = quaternion_group(8)
+julia> g = dicyclic_group(8)
 Pc group of order 8
 
-julia> quaternion_group(PermGroup, 8)
+julia> dicyclic_group(PermGroup, 8)
 Permutation group of degree 8
 
-julia> g = quaternion_group(FPGroup, 8)
+julia> g = dicyclic_group(FPGroup, 8)
 Finitely presented group of order 8
 
 julia> relators(g)
 3-element Vector{FPGroupElem}:
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
-quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
+dicyclic_group(n::IntegerUnion) = dicyclic_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+function dicyclic_group(::Type{T}, n::IntegerUnion) where {T<:GAPGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(_gap_filter(T), n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(_gap_filter(T), n)::GapObj)
 end
 
 # Delegating to the GAP constructor via `_gap_filter` does not work here.
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
+function dicyclic_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(GAP.Globals.IsPcGroup, n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(GAP.Globals.IsPcGroup, n)::GapObj)
 end
 
 @doc raw"""
-    is_quaternion_group(G::GAPGroup)
+    is_dicyclic_group(G::GAPGroup)
 
-Return `true` if `G` is isomorphic to a (generalized) quaternion group
-of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+Return `true` if `G` is isomorphic to a dicyclic group
+of order $4k, k > 1$, and `false` otherwise.
 
 # Examples
 ```jldoctest
-julia> is_quaternion_group(small_group(8, 3))
+julia> is_dicyclic_group(small_group(8, 3))
 false
 
-julia> is_quaternion_group(small_group(8, 4))
+julia> is_dicyclic_group(transitive_group(8, 5))
 true
 ```
 """
-@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool
+is_dicyclic_group(G::GAPGroup) = _is_dicyclic_group(G)
+
+@doc raw"""
+    is_quaternion_group(G::GAPGroup)
+
+Return `true` if `G` is isomorphic to a generalised quaternion group
+of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+"""
+@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsGeneralisedQuaternionGroup(GapObj(G))
+
+# TODO: Remove once IsDicyclicGroup from GAP is available.
+function _is_dicyclic_group(G::GAPGroup)
+  N = order(G)
+  !iszero(mod(N, 4)) && return false
+  
+  n = N//2
+  a = n//2
path: src/Groups/group_constructors.jl
user: login: lgoettgens
body: I think you mean `div` instead of `quo`
created_at: 2025-04-03T10:12:46Z
author_association: MEMBER
diff_hunk: @@ -65,6 +65,10 @@ end
   @test isa(dihedral_group(PermGroup, 6), PermGroup)
 
   @test is_quaternion_group(small_group(8, 4))
+  @test ! is_quaternion_group(small_group(12, 3))
+  @test is_dicyclic_group(small_group(8, 4))
+  @test ! is_dicyclic_group(small_group(13, 1))
path: test/Groups/constructors.jl
user: login: ThomasBreuer
body: A test where `is_dicyclic_group(G)` is `true`  but `is_quaternion_group(G)` is `false` would be useful, for example
```
G = small_group(12, 1);
@test is_dicyclic_group(G) && !is_quaternion_group(G)
```
(This was the motivation for adding `IsDicyclicGroup` to GAP.)
created_at: 2025-04-04T11:24:56Z
author_association: MEMBER
diff_hunk: @@ -821,61 +821,101 @@ false
 @gapattribute is_dihedral_group(G::GAPGroup) = GAP.Globals.IsDihedralGroup(GapObj(G))::Bool
 
 """
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
     quaternion_group(::Type{T} = PcGroup, n::IntegerUnion)
 
-Return the (generalized) quaternion group of order `n`,
-as an instance of `T`,
-where `n` is a power of 2 and `T` is in
-{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
+Return the dicyclic group of order `n`,
+as an instance of `T`, where `n` is a multiple of 4
+and `T` is a suitable group type such as
+`PcGroup`, `SubPcGroup`, `PermGroup`, `FPGroup`, `SubFPGroup`.
+
+!!! note
+    For historical reasons and backwards compatibility, `quaternion_group` is an alias
+    of `dicyclic_group`. The two functions are fully identical.
 
 # Examples
 ```jldoctest
-julia> g = quaternion_group(8)
+julia> g = dicyclic_group(8)
 Pc group of order 8
 
-julia> quaternion_group(PermGroup, 8)
+julia> dicyclic_group(PermGroup, 8)
 Permutation group of degree 8
 
-julia> g = quaternion_group(FPGroup, 8)
+julia> g = dicyclic_group(FPGroup, 8)
 Finitely presented group of order 8
 
 julia> relators(g)
 3-element Vector{FPGroupElem}:
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
-quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
+dicyclic_group(n::IntegerUnion) = dicyclic_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+function dicyclic_group(::Type{T}, n::IntegerUnion) where {T<:GAPGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(_gap_filter(T), n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(_gap_filter(T), n)::GapObj)
 end
 
 # Delegating to the GAP constructor via `_gap_filter` does not work here.
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
+function dicyclic_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(GAP.Globals.IsPcGroup, n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(GAP.Globals.IsPcGroup, n)::GapObj)
 end
 
 @doc raw"""
+    is_dicyclic_group(G::GAPGroup)
     is_quaternion_group(G::GAPGroup)
 
-Return `true` if `G` is isomorphic to a (generalized) quaternion group
-of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+Return `true` if `G` is isomorphic to a dicyclic group
+of order $4k, k > 1$, and `false` otherwise.
 
 # Examples
 ```jldoctest
-julia> is_quaternion_group(small_group(8, 3))
+julia> is_dicyclic_group(small_group(8, 3))
 false
 
-julia> is_quaternion_group(small_group(8, 4))
+julia> is_dicyclic_group(transitive_group(8, 5))
 true
 ```
 """
-@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool
+is_dicyclic_group(G::GAPGroup) = _is_dicyclic_group(G)
+
+# TODO: Remove once IsDicyclicGroup from GAP is available.
+function _is_dicyclic_group(G::GAPGroup)
+  N = order(G)
+  !iszero(mod(N, 4)) && return false
+  
+  n = div(N, 2)
+  a = div(n, 2)
+
+  G1, _ = derived_subgroup(G)
+  (is_cyclic(G1) && order(G1) == a) || return false
+
+  local Zn
+  T = right_transversal(G, G1)
+  @assert length(T) == 4
+  for x in T
+    H = GAP.Globals.ClosureGroup(GapObj(G1), GapObj(x))
+    Zn, _ = Oscar._as_subgroup(G1, H)
+    if is_cyclic(Zn) && order(Zn) == n
+      break
+    end
+  end
+
+  (is_cyclic(Zn) && order(Zn) == n) || return false
+
+  local t = one(G)
+  while t in Zn
+    t = rand(G)
+  end
+
+  !(order(t) == 4 && all(s -> s^t*s == s^0, gens(Zn))) && return false
+
+  # Different from GAP code, here we skip finding other generator.
+  return true
+end
+
+const quaternion_group = dicyclic_group
+const is_quaternion_group = is_dicyclic_group
path: src/Groups/group_constructors.jl
user: login: lgoettgens
body: if you use `@alias` here, the second function also gets a docstring.
created_at: 2025-04-16T17:28:55Z
author_association: MEMBER
diff_hunk: @@ -821,61 +821,101 @@ false
 @gapattribute is_dihedral_group(G::GAPGroup) = GAP.Globals.IsDihedralGroup(GapObj(G))::Bool
 
 """
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
     quaternion_group(::Type{T} = PcGroup, n::IntegerUnion)
 
-Return the (generalized) quaternion group of order `n`,
-as an instance of `T`,
-where `n` is a power of 2 and `T` is in
-{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
+Return the dicyclic group of order `n`,
+as an instance of `T`, where `n` is a multiple of 4
+and `T` is a suitable group type such as
+`PcGroup`, `SubPcGroup`, `PermGroup`, `FPGroup`, `SubFPGroup`.
+
+!!! note
+    For historical reasons and backwards compatibility, `quaternion_group` is an alias
+    of `dicyclic_group`. The two functions are fully identical.
 
 # Examples
 ```jldoctest
-julia> g = quaternion_group(8)
+julia> g = dicyclic_group(8)
 Pc group of order 8
 
-julia> quaternion_group(PermGroup, 8)
+julia> dicyclic_group(PermGroup, 8)
 Permutation group of degree 8
 
-julia> g = quaternion_group(FPGroup, 8)
+julia> g = dicyclic_group(FPGroup, 8)
 Finitely presented group of order 8
 
 julia> relators(g)
 3-element Vector{FPGroupElem}:
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
-quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
+dicyclic_group(n::IntegerUnion) = dicyclic_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+function dicyclic_group(::Type{T}, n::IntegerUnion) where {T<:GAPGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(_gap_filter(T), n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(_gap_filter(T), n)::GapObj)
 end
 
 # Delegating to the GAP constructor via `_gap_filter` does not work here.
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
+function dicyclic_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(GAP.Globals.IsPcGroup, n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(GAP.Globals.IsPcGroup, n)::GapObj)
 end
 
 @doc raw"""
+    is_dicyclic_group(G::GAPGroup)
     is_quaternion_group(G::GAPGroup)
 
-Return `true` if `G` is isomorphic to a (generalized) quaternion group
-of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+Return `true` if `G` is isomorphic to a dicyclic group
+of order $4k, k > 1$, and `false` otherwise.
 
 # Examples
 ```jldoctest
-julia> is_quaternion_group(small_group(8, 3))
+julia> is_dicyclic_group(small_group(8, 3))
 false
 
-julia> is_quaternion_group(small_group(8, 4))
+julia> is_dicyclic_group(transitive_group(8, 5))
 true
 ```
 """
-@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool
+is_dicyclic_group(G::GAPGroup) = _is_dicyclic_group(G)
+
+# TODO: Remove once IsDicyclicGroup from GAP is available.
+function _is_dicyclic_group(G::GAPGroup)
+  N = order(G)
+  !iszero(mod(N, 4)) && return false
+  
+  n = div(N, 2)
+  a = div(n, 2)
+
+  G1, _ = derived_subgroup(G)
+  (is_cyclic(G1) && order(G1) == a) || return false
+
+  local Zn
+  T = right_transversal(G, G1)
+  @assert length(T) == 4
+  for x in T
+    H = GAP.Globals.ClosureGroup(GapObj(G1), GapObj(x))
+    Zn, _ = Oscar._as_subgroup(G1, H)
+    if is_cyclic(Zn) && order(Zn) == n
+      break
+    end
+  end
+
+  (is_cyclic(Zn) && order(Zn) == n) || return false
+
+  local t = one(G)
+  while t in Zn
+    t = rand(G)
+  end
+
+  !(order(t) == 4 && all(s -> s^t*s == s^0, gens(Zn))) && return false
+
+  # Different from GAP code, here we skip finding other generator.
+  return true
+end
+
+const quaternion_group = dicyclic_group
+const is_quaternion_group = is_dicyclic_group
path: src/Groups/group_constructors.jl
user: login: jamesnohilly
body: Great, thanks!
created_at: 2025-04-17T11:14:09Z
author_association: COLLABORATOR
diff_hunk: @@ -821,61 +821,101 @@ false
 @gapattribute is_dihedral_group(G::GAPGroup) = GAP.Globals.IsDihedralGroup(GapObj(G))::Bool
 
 """
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
     quaternion_group(::Type{T} = PcGroup, n::IntegerUnion)
 
-Return the (generalized) quaternion group of order `n`,
-as an instance of `T`,
-where `n` is a power of 2 and `T` is in
-{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
+Return the dicyclic group of order `n`,
+as an instance of `T`, where `n` is a multiple of 4
+and `T` is a suitable group type such as
+`PcGroup`, `SubPcGroup`, `PermGroup`, `FPGroup`, `SubFPGroup`.
+
+!!! note
+    For historical reasons and backwards compatibility, `quaternion_group` is an alias
+    of `dicyclic_group`. The two functions are fully identical.
path: src/Groups/group_constructors.jl
user: login: fingolfin
body: ```suggestion

!!! note
    In the literature, these groups are sometimes called dicyclic groups and
    sometimes (generalized) quaternion groups. For user convenience we support
    both names. However, the two functions are fully identical. 
```
created_at: 2025-04-30T08:29:13Z
author_association: MEMBER
diff_hunk: @@ -821,61 +821,101 @@ false
 @gapattribute is_dihedral_group(G::GAPGroup) = GAP.Globals.IsDihedralGroup(GapObj(G))::Bool
 
 """
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
     quaternion_group(::Type{T} = PcGroup, n::IntegerUnion)
 
-Return the (generalized) quaternion group of order `n`,
-as an instance of `T`,
-where `n` is a power of 2 and `T` is in
-{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
+Return the dicyclic group of order `n`,
+as an instance of `T`, where `n` is a multiple of 4
+and `T` is a suitable group type such as
+`PcGroup`, `SubPcGroup`, `PermGroup`, `FPGroup`, `SubFPGroup`.
+
+!!! note
+    For historical reasons and backwards compatibility, `quaternion_group` is an alias
+    of `dicyclic_group`. The two functions are fully identical.
 
 # Examples
 ```jldoctest
-julia> g = quaternion_group(8)
+julia> g = dicyclic_group(8)
 Pc group of order 8
 
-julia> quaternion_group(PermGroup, 8)
+julia> dicyclic_group(PermGroup, 8)
 Permutation group of degree 8
 
-julia> g = quaternion_group(FPGroup, 8)
+julia> g = dicyclic_group(FPGroup, 8)
 Finitely presented group of order 8
 
 julia> relators(g)
 3-element Vector{FPGroupElem}:
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
-quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
+dicyclic_group(n::IntegerUnion) = dicyclic_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+function dicyclic_group(::Type{T}, n::IntegerUnion) where {T<:GAPGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(_gap_filter(T), n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(_gap_filter(T), n)::GapObj)
 end
 
 # Delegating to the GAP constructor via `_gap_filter` does not work here.
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
+function dicyclic_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(GAP.Globals.IsPcGroup, n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(GAP.Globals.IsPcGroup, n)::GapObj)
 end
 
 @doc raw"""
+    is_dicyclic_group(G::GAPGroup)
     is_quaternion_group(G::GAPGroup)
 
-Return `true` if `G` is isomorphic to a (generalized) quaternion group
-of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+Return `true` if `G` is isomorphic to a dicyclic group
+of order $4k, k > 1$, and `false` otherwise.
 
path: src/Groups/group_constructors.jl
user: login: fingolfin
body: ```suggestion

!!! note
    In the literature, these groups are sometimes called dicyclic groups and
    sometimes (generalized) quaternion groups. For user convenience we support
    both names. However, the two functions are fully identical.
    
    If you wish to check that `G` is a quaternion group in a stricter sense,
    e.g. that its order exactly 8 resp. a power of two, you can check
    check the order explicitly resp. use `is_pgroup`.

```
created_at: 2025-04-30T08:30:19Z
author_association: MEMBER
diff_hunk: @@ -821,61 +821,101 @@ false
 @gapattribute is_dihedral_group(G::GAPGroup) = GAP.Globals.IsDihedralGroup(GapObj(G))::Bool
 
 """
+    dicyclic_group(::Type{T} = PcGroup, n::IntegerUnion)
     quaternion_group(::Type{T} = PcGroup, n::IntegerUnion)
 
-Return the (generalized) quaternion group of order `n`,
-as an instance of `T`,
-where `n` is a power of 2 and `T` is in
-{`PcGroup`, `SubPcGroup`, `PermGroup`,`FPGroup`, `SubFPGroup`}.
+Return the dicyclic group of order `n`,
+as an instance of `T`, where `n` is a multiple of 4
+and `T` is a suitable group type such as
+`PcGroup`, `SubPcGroup`, `PermGroup`, `FPGroup`, `SubFPGroup`.
+
+!!! note
+    For historical reasons and backwards compatibility, `quaternion_group` is an alias
+    of `dicyclic_group`. The two functions are fully identical.
 
 # Examples
 ```jldoctest
-julia> g = quaternion_group(8)
+julia> g = dicyclic_group(8)
 Pc group of order 8
 
-julia> quaternion_group(PermGroup, 8)
+julia> dicyclic_group(PermGroup, 8)
 Permutation group of degree 8
 
-julia> g = quaternion_group(FPGroup, 8)
+julia> g = dicyclic_group(FPGroup, 8)
 Finitely presented group of order 8
 
 julia> relators(g)
 3-element Vector{FPGroupElem}:
  r^2*s^-2
  s^4
  r^-1*s*r*s
-
 ```
 """
-quaternion_group(n::IntegerUnion) = quaternion_group(PcGroup, n)
+dicyclic_group(n::IntegerUnion) = dicyclic_group(PcGroup, n)
 
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: GAPGroup
-  # FIXME: resolve naming: dicyclic vs (generalized) quaternion: only the
-  # former should be for any n divisible by 4; the latter only for powers of 2.
-  # see also debate on the GAP side (https://github.com/gap-system/gap/issues/2725)
+function dicyclic_group(::Type{T}, n::IntegerUnion) where {T<:GAPGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(_gap_filter(T), n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(_gap_filter(T), n)::GapObj)
 end
 
 # Delegating to the GAP constructor via `_gap_filter` does not work here.
-function quaternion_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
+function dicyclic_group(::Type{T}, n::IntegerUnion) where T <: Union{PcGroup, SubPcGroup}
   @assert iszero(mod(n, 4))
-  return T(GAP.Globals.QuaternionGroup(GAP.Globals.IsPcGroup, n)::GapObj)
+  return T(GAP.Globals.DicyclicGroup(GAP.Globals.IsPcGroup, n)::GapObj)
 end
 
 @doc raw"""
+    is_dicyclic_group(G::GAPGroup)
     is_quaternion_group(G::GAPGroup)
 
-Return `true` if `G` is isomorphic to a (generalized) quaternion group
-of order $2^{k+1}, k \geq 2$, and `false` otherwise.
+Return `true` if `G` is isomorphic to a dicyclic group
+of order $4k, k > 1$, and `false` otherwise.
 
 # Examples
 ```jldoctest
-julia> is_quaternion_group(small_group(8, 3))
+julia> is_dicyclic_group(small_group(8, 3))
 false
 
-julia> is_quaternion_group(small_group(8, 4))
+julia> is_dicyclic_group(transitive_group(8, 5))
 true
 ```
 """
-@gapattribute is_quaternion_group(G::GAPGroup) = GAP.Globals.IsQuaternionGroup(GapObj(G))::Bool
+is_dicyclic_group(G::GAPGroup) = _is_dicyclic_group(G)
+
+# TODO: Remove once IsDicyclicGroup from GAP is available.
path: src/Groups/group_constructors.jl
user: login: fingolfin
body: Our code here works fine, I see now reason to remove it again later just because GAP also learns to do this...
```suggestion
```
created_at: 2025-04-30T08:32:43Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-03-12T13:54:35Z
body: ## [Codecov](https://app.codecov.io/gh/oscar-system/Oscar.jl/pull/4661?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oscar-system) Report
Attention: Patch coverage is `91.89189%` with `6 lines` in your changes missing coverage. Please review.
> Project coverage is 84.77%. Comparing base [(`4bccbc1`)](https://app.codecov.io/gh/oscar-system/Oscar.jl/commit/4bccbc1156a287d663e67bd8b7f1b6d2b6fdefd4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oscar-system) to head [(`ca99ed5`)](https://app.codecov.io/gh/oscar-system/Oscar.jl/commit/ca99ed51fd3d6da8fc1aba1186ba01d5caf7e2fd?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oscar-system).
> Report is 64 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/oscar-system/Oscar.jl/pull/4661?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oscar-system) | Patch % | Lines |
|---|---|---|
| [src/Groups/group\_constructors.jl](https://app.codecov.io/gh/oscar-system/Oscar.jl/pull/4661?src=pr&el=tree&filepath=src%2FGroups%2Fgroup_constructors.jl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oscar-system#diff-c3JjL0dyb3Vwcy9ncm91cF9jb25zdHJ1Y3RvcnMuamw=) | 91.89% | [6 Missing :warning: ](https://app.codecov.io/gh/oscar-system/Oscar.jl/pull/4661?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oscar-system) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #4661      +/-   ##
==========================================
+ Coverage   84.38%   84.77%   +0.39%     
==========================================
  Files         673      681       +8     
  Lines       90284    91838    +1554     
==========================================
+ Hits        76185    77859    +1674     
+ Misses      14099    13979     -120     
```

| [Files with missing lines](https://app.codecov.io/gh/oscar-system/Oscar.jl/pull/4661?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oscar-system) | Coverage Δ | |
|---|---|---|
| [src/Groups/group\_constructors.jl](https://app.codecov.io/gh/oscar-system/Oscar.jl/pull/4661?src=pr&el=tree&filepath=src%2FGroups%2Fgroup_constructors.jl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oscar-system#diff-c3JjL0dyb3Vwcy9ncm91cF9jb25zdHJ1Y3RvcnMuamw=) | `93.65% <91.89%> (+0.27%)` | :arrow_up: |

... and [96 files with indirect coverage changes](https://app.codecov.io/gh/oscar-system/Oscar.jl/pull/4661/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oscar-system)

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: ThomasBreuer
created_at: 2025-03-31T07:39:14Z
body: How shall we proceed?
Meanwhile gap-system/gap/pull/5966 got merged, do we want to wait until Oscar can use it, or do we want to add Oscar code that provides the same functionality?
author_association: MEMBER
user: login: jamesnohilly
created_at: 2025-03-31T08:03:10Z
body: I have now merged the docstrings of `dicyclic_group` and `quaternion_group`, with main preference on `dicyclic_group` as in #1630.

As for the `is_{dicyclic,quaternion}_group` issue, we could wait for a new `GAP.jl` release to use these new methods, however I am unsure. Adding this functionality to OSCAR and then eventually replacing is an option aswell.
author_association: COLLABORATOR
user: login: thofma
created_at: 2025-04-04T11:52:38Z
body: Didn't we decide a while ago that `dicyclic_group === quaternion_group` and `is_dicyclic_group === is_quaternion_group` (https://github.com/oscar-system/Oscar.jl/issues/1630 and https://github.com/oscar-system/Oscar.jl/pull/4661#discussion_r2006625848)? I think now having
```
julia> is_quaternion_group(quaternion_group(12))
false
```
is not optimal.
author_association: COLLABORATOR
user: login: ThomasBreuer
created_at: 2025-04-16T08:41:25Z
body: Concerning @thofma's latest comment:
Yes, I think the solution is
- to make also `is_dicyclic_group` and `is_quaternion_group` synonyms, in the same way as `dicyclic_group` and `quaternion_group` are,
- to merge the docstrings of `is_dicyclic_group` and `is_quaternion_group` into one docstring, and
- to change the `N // 2` to `div(N, 2)`.

(The current code wants `is_dicyclic_group` and `is_quaternion_group` to behave analogous to the corresponding functions in GAP, but the situation in GAP is different, since `GAP.Globals.GeneralizedQuaternionGroup` prints a warning if the argument is not a 2-power, and `GAP.Globals.IsGeneralizedQuaternionGroup` returns `false` if the order of the argument is not a 2-power. In gap-system/gap/pull/5966, I had just added the missing function `GAP.Globals.IsDicyclicGroup`, without changing the -perhaps inconsistent- behaviour of `GAP.Globals.IsGeneralizedQuaternionGroup` and `GAP.Globals.GeneralizedQuaternionGroup`.)
author_association: MEMBER
user: login: jamesnohilly
created_at: 2025-04-16T09:53:56Z
body: Regarding this issue, I believe I became confused regarding GAP's functionality vs. OSCAR functionality throughout the reimplementation of `is_dicyclic_group`. However, I will implement @ThomasBreuer's solution in the evening and clean up this issue.
author_association: COLLABORATOR
user: login: fingolfin
created_at: 2025-04-30T08:25:43Z
body: Just to add another data point that convinces me we are doing the right thing merging the two notations: Keith Conrad also considers them as synonyms in https://kconrad.math.uconn.edu/blurbs/grouptheory/genquat.pdf 
author_association: MEMBER