Skip to content

Implement kernel_points and inverse_image for elliptic curve hom #40251

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

Merged
merged 9 commits into from
Jul 25, 2025

Conversation

user202729
Copy link
Contributor

@user202729 user202729 commented Jun 13, 2025

Ideally I want to implement .kernel_gens() (and eventually .kernel()) but that requires a lot of framework (subgroup of E.abelian_group(), etc.)

Also it may be possible to use a more efficient algorithm for EllipticCurveHom_composite, but I haven't worked out the math yet. (if you're lucky then taking the inverse image over each successtive φ works, but otherwise…) Currently it takes successive image, but might have bad worst case behavior.

📝 Checklist

  • The title is concise and informative.
  • The description explains in detail what this PR is about.
  • I have linked a relevant issue or discussion.
  • I have created tests covering the changes.
  • I have updated the documentation and checked the documentation preview.

⌛ Dependencies

@user202729 user202729 requested a review from JohnCremona June 13, 2025 11:30
Copy link

github-actions bot commented Jun 13, 2025

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

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.

A bunch of minor things.

Comment on lines +595 to +596
if Q not in self.codomain():
raise TypeError('input must be a point in the codomain')
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we explicitly cast Q to the codomain? For example, 2/2 in ZZ but it is not an integer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

__contains__ implementation of EllipticCurve checks isinstance(Q, EllipticCurveModel) and Q.curve() == self if I recalled correctly, so I think this does what it should.

Copy link
Contributor Author

@user202729 user202729 Jul 1, 2025

Choose a reason for hiding this comment

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

Actually you're right, the current behavior is like this

sage: f.inverse_image((0, 2))
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
...
AttributeError: 'tuple' object has no attribute 'is_zero'

on the other hand,

sage: f.codomain().coerce((0, 2))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
...
TypeError: no canonical coercion from <class 'tuple'> to Elliptic Curve defined by ...

it's not ideal to directly convert the point, since the zero point in any elliptic curve can be converted into the zero point in any other elliptic curve. I decide to cast it after checking Q in self.codomain().

New behavior:

sage: f.inverse_image((0, 2))
(2 : 3*z2 + 1 : 1)

Or maybe it's a good idea to make conversion not work for the wrong curve, or __contains__ not work for tuple? After all the documentation says

    def __contains__(self, P):
        """
        Return ``True`` if and only if P is a point on the elliptic curve.

        P just has to be something that can be coerced to a point.

see "coerced", not "converted". Yet the implementation converts.

Copy link
Collaborator

Choose a reason for hiding this comment

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

There isn't necessarily anything wrong with it using conversion to check (yes, the doc should be changed). Well, I'm not sure what the best course of action is here as I don't use this code. John is a very good person to ask about this.

Copy link
Member

Choose a reason for hiding this comment

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

I think that using contains as a test is fine. If E1 and E2 are different curves then "E1(0) in E2" is False.

@JohnCremona
Copy link
Member

I noticed this PR just when I was myself computing some inverse images under isogenies, so thanks!

For what I was doing I would need an option extend=True. Perhaps we can mae that a new issue, I would not want to delay this one. It's not an obvious extension since even for the x-coordinates, the roots will in general lie in different fields.

Copy link
Member

@JohnCremona JohnCremona left a comment

Choose a reason for hiding this comment

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

Nothing to add beyond what @tscrim has already said. Good!

@user202729
Copy link
Contributor Author

I realize there's a bug that if you just call any_root(), you get some x-coordinate that lies in the field but there's no guarantee that the y-coordinate is also in the field (in fact there isn't and previously sometimes the assertion fails). I fallback to roots() instead.

@JohnCremona
Copy link
Member

JohnCremona commented Jul 2, 2025

I realize there's a bug that if you just call any_root(), you get some x-coordinate that lies in the field but there's no guarantee that the y-coordinate is also in the field (in fact there isn't and previously sometimes the assertion fails). I fallback to roots() instead.

That is a good catch, which I should have noticed before yesterday's comment.

You might find it helpful to look at the code (which I wrote) for Q.division_points(n) which returns a list of P satisfying nP=Q. That's a specical case of what this function is doing. That code does look at every root x and tries to lift to (x,y). Now if 2Q=0 then both y's are valid solutions, while otherwise at most one is, and you may need to check both to see which one works.

@user202729
Copy link
Contributor Author

I took a look. The only difference with the current implementation is the check of is 2 torsion, but I think that check is not particularly useful, at best it saves 1-2 evaluation of the morphism at the cost of computing _neg_ to check for 2-torsion. Finding root is likely more expensive anyway.

vbraun pushed a commit to vbraun/sage that referenced this pull request Jul 14, 2025
sagemathgh-40251: Implement kernel_points and inverse_image for elliptic curve hom
    
Ideally I want to implement `.kernel_gens()` (and eventually
`.kernel()`) but that requires a lot of framework (subgroup of
`E.abelian_group()`, etc.)

Also it may be possible to use a more efficient algorithm for
`EllipticCurveHom_composite`, but I haven't worked out the math yet. (if
you're lucky then taking the inverse image over each successtive `φ`
works, but otherwise…) Currently it takes successive image, but might
have bad worst case behavior.

### 📝 Checklist

<!-- Put an `x` in all the boxes that apply. -->

- [x] The title is concise and informative.
- [x] The description explains in detail what this PR is about.
- [ ] I have linked a relevant issue or discussion.
- [x] I have created tests covering the changes.
- [ ] I have updated the documentation and checked the documentation
preview.

### ⌛ Dependencies

<!-- List all open PRs that this PR logically depends on. For example,
-->
<!-- - sagemath#12345: short description why this is a dependency -->
<!-- - sagemath#34567: ... -->
    
URL: sagemath#40251
Reported by: user202729
Reviewer(s): John Cremona, Travis Scrimshaw, user202729
vbraun pushed a commit to vbraun/sage that referenced this pull request Jul 18, 2025
sagemathgh-40251: Implement kernel_points and inverse_image for elliptic curve hom
    
Ideally I want to implement `.kernel_gens()` (and eventually
`.kernel()`) but that requires a lot of framework (subgroup of
`E.abelian_group()`, etc.)

Also it may be possible to use a more efficient algorithm for
`EllipticCurveHom_composite`, but I haven't worked out the math yet. (if
you're lucky then taking the inverse image over each successtive `φ`
works, but otherwise…) Currently it takes successive image, but might
have bad worst case behavior.

### 📝 Checklist

<!-- Put an `x` in all the boxes that apply. -->

- [x] The title is concise and informative.
- [x] The description explains in detail what this PR is about.
- [ ] I have linked a relevant issue or discussion.
- [x] I have created tests covering the changes.
- [ ] I have updated the documentation and checked the documentation
preview.

### ⌛ Dependencies

<!-- List all open PRs that this PR logically depends on. For example,
-->
<!-- - sagemath#12345: short description why this is a dependency -->
<!-- - sagemath#34567: ... -->
    
URL: sagemath#40251
Reported by: user202729
Reviewer(s): John Cremona, Travis Scrimshaw, user202729
@yyyyx4
Copy link
Member

yyyyx4 commented Jul 23, 2025

By the way, implementing a .kernel_gens() method on top of this is very easy: There is an AdditiveAbelianGroupWrapper.from_generators() method, and from there you can just extract a basis of the group. It's not the most efficient implementation, but it gets the job done, and optimizations can always come later.

@user202729
Copy link
Contributor Author

Cool. (I haven't looked into what algoithm it uses internally yet, but i the group order is highly composite then it should be polynomial time. Possibly with high exponent.)

If the group object exists, it would be more consistent to implement it as .kernel() of the hom object.

(then .kernel_points() as is would be bad API, since it should be accessible by iter(f.kernel()). still, I think if the latter is sufficiently efficiently implemented, then having .kernel_points() implemented as return iter(self.kernel()) isn't too bad.)

@yyyyx4
Copy link
Member

yyyyx4 commented Jul 23, 2025

The .from_generators() method is definitely polynomial-time in the prime divisors of the order (here: isogeny degree). What I meant with "not the most efficient" is that it is a bit wasteful to enumerate the entire kernel, only to extract just one or two points from the list. In the most common case (cyclic isogenies), it would be more favorable to look for a single root of the kernel polynomial right away whose associated point generates the kernel, and the generalization to non-cyclic isogenies should be easy enough as well.

@vbraun vbraun merged commit fa1479e into sagemath:develop Jul 25, 2025
23 of 25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants