Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

avoid unnecessary divisions and calls to gcd #38924

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from

Conversation

mantepse
Copy link
Collaborator

@mantepse mantepse commented Nov 5, 2024

In an effort to make #38108 more usable, we optimize the computation of gcd's in generic polynomial rings.

Copy link

github-actions bot commented Nov 5, 2024

Documentation preview for this PR (built with commit a8e2ca1; changes) is ready! 🎉
This preview will update shortly after each push to this PR.

@mantepse
Copy link
Collaborator Author

mantepse commented Nov 5, 2024

This reduces the time for the example from #38108 and some others significantly. The old timing is

sage: h = SymmetricFunctions(QQ).h()
sage: L.<t, u> = LazyPowerSeriesRing(h.fraction_field())
sage: D = L.undefined()
sage: s1 = L.sum(lambda n: h[n]*t^(n+1)*u^(n-1), 1)
sage: L.define_implicitly([D], [u*D - u - u*s1*D - t*(D - D(t, 0))])
sage: D[0]
h[]
sage: %time repr(D)
CPU times: user 28.6 s, sys: 8.55 ms, total: 28.6 s
Wall time: 28.7 s
'h[] + h[1]*t^2 + ((h[1,1]+h[2])*t^4+h[2]*t^3*u) + ((h[1,1,1]+3*h[2,1]+h[3])*t^6+(2*h[2,1]+h[3])*t^5*u+h[3]*t^4*u^2) + O(t,u)^7'                             

The new timing is about 5 seconds. With a more aggressive patch we could reduce it to 3.6 seconds.

sage: P.<x,y>=ProjectiveSpace(QQbar, 1)
sage: E=EllipticCurve([1, 2])
sage: f=P.Lattes_map(E, 2)
sage: %time f.Lattes_to_curve(check_lattes=true)
CPU times: user 24.5 s, sys: 156 ms, total: 24.7 s
Wall time: 24.9 s
Elliptic Curve defined by y^2 = x^3 + x + 2 over Rational Field

The new timing is about 15 seconds.

The drawback is that some computations give less beautiful (but of course, equivalent) results. For example:

sage: R.<a,b> = NumberField(x^2 - 3, 'g').extension(x^2 - 7, 'h')[]
sage: h = R.base_ring().gen()
sage: S.<y> = R.fraction_field()[]
sage: xgcd(y^2, a*h*y + b)
(1, 7*a^2/b^2, (((-7)*a)/(h*b^2))*y + 7/(7*b))

instead of (1, 7*a^2/b^2, (((-h)*a)/b^2)*y + 1/b).

@mantepse
Copy link
Collaborator Author

mantepse commented Nov 5, 2024

The failures are a bit puzzling. In arith/misc.py:

        sage: # needs sage.rings.number_field
        sage: R.<a,b> = NumberField(x^2 - 3, 'g').extension(x^2 - 7, 'h')[]
        sage: h = R.base_ring().gen()
        sage: S.<y> = R.fraction_field()[]
        sage: xgcd(y^2, a*h*y + b)
        (1, 7*a^2/b^2, (((-h)*a)/b^2)*y + 1/b)

which gives with this patch

(1, 7*a^2/b^2, (((-7)*a)/(h*b^2))*y + 7/(7*b))

These are the same elements, just represented differently.

In schemes/affine/affine_morphism.py:

            sage: A.<z> = AffineSpace(QQbar, 1)
            sage: H = End(A)
            sage: f = H([2*z / (z^2 + 2*z + 3)])
            sage: f.homogenize(1)
            Scheme endomorphism of Projective Space of dimension 1
             over Algebraic Field
              Defn: Defined on coordinates by sending (x0 : x1) to
                    (x0*x1 : 1/2*x0^2 + x0*x1 + 3/2*x1^2)

we now get

Scheme endomorphism of Projective Space of dimension 1 over Algebraic Field
  Defn: Defined on coordinates by sending (x0 : x1) to
        (2*x0*x1 : x0^2 + 2*x0*x1 + 3*x1^2)

I don't know, whether this is really the same thing.

I guess they come from the fact that gcd(0, a) may differ from a by a unit.

@tscrim
Copy link
Collaborator

tscrim commented Nov 7, 2024

Yes, that is the same morphism as it is projective space (and thus you can scale the coordinates freely).

Copy link
Collaborator

@tscrim tscrim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am pretty sure that b divides d in the last part because d is the gcd of the coefficients of the initial A and B, which then we reduce B by some common factor with A and then get the gcd of the resulting coefficients. So it should be safe to do return (d // b) * B. Although perhaps we should cc someone who is more of an expert to triple-check this.

@mantepse mantepse marked this pull request as draft November 7, 2024 16:00
@mantepse
Copy link
Collaborator Author

mantepse commented Nov 7, 2024

It needs work, because I want to incorporate the much faster version I now have, and I think that the current version has some weak spots. For example, gcd is not required to return 1 for units - at least this is not done in several cases, so testing whether the result equals one is a bit weak.

While my implementation is very much faster on some examples, there are others where it leads to problems, and I have to investigate this.

@tscrim
Copy link
Collaborator

tscrim commented Nov 7, 2024

It needs work, because I want to incorporate the much faster version I now have, and I think that the current version has some weak spots. For example, gcd is not required to return 1 for units - at least this is not done in several cases, so testing whether the result equals one is a bit weak.

Indeed, the gcd is only defined up to a unit.

While my implementation is very much faster on some examples, there are others where it leads to problems, and I have to investigate this.

It would be good with your push to add some tests for these cases (at least, given the bot results, it seems like they are not already included).

@enriqueartal
Copy link
Contributor

It is maybe unrelated but it is possible to compute the gcd for a list of rational functions. I encountered a case where the computation takes long (I do not know how long since I stopped it) but working with the gcd's (and maybe lcm's) of numerators and denominators, it becomes very fast.

@enriqueartal
Copy link
Contributor

Sorry for the noise, lcm is slow.

@mantepse
Copy link
Collaborator Author

mantepse commented Nov 9, 2024

I think the patch as is is quite good, so setting to needs-review, I will also advertise it on sage-devel. The results are not as beautiful as with the current version, but it is significantly faster.

I have an additional improvement, which I am not so sure about. It avoids constructing a new polynomial ring for each variable. Instead, it constructs a univariate ring over the polynomial ring once, and reuses this until all coefficients are constants. I am sure this could be further improved and streamlined, but it is unclear whether it is worth the effort.

@mantepse
Copy link
Collaborator Author

mantepse commented Nov 9, 2024

I am pretty sure that b divides d in the last part because d is the gcd of the coefficients of the initial A and B, which then we reduce B by some common factor with A and then get the gcd of the resulting coefficients. So it should be safe to do return (d // b) * B. Although perhaps we should cc someone who is more of an expert to triple-check this.

This turns out not to be the case:

sage: B.<X, Y> = QQ[]
sage: C.<f> = B[]
sage: p = X*f^3 + X^2*Y*f
sage: q = X*Y*f^2
sage: C._gcd_univariate_polynomial(p, q)

At the end of the algorithm, B = X*Y*f, b = X*Y and d = X, so b does not divide d.

I did not yet check whether this is to be expected.

@mantepse mantepse marked this pull request as ready for review November 9, 2024 20:54
@mantepse
Copy link
Collaborator Author

I actually do not quite understand what happens concerning the beauty of results. For example, also in develop we have

sage: R.<x,y> = QQbar[]
sage: 7*x / (7*y)
7*x/(7*y)

@tscrim
Copy link
Collaborator

tscrim commented Nov 12, 2024

That's a good example. Can you add it as a test and a note in the code?

I don't know why that isn't reducing itself properly. That would be a bug IMO.

@mantepse
Copy link
Collaborator Author

That's a good example. Can you add it as a test and a note in the code?

I'm not sure where, but yes.

I don't know why that isn't reducing itself properly. That would be a bug IMO.

I don't think it's a bug: the gcd is one:

sage: a = 7*x; b = 7*y
sage: gcd(a, b)
1

We could require fractions of polynomials to have monic leading terms, but I don't think we always want that:

sage: R.<r,s> = QQ[]
sage: (3*r+s)/(2*r+s)
(3*r + s)/(2*r + s)

@mantepse
Copy link
Collaborator Author

In fact:

sage: R.<r,s> = QQ[]
sage: (3*r+6)/(3*r+3)
(3*r + 6)/(3*r + 3)

@@ -652,7 +657,7 @@ def gcd(self,other):
from sage.rings.integer_ring import ZZ
try:
return P(ZZ(self).gcd(ZZ(other)))
except TypeError:
except (TypeError, ValueError):
Copy link
Collaborator Author

@mantepse mantepse Nov 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly this is wrong. Should we always raise a TypeError in _integer_? Currently we don't, for example, in QmodnZ_Element, ComplexIntervalFieldElement, ComplexBall, RealBall, LaurentPolynomial, LocalizationElement, AlgebraicNumber, AlgebraicReal, RealIntervalFieldElement, pAdicZZpXCAElement, pAdicZZpXFMElement, pAdicZZpXCRElement, pAdicCappedRelativeElement.

Or should it actually always be a ValueError?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is good to widen what we catch. Yes, we probably should be more consistent, but that isn't a reason for this to fail. Do you have an example where it was breaking before this change?

@tscrim
Copy link
Collaborator

tscrim commented Nov 12, 2024

I don't know why that isn't reducing itself properly. That would be a bug IMO.

I don't think it's a bug: the gcd is one:

sage: a = 7*x; b = 7*y
sage: gcd(a, b)
1

Okay, but only because 7 is a unit. (Although if you do the gcd computation in QQbar, you get 7.) It is kind of a bug since it isn't detecting that the unit in the rational function is the leading coefficients of both...

@mantepse
Copy link
Collaborator Author

I don't know why that isn't reducing itself properly. That would be a bug IMO.

I don't think it's a bug: the gcd is one:

sage: a = 7*x; b = 7*y
sage: gcd(a, b)
1

Okay, but only because 7 is a unit. (Although if you do the gcd computation in QQbar, you get 7.) It is kind of a bug since it isn't detecting that the unit in the rational function is the leading coefficients of both...

Yes, but then

sage: R.<r,s> = QQ[]
sage: (3*r+6)/(3*r+3)
(3*r + 6)/(3*r + 3)

is also a bug. Put differently, it is a decision to take in multivariate polynomial normalization, I think.

@tscrim
Copy link
Collaborator

tscrim commented Nov 14, 2024

This is an interesting inconsistency (tested on 10.5.beta3):

sage: R.<x,y> = QQbar[]
sage: gcd(7*x, 7*y)
7
sage: (7*x).gcd(7*y)
7
sage: R.<x,y> = QQ[]
sage: gcd(7*x, 7*y)
1

Personally, I feel that putting at least the leading coefficient of the denominator when working over a field to 1 is desirable to normalize the elements. Well, I guess this is off topic to this PR at the end of the day.

@mantepse
Copy link
Collaborator Author

Personally, I feel that putting at least the leading coefficient of the denominator when working over a field to 1 is desirable to normalize the elements. Well, I guess this is off topic to this PR at the end of the day.

OK, I'll adapt the doctests then. I really dislike

            sage: P.<x,y> = ProjectiveSpace(Qp(3), 1)
            sage: f = DynamicalSystem_Berkovich([2*x^2, 2*y^2])
            sage: f.normalize_coordinates(); f
            Dynamical system of Projective Berkovich line over Cp(3) of precision 20
             induced by the map
              Defn: Defined on coordinates by sending (x : y)
               to ((2 + O(3^20))*x^2 : (2 + O(3^20))*y^2)

but I got no reaction on sage-devel.

@mantepse
Copy link
Collaborator Author

So, I just checked how univariate polynomial rings are treated specially. There is an implementation of the method fraction_field in PolynomialRing_field (which is for univariate polynomial rings only), which returns FractionField_1poly_field. On the other hand, there is apparently no special class for multivariate polynomial rings over fields.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants