Repository: oscar-system/Oscar.jl
Pull request #4841: Fix saturation for principal ideals
Author: login: ederc
Labels: name: topic: commutative algebra
name: release notes: use title
description: For PRs: the title of this PR is suitable for direct use in the release notes
name: backport 1.3.x
description: backport for release branch 1.3
Created: 2025-04-29T12:33:54Z | Merged: 2025-05-05T19:12:14Z

Fixes saturation for principal ideals, see #4840.


Diff:
diff --git a/src/Rings/mpoly-ideals.jl b/src/Rings/mpoly-ideals.jl
index 265b5e9d5624..201e3fee9103 100644
--- a/src/Rings/mpoly-ideals.jl
+++ b/src/Rings/mpoly-ideals.jl
@@ -352,11 +352,11 @@ Ideal generated by
 ```
 """
 function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where T
-  # `remove` is faster than `Singular.saturation` when saturating with
+  # `ppio` is faster than `Singular.saturation` when saturating with
   # respect to principal ideals
-  if ngens(I) == 1 && ngens(J) == 1
-    is_unit(J[1]) && return I
-    return ideal(remove(I[1], J[1])[2])
+  if base_ring(I) isa Field && is_known(is_principal, I) && is_principal(I) && is_known(is_principal, J) && is_principal(J)
+    is_unit(principal_generator(I)) && return I
+    return ideal(ppio(principal_generator(I), principal_generator(J))[2])
   end
 
   if iteration
@@ -415,10 +415,17 @@ julia> K, m = saturation_with_index(I)
 function saturation_with_index(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I)))) where T
   # `remove` is faster than `Singular.saturation` when saturating with
   # respect to principal ideals
-  if ngens(I) == 1 && ngens(J) == 1
-    is_unit(J[1]) && return (I, base_ring(I)(0))
-    pair = remove(I[1], J[1])
-    return (ideal(pair[2]), pair[1])
+  if base_ring(I) isa Field && is_known(is_principal, I) && is_principal(I) && is_known(is_principal, J) && is_principal(J)
+    is_unit(principal_generator(I)) && return (I, base_ring(I)(0))
+
+    # cheap heuristic to allow a speedup by the use of 
+    # remove insted of Singular's saturation precisely in the following setting:
+    #  I, J known to be principal, J known to be irreducible (degree 1 over a field)
+    # main use-case: strict transforms in toric blow ups
+    if total_degree(principal_generator(J)) == 1
+      pair = remove(principal_generator(I), principal_generator(J))
+      return (ideal(pair[2]), pair[1])
+    end
   end
 
   K, k = Singular.saturation(singular_generators(I), singular_generators(J))
@@ -2515,3 +2522,25 @@ end
 function hash(I::Ideal, c::UInt)
   return hash(base_ring(I), c)
 end
+
+# Recognition of principal ideals
+function is_known(::typeof(is_principal), I::MPolyIdeal)
+  is_one(ngens(I)) && return true
+  has_attribute(I, :is_principal) && return true
+  return false
+end
+
+@attr Bool function is_principal(I::MPolyIdeal)
+  is_one(ngens(I)) && return true
+  g = small_generating_set(I)
+  !is_one(length(g)) && return false
+  set_attribute!(I, :principal_generator=>only(g))
+  return true
+end
+
+function principal_generator(I::MPolyIdeal{T}) where {T}
+  is_one(ngens(I)) && return only(gens(I))
+  @req is_principal(I) "ideal is not principal" # sets the attribute in particular
+  return get_attribute(I, :principal_generator)::T
+end
+ 
diff --git a/test/Rings/mpoly.jl b/test/Rings/mpoly.jl
index 2c4941c903c4..cc52d434c903 100644
--- a/test/Rings/mpoly.jl
+++ b/test/Rings/mpoly.jl
@@ -91,6 +91,11 @@ end
   @test saturation(I) == saturation(I, J)
   @test saturation_with_index(I) == (ideal(R, [x]), 2)
   @test saturation_with_index(I) == saturation_with_index(I, J)
+  # issue 4840
+  I = ideal(R, [x^5*y])
+  J = ideal(R, [x^2])
+  @test saturation(I, J) == ideal(R, [y])
+  @test saturation_with_index(I, J) == (ideal(R, [y]), 3)
 
   @test I != J
   RR, (xx, yy) = grade(R, [1, 1])
@@ -673,3 +678,20 @@ end
   @test dim(R) == 2
 end
 
+@testset "principally generated ideals" begin
+  R, (x, y) = QQ[:x, :y]
+  I1 = ideal(R, [x])
+  I2 = ideal(R, [x, x^2])
+  I3 = ideal(R, [x, y])
+  @test Oscar.is_known(is_principal, I1) # obvious to check
+  @test !Oscar.is_known(is_principal, I2) # not obvious to check, needs GB
+  g = principal_generator(I2) # computes a `small_generating_set`
+  @test Oscar.is_known(is_principal, I2) # result is cached
+  @test Oscar.is_principal(I2)
+  @test !Oscar.is_known(is_principal, I3) # not obvious to check
+  @test !Oscar.is_principal(I3) # check can be done
+  @test Oscar.is_known(is_principal, I3) # result is cached
+end
+
+  
+  
diff --git a/test/book/specialized/brandhorst-zach-fibration-hopping/vinberg_2.jlcon b/test/book/specialized/brandhorst-zach-fibration-hopping/vinberg_2.jlcon
index f844d16e5f61..376a1d089a91 100644
--- a/test/book/specialized/brandhorst-zach-fibration-hopping/vinberg_2.jlcon
+++ b/test/book/specialized/brandhorst-zach-fibration-hopping/vinberg_2.jlcon
@@ -36,11 +36,11 @@ Scheme
 with default covering
   described by patches
     1: scheme(-(x//z)^3 + (y//z)^2 - t^7 + 2*t^6 - t^5)
-    2: scheme(-(z//x)^3*t^7 + 2*(z//x)^3*t^6 - (z//x)^3*t^5 + (z//x)*(y//x)^2 - 1)
-    3: scheme(-(z//y)^3*t^7 + 2*(z//y)^3*t^6 - (z//y)^3*t^5 + (z//y) - (x//y)^3)
-    4: scheme(-(x//z)^3 + (y//z)^2 - s^7 + 2*s^6 - s^5)
-    5: scheme(-(z//x)^3*s^7 + 2*(z//x)^3*s^6 - (z//x)^3*s^5 + (z//x)*(y//x)^2 - 1)
-    6: scheme(-(z//y)^3*s^7 + 2*(z//y)^3*s^6 - (z//y)^3*s^5 + (z//y) - (x//y)^3)
+    2: scheme((z//x)^3*t^7 - 2*(z//x)^3*t^6 + (z//x)^3*t^5 - (z//x)*(y//x)^2 + 1)
+    3: scheme((z//y)^3*t^7 - 2*(z//y)^3*t^6 + (z//y)^3*t^5 - (z//y) + (x//y)^3)
+    4: scheme((x//z)^3 - (y//z)^2 + s^7 - 2*s^6 + s^5)
+    5: scheme((z//x)^3*s^7 - 2*(z//x)^3*s^6 + (z//x)^3*s^5 - (z//x)*(y//x)^2 + 1)
+    6: scheme((z//y)^3*s^7 - 2*(z//y)^3*s^6 + (z//y)^3*s^5 - (z//y) + (x//y)^3)
   in the coordinate(s)
     1: [(x//z), (y//z), t]
     2: [(z//x), (y//x), t]


Reviews:
user: login: HechtiDerLachs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T13:11:32Z
user: login: HechtiDerLachs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T13:22:45Z
user: login: lgoettgens
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T13:31:26Z
user: login: HechtiDerLachs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T13:34:34Z
user: login: HechtiDerLachs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T13:35:09Z
user: login: paemurru
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T13:36:02Z
user: login: lgoettgens
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-30T13:39:56Z
user: login: paemurru
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T13:51:19Z
user: login: paemurru
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T13:52:15Z
user: login: joschmitt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T08:15:10Z
user: login: paemurru
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-01T09:07:56Z
user: login: paemurru
body: Allow `IntegerUnion` as coefficients
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-01T13:56:21Z
user: login: wdecker
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-01T17:51:04Z
user: login: afkafkafk13
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-01T18:18:39Z
user: login: paemurru
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-01T21:15:44Z
user: login: afkafkafk13
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-01T21:49:05Z
user: login: lgoettgens
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T11:01:43Z
user: login: ederc
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T12:25:23Z
user: login: paemurru
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-02T13:20:43Z
user: login: paemurru
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-02T13:22:24Z
user: login: lgoettgens
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T13:23:54Z
user: login: paemurru
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-02T13:41:28Z
user: login: paemurru
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-02T13:41:51Z
user: login: afkafkafk13
body: I left you 3 little comments. If they are treated, this is good to go.

Just a question aside: is this is_known construction standard? I can think of several other places, where this might be very helpful....
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-05T17:26:05Z
user: login: paemurru
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T18:03:57Z
user: login: paemurru
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T18:04:32Z

Review comments:
diff_hunk: @@ -352,11 +352,11 @@ Ideal generated by
 ```
 """
 function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where T
-  # `remove` is faster than `Singular.saturation` when saturating with
+  # `ppio` is faster than `Singular.saturation` when saturating with
   # respect to principal ideals
-  if ngens(I) == 1 && ngens(J) == 1
-    is_unit(J[1]) && return I
-    return ideal(remove(I[1], J[1])[2])
+  if is_known(is_principal, I) && is_principal(I) && is_known(is_principal, J) && is_principal(J)
+    is_unit(principal_generator(I)) && return I
+    return ideal(ppio(principal_generator(I), principal_generator(J))[2])
path: src/Rings/mpoly-ideals.jl
user: login: HechtiDerLachs
body: ```suggestion
    is_unit(principal_generator(I)) && return I
    is_gen(principal_generator(I)) && ideal(remove(principal_generator(I), principal_generator(J))[2]) # catches a special case in strict transforms of toric blowups 
    return ideal(ppio(principal_generator(I), principal_generator(J))[2])
```
created_at: 2025-04-30T13:11:32Z
author_association: COLLABORATOR
diff_hunk: @@ -2515,3 +2507,25 @@ end
 function hash(I::Ideal, c::UInt)
   return hash(base_ring(I), c)
 end
+
+# Recognition of principal ideals
+function is_known(::typeof(is_principal), I::MPolyIdeal)
+  is_one(length(gens(I))) && return true
+  has_attribute(I, :is_principal) && return true
+  return false
+end
+
+@attr Bool function is_principal(I::MPolyIdeal)
+  is_one(ngens(I)) && return true
+  g = small_generating_set(I)
+  !is_one(length(g)) && return false
+  set_attribute!(I, :principal_generator=>only(g))
+  return true
+end
+
+function principal_generator(I::MPolyIdeal{T}) where {T}
+  is_one(length(gens(I))) && return only(gens(I))
+  @req is_principal(I) "ideal is not principal" # sets the attribute in particular
+  return get_attribute(I, :principal_generator)::T
+end
+
path: src/Rings/mpoly-ideals.jl
user: login: HechtiDerLachs
body: ```suggestion
end
 
 # This is a catchall method which should probably be moved elsewher
 function is_gen(a::MPolyRingElem)
   is_zero(a) && return false
   !is_one(length(a)) && return false
   e = only(AbstractAlgebra.exponent_vectors(a))
   i = findfirst(is_one, e)
   isnothing(i) && return false
   return all(k == i || is_zero(n) for (k, n) in enumerate(e))
 end
```
created_at: 2025-04-30T13:22:45Z
author_association: COLLABORATOR
diff_hunk: @@ -352,11 +352,12 @@ Ideal generated by
 ```
 """
 function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where T
-  # `remove` is faster than `Singular.saturation` when saturating with
+  # `ppio` is faster than `Singular.saturation` when saturating with
   # respect to principal ideals
-  if ngens(I) == 1 && ngens(J) == 1
-    is_unit(J[1]) && return I
-    return ideal(remove(I[1], J[1])[2])
+  if is_known(is_principal, I) && is_principal(I) && is_known(is_principal, J) && is_principal(J)
+    is_unit(principal_generator(I)) && return I
+    is_gen(principal_generator(I)) && ideal(remove(principal_generator(I), principal_generator(J))[2]) # catches a special case in strict transforms of toric blowups 
path: src/Rings/mpoly-ideals.jl
user: login: lgoettgens
body: ```suggestion
    is_gen(principal_generator(I)) && return ideal(remove(principal_generator(I), principal_generator(J))[2]) # catches a special case in strict transforms of toric blowups 
```
I think you mean this
created_at: 2025-04-30T13:31:26Z
author_association: MEMBER
diff_hunk: @@ -352,11 +352,12 @@ Ideal generated by
 ```
 """
 function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where T
path: src/Rings/mpoly-ideals.jl
user: login: HechtiDerLachs
body: ```suggestion
function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where {CT<:FieldElem, T <: MPolyRingElem{CT}}
```
created_at: 2025-04-30T13:34:34Z
author_association: COLLABORATOR
diff_hunk: @@ -2515,3 +2508,34 @@ end
 function hash(I::Ideal, c::UInt)
   return hash(base_ring(I), c)
 end
+
+# Recognition of principal ideals
+function is_known(::typeof(is_principal), I::MPolyIdeal)
+  is_one(length(gens(I))) && return true
+  has_attribute(I, :is_principal) && return true
+  return false
+end
+
+@attr Bool function is_principal(I::MPolyIdeal)
+  is_one(ngens(I)) && return true
+  g = small_generating_set(I)
+  !is_one(length(g)) && return false
+  set_attribute!(I, :principal_generator=>only(g))
+  return true
+end
+
+function principal_generator(I::MPolyIdeal{T}) where {T}
+  is_one(length(gens(I))) && return only(gens(I))
+  @req is_principal(I) "ideal is not principal" # sets the attribute in particular
+  return get_attribute(I, :principal_generator)::T
+end
+ 
+ # This is a catchall method which should probably be moved elsewher
+ function is_gen(a::MPolyRingElem)
+   is_zero(a) && return false
+   !is_one(length(a)) && return false
+   e = only(AbstractAlgebra.exponent_vectors(a))
path: src/Rings/mpoly-ideals.jl
user: login: HechtiDerLachs
body: ```suggestion
   e = only(AbstractAlgebra.exponent_vectors(a))
   is_one(only(AbstractAlgebra.coefficients(a))) || return false
```
created_at: 2025-04-30T13:35:08Z
author_association: COLLABORATOR
diff_hunk: @@ -413,14 +414,6 @@ julia> K, m = saturation_with_index(I)
 ```
 """
 function saturation_with_index(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I)))) where T
path: src/Rings/mpoly-ideals.jl
user: login: paemurru
body: ```suggestion
function saturation_with_index(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I)))) where {CT<:FieldElem, T <: MPolyRingElem{CT}}
```
created_at: 2025-04-30T13:36:02Z
author_association: COLLABORATOR
diff_hunk: @@ -2515,3 +2508,35 @@ end
 function hash(I::Ideal, c::UInt)
   return hash(base_ring(I), c)
 end
+
+# Recognition of principal ideals
+function is_known(::typeof(is_principal), I::MPolyIdeal)
+  is_one(length(gens(I))) && return true
+  has_attribute(I, :is_principal) && return true
+  return false
+end
+
+@attr Bool function is_principal(I::MPolyIdeal)
+  is_one(ngens(I)) && return true
+  g = small_generating_set(I)
+  !is_one(length(g)) && return false
+  set_attribute!(I, :principal_generator=>only(g))
+  return true
+end
+
+function principal_generator(I::MPolyIdeal{T}) where {T}
+  is_one(length(gens(I))) && return only(gens(I))
+  @req is_principal(I) "ideal is not principal" # sets the attribute in particular
+  return get_attribute(I, :principal_generator)::T
+end
+ 
+ # This is a catchall method which should probably be moved elsewher
+ function is_gen(a::MPolyRingElem)
+   is_zero(a) && return false
+   !is_one(length(a)) && return false
+   e = only(AbstractAlgebra.exponent_vectors(a))
+   is_one(only(AbstractAlgebra.coefficients(a))) || return false
+   i = findfirst(is_one, e)
+   isnothing(i) && return false
+   return all(k == i || is_zero(n) for (k, n) in enumerate(e))
+ end
path: src/Rings/mpoly-ideals.jl
user: login: lgoettgens
body: This should already exist upstream in AA/Nemo. If you have a concrete input where it doesn't work, please open an issue in AA/Nemo so that we can fix it.
Merging this here just makes later moving it upstream incredibly more complicated
created_at: 2025-04-30T13:39:50Z
author_association: MEMBER
diff_hunk: @@ -351,12 +351,13 @@ Ideal generated by
 
 ```
 """
-function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where T
-  # `remove` is faster than `Singular.saturation` when saturating with
+function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where {CT<:FieldElem, T <: MPolyRingElem{CT}}
+  # `ppio` is faster than `Singular.saturation` when saturating with
   # respect to principal ideals
-  if ngens(I) == 1 && ngens(J) == 1
-    is_unit(J[1]) && return I
-    return ideal(remove(I[1], J[1])[2])
+  if is_known(is_principal, I) && is_principal(I) && is_known(is_principal, J) && is_principal(J)
+    is_unit(principal_generator(I)) && return I
+    is_gen(principal_generator(I)) && return ideal(remove(principal_generator(I), principal_generator(J))[2]) # catches a special case in strict transforms of toric blowups 
path: src/Rings/mpoly-ideals.jl
user: login: paemurru
body: ```suggestion
```
created_at: 2025-04-30T13:42:41Z
author_association: COLLABORATOR
diff_hunk: @@ -412,15 +413,7 @@ julia> K, m = saturation_with_index(I)
 
 ```
 """
-function saturation_with_index(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I)))) where T
-  # `remove` is faster than `Singular.saturation` when saturating with
-  # respect to principal ideals
-  if ngens(I) == 1 && ngens(J) == 1
-    is_unit(J[1]) && return (I, base_ring(I)(0))
-    pair = remove(I[1], J[1])
-    return (ideal(pair[2]), pair[1])
-  end
-
+function saturation_with_index(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I)))) where {CT<:FieldElem, T <: MPolyRingElem{CT}}
   K, k = Singular.saturation(singular_generators(I), singular_generators(J))
path: src/Rings/mpoly-ideals.jl
user: login: paemurru
body: ```suggestion
  # `remove` is faster than `Singular.saturation` when saturating with
  # respect to principal ideals
  if base_ring(I) isa Field && is_known(is_principal, I) && is_principal(I) && is_known(is_principal, J) && is_principal(J)
    is_unit(principal_generator(I)) && return (I, base_ring(I)(0))

    # We can use `remove` if its second argument is irreducible.
    # This is true for example if the polynomial is of degree 1.
    # This is the most important case for strict transforms in toric blowups.
    if total_degree(principal_generator(J)) == 1
      pair = remove(principal_generator(I), principal_generator(J))
      return (ideal(pair[2]), pair[1])
    end
  end

  K, k = Singular.saturation(singular_generators(I), singular_generators(J))
```
created_at: 2025-04-30T13:50:35Z
author_association: COLLABORATOR
diff_hunk: @@ -2515,3 +2508,35 @@ end
 function hash(I::Ideal, c::UInt)
   return hash(base_ring(I), c)
 end
+
+# Recognition of principal ideals
+function is_known(::typeof(is_principal), I::MPolyIdeal)
+  is_one(length(gens(I))) && return true
+  has_attribute(I, :is_principal) && return true
+  return false
+end
+
+@attr Bool function is_principal(I::MPolyIdeal)
+  is_one(ngens(I)) && return true
+  g = small_generating_set(I)
+  !is_one(length(g)) && return false
+  set_attribute!(I, :principal_generator=>only(g))
+  return true
+end
+
+function principal_generator(I::MPolyIdeal{T}) where {T}
+  is_one(length(gens(I))) && return only(gens(I))
+  @req is_principal(I) "ideal is not principal" # sets the attribute in particular
+  return get_attribute(I, :principal_generator)::T
+end
+ 
+ # This is a catchall method which should probably be moved elsewher
+ function is_gen(a::MPolyRingElem)
+   is_zero(a) && return false
+   !is_one(length(a)) && return false
+   e = only(AbstractAlgebra.exponent_vectors(a))
+   is_one(only(AbstractAlgebra.coefficients(a))) || return false
+   i = findfirst(is_one, e)
+   isnothing(i) && return false
+   return all(k == i || is_zero(n) for (k, n) in enumerate(e))
+ end
path: src/Rings/mpoly-ideals.jl
user: login: paemurru
body: I changed to checking whether `total_degree` is 1, so that `is_gen` is not needed here
created_at: 2025-04-30T13:52:15Z
author_association: COLLABORATOR
diff_hunk: @@ -351,12 +351,13 @@ Ideal generated by
 
 ```
 """
-function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where T
-  # `remove` is faster than `Singular.saturation` when saturating with
+function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where {CT<:FieldElem, T <: MPolyRingElem{CT}}
path: src/Rings/mpoly-ideals.jl
user: login: joschmitt
body: These type restrictions are too strict: the tests call the function with ideals over the integers (I assume that Singular can handle this case).
created_at: 2025-05-01T08:15:09Z
author_association: MEMBER
diff_hunk: @@ -351,12 +351,13 @@ Ideal generated by
 
 ```
 """
-function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where T
-  # `remove` is faster than `Singular.saturation` when saturating with
+function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where {CT<:FieldElem, T <: MPolyRingElem{CT}}
path: src/Rings/mpoly-ideals.jl
user: login: paemurru
body: I am confused. Open the book "A Singular Introduction to Commutative Algebra" by Greuel and Pfister on page 81, Section 1.8.9 "Saturation". The coefficient ring is a field. Surely the tests need to be altered?
created_at: 2025-05-01T09:07:56Z
author_association: COLLABORATOR
diff_hunk: @@ -351,12 +351,13 @@ Ideal generated by
 
 ```
 """
-function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where T
-  # `remove` is faster than `Singular.saturation` when saturating with
+function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where {CT<:FieldElem, T <: MPolyRingElem{CT}}
path: src/Rings/mpoly-ideals.jl
user: login: paemurru
body: ```suggestion
function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where T
```
created_at: 2025-05-01T13:49:46Z
author_association: COLLABORATOR
diff_hunk: @@ -2515,3 +2508,35 @@ end
 function hash(I::Ideal, c::UInt)
   return hash(base_ring(I), c)
 end
+
+# Recognition of principal ideals
+function is_known(::typeof(is_principal), I::MPolyIdeal)
+  is_one(length(gens(I))) && return true
+  has_attribute(I, :is_principal) && return true
+  return false
+end
+
+@attr Bool function is_principal(I::MPolyIdeal)
+  is_one(ngens(I)) && return true
+  g = small_generating_set(I)
+  !is_one(length(g)) && return false
+  set_attribute!(I, :principal_generator=>only(g))
+  return true
+end
+
+function principal_generator(I::MPolyIdeal{T}) where {T}
+  is_one(length(gens(I))) && return only(gens(I))
+  @req is_principal(I) "ideal is not principal" # sets the attribute in particular
+  return get_attribute(I, :principal_generator)::T
+end
+ 
+ # This is a catchall method which should probably be moved elsewher
+ function is_gen(a::MPolyRingElem)
+   is_zero(a) && return false
+   !is_one(length(a)) && return false
+   e = only(AbstractAlgebra.exponent_vectors(a))
+   is_one(only(AbstractAlgebra.coefficients(a))) || return false
+   i = findfirst(is_one, e)
+   isnothing(i) && return false
+   return all(k == i || is_zero(n) for (k, n) in enumerate(e))
+ end
path: src/Rings/mpoly-ideals.jl
user: login: paemurru
body: The `is_gen` in AA/Nemo does not allow graded polynomial rings.
The `is_gen` is not used here (I have replaced it with a `total_degree` check, which is OK as currently we allow only coefficient rings that are integral domains), so can be removed from here as far as I am concerned.
created_at: 2025-05-01T13:51:48Z
author_association: COLLABORATOR
diff_hunk: @@ -412,15 +413,7 @@ julia> K, m = saturation_with_index(I)
 
 ```
 """
-function saturation_with_index(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I)))) where T
-  # `remove` is faster than `Singular.saturation` when saturating with
-  # respect to principal ideals
-  if ngens(I) == 1 && ngens(J) == 1
-    is_unit(J[1]) && return (I, base_ring(I)(0))
-    pair = remove(I[1], J[1])
-    return (ideal(pair[2]), pair[1])
-  end
-
+function saturation_with_index(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I)))) where {CT<:FieldElem, T <: MPolyRingElem{CT}}
path: src/Rings/mpoly-ideals.jl
user: login: paemurru
body: ```suggestion
function saturation_with_index(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I)))) where T
```
created_at: 2025-05-01T13:53:59Z
author_association: COLLABORATOR
diff_hunk: @@ -351,12 +351,13 @@ Ideal generated by
 
 ```
 """
-function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where T
-  # `remove` is faster than `Singular.saturation` when saturating with
+function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where {CT<:FieldElem, T <: MPolyRingElem{CT}}
path: src/Rings/mpoly-ideals.jl
user: login: wdecker
body: Why restrict types here at all? This is not done for any other commutative algebra function (except in cases where a field is required). In some future, we might have more general coefficient rings. @ederc ???
created_at: 2025-05-01T17:51:04Z
author_association: COLLABORATOR
diff_hunk: @@ -351,12 +351,13 @@ Ideal generated by
 
 ```
 """
-function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where T
-  # `remove` is faster than `Singular.saturation` when saturating with
+function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where {CT<:FieldElem, T <: MPolyRingElem{CT}}
path: src/Rings/mpoly-ideals.jl
user: login: afkafkafk13
body: No, the tests are fine. The point is that standard bases have only been introduced over a field in the cited book. But there exist generalizations which work over euclidean rings as coefficient rings. An implementation of this latter kind is available in Singular for the case of ZZ. Using this implementation, we also have saturation in ZZ[\underline{x}] and the tests ensure that this does not get broken by accident. It is e.g. used in the context of blow-ups in the mixed characteristic case.
 
On the other hand, I would suggest that the backend throws an error, if the functionality is not available for the specific type of coefficient ring -- to be open for future implementations of the underlying algorithms without having to take out the security mechanisms everywhere. What I would add, however, is a comment that throwing the error is left to the backend.
created_at: 2025-05-01T18:18:38Z
author_association: COLLABORATOR
diff_hunk: @@ -351,12 +351,13 @@ Ideal generated by
 
 ```
 """
-function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where T
-  # `remove` is faster than `Singular.saturation` when saturating with
+function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where {CT<:FieldElem, T <: MPolyRingElem{CT}}
path: src/Rings/mpoly-ideals.jl
user: login: paemurru
body: > What I would add, however, is a comment that throwing the error is left to the backend.

That would be good. But if you mean throwing an error in Singular, then I would not know how to program that.

> Why restrict types here at all?

If there is a chance of the output being incorrect for certain rings, then this would be very bad.

In this particular case, I used the faster `remove` function in case the second argument is a degree 1 polynomial. If the coefficient ring is not an integral domain, then degree 1 polynomials need not be irreducible, so the function would give wrong output.

One way to fix this for general rings would be to instead check for irreducibility of the polynomial. Do we prefer this, or can this be very slow in certain cases? @wdecker @afkafkafk13
created_at: 2025-05-01T21:15:44Z
author_association: COLLABORATOR
diff_hunk: @@ -351,12 +351,13 @@ Ideal generated by
 
 ```
 """
-function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where T
-  # `remove` is faster than `Singular.saturation` when saturating with
+function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where {CT<:FieldElem, T <: MPolyRingElem{CT}}
path: src/Rings/mpoly-ideals.jl
user: login: afkafkafk13
body: > > What I would add, however, is a comment that throwing the error is left to the backend.
> 
> That would be good. But if you mean throwing an error in Singular, then I would not know how to program that.
What I mean is: leave every case for which you are unsure to Singular. It will throw an error, if it does not know how to handle it.

> > Why restrict types here at all?
> 
> If there is a chance of the output being incorrect for certain rings, then this would be very bad.
>
> In this particular case, I used the faster `remove` function in case the second argument is a degree 1 polynomial. If the coefficient ring is not an integral domain, then degree 1 polynomials need not be irreducible, so the function would give wrong output.
> 
> One way to fix this for general rings would be to instead check for irreducibility of the polynomial. Do we prefer this, or can this be very slow in certain cases? @wdecker @afkafkafk13

This indeed points to a differnt solution: create two functions, one with a specialized signature (coefficient ring is a field) and with the safe use of the `remove` call. This function then calls an internal function doing all  the rest (i.e. delegate this to Singular, which then might or might not throw an error). The other function is one with the old rather general signature which is nothing but a wrapper und directly calls the internal function. The comment on Singular being responsible for throwing the error should then be placed in the internal one.

created_at: 2025-05-01T21:49:05Z
author_association: COLLABORATOR
diff_hunk: @@ -2515,3 +2508,35 @@ end
 function hash(I::Ideal, c::UInt)
   return hash(base_ring(I), c)
 end
+
+# Recognition of principal ideals
+function is_known(::typeof(is_principal), I::MPolyIdeal)
+  is_one(length(gens(I))) && return true
+  has_attribute(I, :is_principal) && return true
+  return false
+end
+
+@attr Bool function is_principal(I::MPolyIdeal)
+  is_one(ngens(I)) && return true
+  g = small_generating_set(I)
+  !is_one(length(g)) && return false
+  set_attribute!(I, :principal_generator=>only(g))
+  return true
+end
+
+function principal_generator(I::MPolyIdeal{T}) where {T}
+  is_one(length(gens(I))) && return only(gens(I))
+  @req is_principal(I) "ideal is not principal" # sets the attribute in particular
+  return get_attribute(I, :principal_generator)::T
+end
+ 
+ # This is a catchall method which should probably be moved elsewher
+ function is_gen(a::MPolyRingElem)
+   is_zero(a) && return false
+   !is_one(length(a)) && return false
+   e = only(AbstractAlgebra.exponent_vectors(a))
+   is_one(only(AbstractAlgebra.coefficients(a))) || return false
+   i = findfirst(is_one, e)
+   isnothing(i) && return false
+   return all(k == i || is_zero(n) for (k, n) in enumerate(e))
+ end
path: src/Rings/mpoly-ideals.jl
user: login: lgoettgens
body: ```suggestion
function is_gen(a::MPolyDecRingElem)
  return is_gen(forget_grading(x))
end
```
If the issue is that this doesn't work for graded rings, then it would be better to add a method *just* for graded rings. And this should ideally delegate to the underlying ungraded ring to make use of more efficient implementations from FLINT
created_at: 2025-05-02T11:01:43Z
author_association: MEMBER
diff_hunk: @@ -351,12 +351,13 @@ Ideal generated by
 
 ```
 """
-function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where T
-  # `remove` is faster than `Singular.saturation` when saturating with
+function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where {CT<:FieldElem, T <: MPolyRingElem{CT}}
+  # `ppio` is faster than `Singular.saturation` when saturating with
   # respect to principal ideals
-  if ngens(I) == 1 && ngens(J) == 1
-    is_unit(J[1]) && return I
-    return ideal(remove(I[1], J[1])[2])
+  if is_known(is_principal, I) && is_principal(I) && is_known(is_principal, J) && is_principal(J)
path: src/Rings/mpoly-ideals.jl
user: login: ederc
body: Can't we just test if the corresponding base_ring is a field in order to apply your optimization and let Singular handle the saturation otherwise? Then we would not need to restrict `T` at all.

```suggestion
  if base_ring(I) isa Field && is_known(is_principal, I) && is_principal(I) && is_known(is_principal, J) && is_principal(J)
```
created_at: 2025-05-02T12:25:23Z
author_association: MEMBER
diff_hunk: @@ -2515,3 +2521,29 @@ end
 function hash(I::Ideal, c::UInt)
   return hash(base_ring(I), c)
 end
+
+# Recognition of principal ideals
+function is_known(::typeof(is_principal), I::MPolyIdeal)
+  is_one(length(gens(I))) && return true
+  has_attribute(I, :is_principal) && return true
+  return false
+end
+
+@attr Bool function is_principal(I::MPolyIdeal)
+  is_one(ngens(I)) && return true
+  g = small_generating_set(I)
+  !is_one(length(g)) && return false
+  set_attribute!(I, :principal_generator=>only(g))
+  return true
+end
+
+function principal_generator(I::MPolyIdeal{T}) where {T}
+  is_one(length(gens(I))) && return only(gens(I))
+  @req is_principal(I) "ideal is not principal" # sets the attribute in particular
+  return get_attribute(I, :principal_generator)::T
+end
+ 
+ # This is a catchall method which should probably be moved elsewher
path: src/Rings/mpoly-ideals.jl
user: login: paemurru
body: ```suggestion
 # This is a catchall method which should probably be moved elsewhere
```
created_at: 2025-05-02T13:20:43Z
author_association: COLLABORATOR
diff_hunk: @@ -351,12 +351,13 @@ Ideal generated by
 
 ```
 """
-function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where T
-  # `remove` is faster than `Singular.saturation` when saturating with
+function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where {CT<:FieldElem, T <: MPolyRingElem{CT}}
path: src/Rings/mpoly-ideals.jl
user: login: paemurru
body: Instead of two functions, used the idea of @ederc to check whether the base ring is a field using `isa`.
created_at: 2025-05-02T13:22:24Z
author_association: COLLABORATOR
diff_hunk: @@ -2515,3 +2521,29 @@ end
 function hash(I::Ideal, c::UInt)
   return hash(base_ring(I), c)
 end
+
+# Recognition of principal ideals
+function is_known(::typeof(is_principal), I::MPolyIdeal)
+  is_one(length(gens(I))) && return true
+  has_attribute(I, :is_principal) && return true
+  return false
+end
+
+@attr Bool function is_principal(I::MPolyIdeal)
+  is_one(ngens(I)) && return true
+  g = small_generating_set(I)
+  !is_one(length(g)) && return false
+  set_attribute!(I, :principal_generator=>only(g))
+  return true
+end
+
+function principal_generator(I::MPolyIdeal{T}) where {T}
+  is_one(length(gens(I))) && return only(gens(I))
+  @req is_principal(I) "ideal is not principal" # sets the attribute in particular
+  return get_attribute(I, :principal_generator)::T
+end
+ 
+ # This is a catchall method which should probably be moved elsewhere
+function is_gen(a::MPolyDecRingElem)
path: src/Rings/mpoly-ideals.jl
user: login: lgoettgens
body: This is no longer a catchall method. The best place for this to live would be https://github.com/oscar-system/Oscar.jl/blob/c2562fceaf64c4cb5c2d356eaea1c9bdd6e71cd9/src/Rings/mpoly-graded.jl#L1373 IMO, so maybe @paemurru could move it there?
created_at: 2025-05-02T13:23:54Z
author_association: MEMBER
diff_hunk: @@ -2515,3 +2521,29 @@ end
 function hash(I::Ideal, c::UInt)
   return hash(base_ring(I), c)
 end
+
+# Recognition of principal ideals
+function is_known(::typeof(is_principal), I::MPolyIdeal)
+  is_one(length(gens(I))) && return true
+  has_attribute(I, :is_principal) && return true
+  return false
+end
+
+@attr Bool function is_principal(I::MPolyIdeal)
+  is_one(ngens(I)) && return true
+  g = small_generating_set(I)
+  !is_one(length(g)) && return false
+  set_attribute!(I, :principal_generator=>only(g))
+  return true
+end
+
+function principal_generator(I::MPolyIdeal{T}) where {T}
+  is_one(length(gens(I))) && return only(gens(I))
+  @req is_principal(I) "ideal is not principal" # sets the attribute in particular
+  return get_attribute(I, :principal_generator)::T
+end
+ 
+ # This is a catchall method which should probably be moved elsewhere
+function is_gen(a::MPolyDecRingElem)
+  return is_gen(forget_grading(x))
+end
path: src/Rings/mpoly-ideals.jl
user: login: paemurru
body: ```suggestion
```
created_at: 2025-05-02T13:41:28Z
author_association: COLLABORATOR
diff_hunk: @@ -2515,3 +2521,29 @@ end
 function hash(I::Ideal, c::UInt)
   return hash(base_ring(I), c)
 end
+
+# Recognition of principal ideals
+function is_known(::typeof(is_principal), I::MPolyIdeal)
+  is_one(length(gens(I))) && return true
+  has_attribute(I, :is_principal) && return true
+  return false
+end
+
+@attr Bool function is_principal(I::MPolyIdeal)
+  is_one(ngens(I)) && return true
+  g = small_generating_set(I)
+  !is_one(length(g)) && return false
+  set_attribute!(I, :principal_generator=>only(g))
+  return true
+end
+
+function principal_generator(I::MPolyIdeal{T}) where {T}
+  is_one(length(gens(I))) && return only(gens(I))
+  @req is_principal(I) "ideal is not principal" # sets the attribute in particular
+  return get_attribute(I, :principal_generator)::T
+end
+ 
+ # This is a catchall method which should probably be moved elsewhere
+function is_gen(a::MPolyDecRingElem)
path: src/Rings/mpoly-ideals.jl
user: login: paemurru
body: https://github.com/oscar-system/Oscar.jl/pull/4851
created_at: 2025-05-02T13:41:51Z
author_association: COLLABORATOR
diff_hunk: @@ -415,10 +415,16 @@ julia> K, m = saturation_with_index(I)
 function saturation_with_index(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I)))) where T
   # `remove` is faster than `Singular.saturation` when saturating with
   # respect to principal ideals
-  if ngens(I) == 1 && ngens(J) == 1
-    is_unit(J[1]) && return (I, base_ring(I)(0))
-    pair = remove(I[1], J[1])
-    return (ideal(pair[2]), pair[1])
+  if base_ring(I) isa Field && is_known(is_principal, I) && is_principal(I) && is_known(is_principal, J) && is_principal(J)
+    is_unit(principal_generator(I)) && return (I, base_ring(I)(0))
+
+    # We can use `remove` if its second argument is irreducible.
+    # This is true for example if the polynomial is of degree 1.
+    # This is the most important case for strict transforms in toric blowups.
path: src/Rings/mpoly-ideals.jl
user: login: afkafkafk13
body: Maybe just clarify the comment so that someone else will still understand it in 10 years.
```suggestion
    # cheap heuristic to allow a speedup by the use of 
    # remove insted of Singular's saturation precisely in the following setting:
    #  I, J known to be principal, J known to be irreducible (degree 1 over a field)
    # main use-case: strict transforms in toric blow ups
```
created_at: 2025-05-05T16:54:48Z
author_association: COLLABORATOR
diff_hunk: @@ -2515,3 +2521,25 @@ end
 function hash(I::Ideal, c::UInt)
   return hash(base_ring(I), c)
 end
+
+# Recognition of principal ideals
+function is_known(::typeof(is_principal), I::MPolyIdeal)
+  is_one(length(gens(I))) && return true
path: src/Rings/mpoly-ideals.jl
user: login: afkafkafk13
body: ngens ?
created_at: 2025-05-05T17:14:26Z
author_association: COLLABORATOR
diff_hunk: @@ -2515,3 +2521,25 @@ end
 function hash(I::Ideal, c::UInt)
   return hash(base_ring(I), c)
 end
+
+# Recognition of principal ideals
+function is_known(::typeof(is_principal), I::MPolyIdeal)
+  is_one(length(gens(I))) && return true
+  has_attribute(I, :is_principal) && return true
+  return false
+end
+
+@attr Bool function is_principal(I::MPolyIdeal)
+  is_one(ngens(I)) && return true
+  g = small_generating_set(I)
+  !is_one(length(g)) && return false
+  set_attribute!(I, :principal_generator=>only(g))
+  return true
+end
+
+function principal_generator(I::MPolyIdeal{T}) where {T}
+  is_one(length(gens(I))) && return only(gens(I))
path: src/Rings/mpoly-ideals.jl
user: login: afkafkafk13
body: ngens ?
created_at: 2025-05-05T17:15:05Z
author_association: COLLABORATOR
diff_hunk: @@ -2515,3 +2521,25 @@ end
 function hash(I::Ideal, c::UInt)
   return hash(base_ring(I), c)
 end
+
+# Recognition of principal ideals
+function is_known(::typeof(is_principal), I::MPolyIdeal)
+  is_one(length(gens(I))) && return true
path: src/Rings/mpoly-ideals.jl
user: login: paemurru
body: ```suggestion
  is_one(ngens(I)) && return true
```
created_at: 2025-05-05T18:03:56Z
author_association: COLLABORATOR
diff_hunk: @@ -2515,3 +2521,25 @@ end
 function hash(I::Ideal, c::UInt)
   return hash(base_ring(I), c)
 end
+
+# Recognition of principal ideals
+function is_known(::typeof(is_principal), I::MPolyIdeal)
+  is_one(length(gens(I))) && return true
+  has_attribute(I, :is_principal) && return true
+  return false
+end
+
+@attr Bool function is_principal(I::MPolyIdeal)
+  is_one(ngens(I)) && return true
+  g = small_generating_set(I)
+  !is_one(length(g)) && return false
+  set_attribute!(I, :principal_generator=>only(g))
+  return true
+end
+
+function principal_generator(I::MPolyIdeal{T}) where {T}
+  is_one(length(gens(I))) && return only(gens(I))
path: src/Rings/mpoly-ideals.jl
user: login: paemurru
body: ```suggestion
  is_one(ngens(I)) && return only(gens(I))
```
created_at: 2025-05-05T18:04:32Z
author_association: COLLABORATOR

PR comments:
user: login: joschmitt
created_at: 2025-04-29T12:44:54Z
body: The same issue also affects `saturation_with_index` from what I can see.
author_association: MEMBER
user: login: len
created_at: 2025-04-29T15:17:35Z
body: Is ppio still faster than Singular saturation? It doesn’t return the exponent, tho, so you can’t just replace `remove` by `ppio` in saturation_with_index..
author_association: NONE
user: login: ederc
created_at: 2025-04-29T16:51:01Z
body: Yes, `ppio` is still faster than Singular's saturation call. 
author_association: MEMBER
user: login: ederc
created_at: 2025-04-29T17:25:26Z
body: For the `saturation_with_index` calls I switched back to Singular's saturation call to get at least a correct version. If anyone has a better idea just let me know.
author_association: MEMBER
user: login: paemurru
created_at: 2025-04-30T09:46:41Z
body: Some of the most computational parts of `FTheoryTools` (pinging @HereAround ) rely on these functions. So, just switching back to `saturation_with_index` is not good.

The most important case for toric geometry is saturating with respect to a variable. So we should at least check whether the second argument is linear, so that it is safe to apply `remove`.

How does the speed of `ppio` compare with `remove`? Just being faster than Singular's saturation call is not good enough. Using `remove` often gave a speedup of at least 100 times. If `ppio` is ten times slower, then again, at least for the linear case, I would rather use `remove`.

And this should then be documented (in the docstring of `saturation_with_index`), that if the second variable is an irreducible nonlinear polynomial, then the user should instead use `remove`, which is often around 100 times faster than `saturation_with_index`.
author_association: COLLABORATOR
user: login: fingolfin
created_at: 2025-04-30T10:28:41Z
body: `ppio` should have about the same performance as `remove`.


Using `remove` may be 100 times faster but if it has the drawback of being occasionally wrong, I think that's a bad tradeoff...
author_association: MEMBER
user: login: HechtiDerLachs
created_at: 2025-04-30T10:51:23Z
body: I added some (hopefully) better recognition and handling for prinicpal ideals. If you disagree, feel free to remove my commits again. 
author_association: COLLABORATOR
user: login: paemurru
created_at: 2025-04-30T12:06:22Z
body: > but if it has the drawback of being occasionally wrong

That is of course unacceptable.

What I mean is, if the ring is a graded polynomial ring and the ideal has only one generator, then `remove` gives the correct answer and we should use it, as this is the most common case for toric varieties. Slightly more generally, if the ring is known to be an integral domain, possibly graded, and the ideal is known to be principal with the generator being a linear combination of the variables (not necessarily of degree 1 if the ring is graded).
author_association: COLLABORATOR
user: login: HechtiDerLachs
created_at: 2025-04-30T12:11:53Z
body: > if the ring is a graded polynomial ring and the ideal has only one generator, then `remove` gives the correct answer and we should use it

This sounds like you have a particular application from the toric setting in mind which you would like to be recognized and I think we're all happy to support that. However, the current implementation was clearly wrong. As discussed on slack yesterday, it seems that `remove` gives the correct results if 

1. both ideals `I` and `J` in `saturation(I, J)` are principal
2. the generator of `J` is squarefree. 

Does that agree with what we find in the specific toric settings? 

In either case, we should make sure the specific toric setting is recognized and handled efficiently. 
author_association: COLLABORATOR
user: login: paemurru
created_at: 2025-04-30T12:31:52Z
body: > However, the current implementation was clearly wrong.

Yes, we agree on this.

> gives the correct results if ... the generator of J is squarefree

That is not enough. The generator should be irreducible. Example where squarefree is not enough:

```
julia> R, (x, y) = QQ[:x, :y];

julia> saturation(ideal(x^5 * y), ideal(x*y))
Ideal generated by
  x^4

julia> Singular.saturation(Oscar.singular_generators(ideal(x^5 * y)), Oscar.singular_generators(ideal(x*y)))
(Singular ideal over Singular polynomial ring (QQ),(x,y),(dp(2),C) with generators (1), 5)
```

Checking irreducibility is in general complicated. That's why I suggest the idea in https://github.com/oscar-system/Oscar.jl/pull/4841#issuecomment-2841763450 instead
author_association: COLLABORATOR
user: login: paemurru
created_at: 2025-04-30T14:18:40Z
body: The function `saturation_with_index` now uses `remove` if the ideals are known to be principal and the principal generator of the second ideal is of total degree 1. This assumes the coefficient ring is an integral domain, otherwise there might exist degree 1 polynomials that are not irreducible.

But if the function `is_irreducible` is actually very fast, then we could use `is_irreducible` instead of checking `total_degree`. Maybe @afkafkafk13 can comment on this?

Also, there was no check on what the coefficient ring should be. So we added a restriction that it should be a field. Not sure if any more general settings are needed.
author_association: COLLABORATOR
user: login: wdecker
created_at: 2025-05-01T10:11:52Z
body: The book how shows how to do it over a field. This does NOT mean that it can’t be done, for example,  over the integers.

> Am 01.05.2025 um 11:08 schrieb Erik Paemurru ***@***.***>:
> 
> 
> @paemurru commented on this pull request.
> 
> In src/Rings/mpoly-ideals.jl <https://github.com/oscar-system/Oscar.jl/pull/4841#discussion_r2070021050>:
> 
> > @@ -351,12 +351,13 @@ Ideal generated by
>  
>  ```
>  """
> -function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where T
> -  # `remove` is faster than `Singular.saturation` when saturating with
> +function saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=false) where {CT<:FieldElem, T <: MPolyRingElem{CT}}
> I am confused. Open the book "A Singular Introduction to Commutative Algebra" by Greuel and Pfister on page 81, Section 1.8.9 "Saturation". The coefficient ring is a field. Surely the tests need to be altered?
> 
> —
> Reply to this email directly, view it on GitHub <https://github.com/oscar-system/Oscar.jl/pull/4841#discussion_r2070021050>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AASSXEQ6IMRLURSRDSCTNFL24HQAHAVCNFSM6AAAAAB4C3LCEWVHI2DSMVQWIX3LMV43YUDVNRWFEZLROVSXG5CSMV3GSZLXHMZDQMBZGQYDSMJUG4>.
> You are receiving this because you are subscribed to this thread.
> 


author_association: COLLABORATOR
user: login: paemurru
created_at: 2025-05-01T11:00:00Z
body: @wdecker Thank you for the comment. What is the precise condition on the base ring for the Singular algorithm to work? Does it work with any integral domain? Matthias was sure that it does not work with a general ring
author_association: COLLABORATOR
user: login: wdecker
created_at: 2025-05-01T11:24:52Z
body: > Does it work with any integral domain?

@paemurru What do you mean here? What integral domains as coefficient rings can be handled by OSCAR?

In the OSCAR docu, introduction to the commutative algebra chapter, we write:

Our main focus in this chapter is on multivariate polynomial rings over fields (exact fields supported by OSCAR). Where not indicated otherwise, the presented functions also apply to polynomial rings over ZZ. 

I suggest, that authors who extend one or the other method to one or the other coefficient field, add a corresponding !!! note to the docu of the method.

For Singular, please see here: https://www.singular.uni-kl.de/Manual/4-0-3/sing_28.htm

I , personnally, have not tested the case of finite rings $Z/m$ with $m\in Z$ not a prime with respect to saturation. @hannes14?

author_association: COLLABORATOR
user: login: paemurru
created_at: 2025-05-01T13:45:57Z
body: I checked the three books in the [Commutative Algebra introduction](https://docs.oscar-system.org/dev/CommutativeAlgebra/intro/) but none of them mention what the specific conditions for the coefficient ring are.

So I will change to what @wdecker said, to allow any fields and the ring of integers. If the tests pass, then everything is good from my point of view.

Singular manual does not explicitly say that saturation works for rings that are not fields (it does list that `std` works, for example).
https://www.singular.uni-kl.de/Manual/4-0-3/sing_28.htm
Matthias was quite sure that for arbitrary rings, we would get an error somewhere down the line.

Related issue: https://github.com/oscar-system/Oscar.jl/issues/4848 - in the Gröbner bases section it is also unclear what the coefficient ring should be.
author_association: COLLABORATOR
user: login: wdecker
created_at: 2025-05-01T23:08:52Z
body: @HechtiDerLachs, @paemurru Do I understand that correctly: When computing saturation, you wish to always ask the following: `is_principal(I)`, is_principal(J)`. So,  in order to speed up one very special case, you slow down all other cases by introducing additional Gröbner basis computations?
author_association: COLLABORATOR
user: login: paemurru
created_at: 2025-05-01T23:16:31Z
body: > When computing saturation, you wish to always ask the following: is_principal(I)

No, absolutely not. Instead, we ask whether it is already known whether I and J are principal. This is what the `is_known` function does that Matthias introduced. We will not do any redundant heavy computations.
author_association: COLLABORATOR
user: login: afkafkafk13
created_at: 2025-05-02T06:14:36Z
body: > > When computing saturation, you wish to always ask the following: is_principal(I)
> 
> No, absolutely not. Instead, we ask whether it is already known whether I and J are principal. This is what the `is_known` function does that Matthias introduced. We will not do any redundant heavy computations.

I already understood this aspect, but I wonder what methods might set the `is_principal` attribute. You plan to obtain this as a side effect (maybe from something like certain blow-ups). If so, this makes a lot of sense, but is not visible in this PR.
author_association: COLLABORATOR
user: login: hannes14
created_at: 2025-05-02T07:46:25Z
body: Singular.saturation (i.e. iterated quotient) only requires Groebner bases, works also for non-field coefficients.
Singular.saturation2 requires in its current implementation field coefficients, but calls in the  case of non-field coefficients simply the routine behind Singular.saturation
 

author_association: MEMBER
user: login: HechtiDerLachs
created_at: 2025-05-02T08:02:14Z
body: > I already understood this aspect, but I wonder what methods might set the `is_principal` attribute. You plan to obtain this as a side effect (maybe from something like certain blow-ups). If so, this makes a lot of sense, but is not visible in this PR.

The code says `if is_known(is_principal, I) && is_principal(I) ...` 

That is nothing but an extended version of the previous `ngens(I) == 1` and it does not compute any Groebner basis. But in particular it takes into account any eventually set attributes and previous computations which might have revealed that an ideal is actually principal. 

@paemurru : Your suggestion using `IntegerUnion` does not go in the right direction, I'm afraid. It's a type to unify stuff like `Int`, but it's not a type of `RingElem`s and should, hence, not be used as a parameter for ideals. I think, it's fine if we go back to the `ZZRingElem` as before. 

But, as @afkafkafk13 said, we can leave it to the backend to throw an error if we switch from the use of `total_degree` back to `is_gen`, as suggested by @paemurru . 
author_association: COLLABORATOR
user: login: paemurru
created_at: 2025-05-02T08:35:46Z
body: It seems like `ppio` and `remove` give incorrect results if the coefficient ring is not an integral domain.

```
julia> S = quo(ZZ, ZZ(6))[1]
Integers modulo 6

julia> R, (x, y) = polynomial_ring(S, ["x", "y"])
(Multivariate polynomial ring in 2 variables over S, AbstractAlgebra.Generic.MPoly{ZZModRingElem}[x, y])

julia> (2*x + 1)*(3*x + 1)
5*x + 1

julia> saturation(ideal(5*x + 1), ideal(2*x + 1))
Ideal generated by
  5*x + 1

julia> 3*x + 1 in ideal(5*x + 1)
false
```

So, perhaps Anne's suggestion is the best: have two functions, one with a specialized signature, allowing only `Union{FieldElem,ZZRingElem}` as coefficient rings. This function is often around 100 times faster. And have a function with a more general signature that directly calls Singular.

The comment of @hannes14 is very important:

> Singular.saturation (i.e. iterated quotient) only requires Groebner bases, works also for non-field coefficients.
> Singular.saturation2 requires in its current implementation field coefficients, but calls in the case of non-field coefficients simply the routine behind Singular.saturation

We should add this to the documentation! This explanation does not appear in the OSCAR book or the three books I mentioned earlier, this is not written in the Singular link that Wolfram shared and neither in the OSCAR Commutative Algebra introduction. Otherwise, it is not so easy to find out that saturation also works for more general rings. It seems that the precise list of rings where it works is given here (the link that Wolfram shared): https://www.singular.uni-kl.de/Manual/4-0-3/sing_28.htm
author_association: COLLABORATOR
user: login: wdecker
created_at: 2025-05-02T10:38:18Z
body: @paemurru I am more and more confused by this discussion. First, we are talking integral domains. Now we are talking non-integral domains. What is your personal use case? Why do you need saturation over ZZ/6? 

With regard to correctness, without the additional stuff you introduced, saturation works fine in your example:

julia> function test_saturation(I::MPolyIdeal{T}, J::MPolyIdeal{T} = ideal(base_ring(I), gens(base_ring(I))); iteration::Bool=true) where T
         # `remove` is faster than `Singular.saturation` when saturating with
         # respect to principal ideals
       ###  if ngens(I) == 1 && ngens(J) == 1
       ###    is_unit(J[1]) && return I
       ###   return ideal(remove(I[1], J[1])[2])
       ###  end

         if iteration
           K, _ = Singular.saturation(Oscar.singular_generators(I), Oscar.singular_generators(J))
         else
           K, _ = Singular.saturation2(Oscar.singular_generators(I), singular_generators(J))
         end
         return MPolyIdeal(base_ring(I), K)
       end
test_saturation (generic function with 2 methods)

julia> test_saturation(ideal(5*x + 1), ideal(2*x + 1))
Ideal generated by
  2
  x + 5

So, the additional stuff introduced bugs.

author_association: COLLABORATOR
user: login: wdecker
created_at: 2025-05-02T12:26:08Z
body: As @hannes14 points out, there is a more uptodate source for what coefficient rings can be used: 

https://oscar-system.github.io/Singular.jl/stable/

Adding tests for all this in OSCAR und documenting it takes plenty of time. I will think about it (though, again, it would be helpful to have use cases in mind).

With respect to the one method we disucss here, saturation, @ederc will add another line to his fix.

author_association: COLLABORATOR
user: login: paemurru
created_at: 2025-05-02T12:40:45Z
body: @wdecker To explain a bit: I have no use case for non-integral domains. I wrote the 100x speed improvement with integral domains in mind. So it does not matter for my work whether we restrict the function signatures to only integral domains.

But if the function signature does allow more general rings, then we must assure that it does not return incorrect results in those cases.
author_association: COLLABORATOR
user: login: HechtiDerLachs
created_at: 2025-05-05T17:42:38Z
body: > is this is_known construction standard?

I made an effort to introduce and standardize this in #4507 and it can now be used, yes.
author_association: COLLABORATOR
user: login: afkafkafk13
created_at: 2025-05-05T17:50:03Z
body: > > is this is_known construction standard?
> 
> I made an effort to introduce and standardize this in #4507 and it can now be used, yes.

Great thanks. (I did not see it at the time. My fault...)
author_association: COLLABORATOR
user: login: joschmitt
created_at: 2025-05-05T18:27:55Z
body: I mark this for backporting because it fixes a regression introduced in 1.3.0 (according to the change log).
However, the PR also contains some new `is_principal` functionality, so the final decision is (as always) left to our release managers...
author_association: MEMBER