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

Extend the method get_knotinfo of the Link class to cover non-prime knots #38254

Merged
merged 22 commits into from
Sep 29, 2024

Conversation

soehms
Copy link
Member

@soehms soehms commented Jun 21, 2024

This PR implements a task from the following code-comment in link.py:

   # ToDo: extension to non prime links in which case an element of the monoid
   # over :class:`KnotInfo` should be returned

In connection with this I switch the output of get_knotinfo to elements of the new class FreeKnotInfoMonoid for all knots (prime or not) For multi-component links I keep the output as it is, since the FreeKnotInfoMonoid does not make sence here.

Another thing changes: Currently, if the unique=False option is used symmetry mutants may be listed separately, even though they are known to be isotopic because their symmetry type is reversible or amphicheiral. Now, if symmetry mutants are known to be isotopic because of their symmetry type, just the minimal of them is listed (in the order of the enum).

📝 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

Copy link

github-actions bot commented Jun 21, 2024

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

@soehms
Copy link
Member Author

soehms commented Jun 27, 2024

All bots green!

@soehms soehms marked this pull request as ready for review June 27, 2024 16:58
@soehms
Copy link
Member Author

soehms commented Jun 28, 2024

Test get_knotinfo against the entire database

I haven't done this since the additional 13-crossing knots appeared, so I've done this now. On the first pass, I noticed an inconsistency between the results of the new from_knot method of the new monoid class and the existing get_knotinfo method of the Link class. This was fixed in the previous commit (see the test with K12a_165).

After that five knots are left, that cannot be recovered (non uniquely) by get_knotinfo:

sage: %time ll = [L for L in KnotInfo if not L.is_recoverable(unique=False)]
CPU times: user 2h 45min 38s, sys: 1.65 s, total: 2h 45min 39s
Wall time: 2h 45min 40s
sage: ll
[<KnotInfo.K13a_260: '13a_260'>,
 <KnotInfo.K13a_1805: '13a_1805'>,
 <KnotInfo.K13a_3534: '13a_3534'>,
 <KnotInfo.K13n_615: '13n_615'>,
 <KnotInfo.K13n_618: '13n_618'>]

But there must be something wrong with these in the database. All five are recorded as chiral knots:

sage: for K in ll:
....:     print(K, K.symmetry_type())
....:
KnotInfo.K13a_260 chiral
KnotInfo.K13a_1805 chiral
KnotInfo.K13a_3534 chiral
KnotInfo.K13n_615 chiral
KnotInfo.K13n_618 chiral

If this is correct, the braid_notation must be wrong, because it is the reverse of the pd_notation:

sage: for K in ll:
....:     print(K, K.link(use_item=K.items.braid_notation).get_knotinfo(unique=False))
....:
KnotInfo.K13a_260 [KnotInfo['K13a_260r']]
KnotInfo.K13a_1805 [KnotInfo['K13a_1805r']]
KnotInfo.K13a_3534 [KnotInfo['K13a_1805c'], KnotInfo['K13a_1805m'], KnotInfo['K13a_3534'], KnotInfo['K13a_3534r']]
KnotInfo.K13n_615 [KnotInfo['K13n_615r']]
KnotInfo.K13n_618 [KnotInfo['K13n_618r']]

This can be traced back to the conjugation of braids:

sage: a = KnotInfo.K13a_260.link().braid()
sage: b = KnotInfo.K13a_260.braid()
sage: a.is_conjugated(b)
False
sage: a.is_conjugated(b.reverse())
True
sage: a = KnotInfo.K13a_3534.link().braid()
sage: b = KnotInfo.K13a_3534.braid()
sage: a.is_conjugated(b)
False
sage: a.is_conjugated(b.reverse())
True
sage: am = a.mirror_image()
sage: bm = b.mirror_image()
sage: am.is_conjugated(bm)
False
sage: am.is_conjugated(bm.reverse())
True

I will clarify this with Chuck Livingston.

@soehms
Copy link
Member Author

soehms commented Jul 3, 2024

I will clarify this with Chuck Livingston.

There are much more cases with the above problem:

sage: %time mm = [K for K in KnotInfo if K.is_knot() and K.symmetry_type() == 'chiral' and K.link().braid().is_conjugated(K.braid().reverse())]
CPU times: user 22.6 s, sys: 30 µs, total: 22.6 s
Wall time: 22.6 s
sage: len(mm)
692

I didn't notice them before because they were better hidden by the original get_knotinfo implementation, which is still used for irreducible HOMFLY polynomials. More precisely, that implementation tries to identify the knot directly by pd_code, which has not been the case with the from_knot method of FreeKnotInfoMonoid until the previous commit. The smallest example in the above list mm is this:

sage: K = KnotInfo.K9_33
sage: b1 = K.braid(); b1
s0^-1*s1*s0^-1*s2^-1*s1^2*s0^-1*s1*s2^-1
sage: b2 = K.link().braid(); b2
(s0^-1*s1)^2*s1*s0^-1*s2^-1*s1*s2^-1
sage: b1.is_conjugated(b2)
False
sage: b1.is_conjugated(b2.reverse())
True

At the moment it is unclear to me whether there is incorrect data in the database or whether the Sage method braid returns reversed braids in these cases. However, this problem is independent of the changes made here.

@soehms soehms closed this Jul 5, 2024
@soehms soehms deleted the get_knotinfo_non_prime branch July 5, 2024 13:47
@soehms soehms restored the get_knotinfo_non_prime branch July 5, 2024 13:47
@soehms soehms reopened this Jul 5, 2024
@soehms soehms requested a review from tscrim July 5, 2024 13:51
@soehms
Copy link
Member Author

soehms commented Sep 18, 2024

CI shows a problem in this run, probably an incompatibility after merging:

@tscrim, I need your help here. According to your suggestion on July 15th I added an inheritance to Singleton in this commit on July 25th. This commit has been relative to 10.5.beta0. At that time all tests passed. But, after I merged with 10.5.beta3 this is failing, now.

It took me some time to find the commit which causes this. It was authored by @enriqueartal in connection with #38155 and changes the inheritance order in UniqueRepresentation. If you revert that change test-wise, the failure disappears:

diff --git a/src/sage/structure/unique_representation.py b/src/sage/structure/unique_representation.py
index c7bb3783658..cc843e9ac9e 100644
--- a/src/sage/structure/unique_representation.py
+++ b/src/sage/structure/unique_representation.py
@@ -1201,7 +1201,7 @@ class CachedRepresentation(WithPicklingByInitArgs):
             del cache[k]


-class UniqueRepresentation(WithEqualityById, CachedRepresentation):
+class UniqueRepresentation(CachedRepresentation, WithEqualityById):
     r"""
     Classes derived from ``UniqueRepresentation`` inherit a unique
     representation behavior for their instances

At the moment I have no idea to fix that properly. Of course one option would be to remove the inheritance from Singleton again. As far as I have seen, it did not really improve performance. What do you think?

@tscrim
Copy link
Collaborator

tscrim commented Sep 19, 2024

Hmm…interesting. I am a bit surprised by this, but it might be a good thing that this is showing up. I might need to look at it much more closely later.

One thing to do would be to put the Singleton class first in the MRO, but that probably won’t fix the problem. The issue seems to be that it is calling the __classcall__ of a base class, which takes limited arguments. As such, it likely needs to pass along *args, **kwds, or you deliberately skip it by replacing the super() with a more explicit base class.

@enriqueartal
Copy link
Contributor

This change was caused by a change of how to uniquely associate a GAP group to a Sage group. The MRO compiicated everything and the commit was done to solve it, from a suggestion from @tscrim . I completely trust @tscrim for possible solutions.

@soehms
Copy link
Member Author

soehms commented Sep 20, 2024

One thing to do would be to put the Singleton class first in the MRO, but that probably won’t fix the problem. The issue seems to be that it is calling the __classcall__ of a base class, which takes limited arguments. As such, it likely needs to pass along *args, **kwds, or you deliberately skip it by replacing the super() with a more explicit base class.

Many thanks to have a look at it! I have experimented with your suggestions. If I combine your first point with the second, I get:

Failed example:
    FKIM =  FreeKnotInfoMonoid()
Exception raised:
    Traceback (most recent call last):
      File "/home/sebastian/devel/sage/src/sage/doctest/forker.py", line 716, in _run
        self.compile_and_execute(example, compiler, test.globs)
      File "/home/sebastian/devel/sage/src/sage/doctest/forker.py", line 1146, in compile_and_execute
        exec(compiled, globs)
      File "<doctest sage.knots.free_knotinfo_monoid.FreeKnotInfoMonoidElement.as_knot[1]>", line 1, in <module>
        FKIM =  FreeKnotInfoMonoid()
      File "sage/misc/classcall_metaclass.pyx", line 321, in sage.misc.classcall_metaclass.ClasscallMetaclass.__call__
        return cls.classcall(cls, *args, **kwds)
      File "/home/sebastian/devel/sage/src/sage/knots/free_knotinfo_monoid.py", line 109, in __classcall_private__
        return super().__classcall__(cls, max_crossing_number, prefix=prefix, **kwds)
      File "sage/misc/fast_methods.pyx", line 298, in sage.misc.fast_methods.Singleton.__classcall__
        res = typecall(cls)
      File "sage/misc/classcall_metaclass.pyx", line 472, in sage.misc.classcall_metaclass.typecall
        return (<PyTypeObject*>type).tp_call(cls, args, kwds)
    TypeError: FreeKnotInfoMonoid.__init__() missing 1 required positional argument: 'max_crossing_number'

If I combine your first point with the third I get:

Failed example:
    TestSuite(FKIM).run()
Expected nothing
Got:
      Failure in _test_pickling:
      Traceback (most recent call last):
        File "/home/sebastian/devel/sage/src/sage/misc/sage_unittest.py", line 298, in run
          test_method(tester=tester)
        File "sage/structure/sage_object.pyx", line 679, in sage.structure.sage_object.SageObject._test_pickling
          tester.assertEqual(loads(dumps(self)), self)
        File "sage/misc/persist.pyx", line 990, in sage.misc.persist.loads
          ans = unpickler.load()
        File "sage/misc/classcall_metaclass.pyx", line 321, in sage.misc.classcall_metaclass.ClasscallMetaclass.__call__
          return cls.classcall(cls, *args, **kwds)
        File "sage/misc/fast_methods.pyx", line 297, in sage.misc.fast_methods.Singleton.__classcall__
          assert cls.mro()[1] == Singleton, "{} is not a direct subclass of {}".format(cls, Singleton)
      AssertionError: <class 'sage.knots.free_knotinfo_monoid.FreeKnotInfoMonoid_with_category'> is not a direct subclass of <class 'sage.misc.fast_methods.Singleton'>

@soehms
Copy link
Member Author

soehms commented Sep 20, 2024

This change was caused by a change of how to uniquely associate a GAP group to a Sage group. The MRO compiicated everything and the commit was done to solve it, from a suggestion from @tscrim .

Thanks for the info!

I completely trust @tscrim for possible solutions.

I do so, too!

@tscrim
Copy link
Collaborator

tscrim commented Sep 24, 2024

Sorry for being slow to respond (1 week old baby and public holidays). I need to experiment with this to figure things out. I hope to do so this week
Keep pestering me if I don't.

@soehms
Copy link
Member Author

soehms commented Sep 24, 2024

1 week old baby ...

That's really great news, Travis! Congratulations!

My kids are already adults and live more than 200 km away (both happen to be in Munich). But I still remember very well how radically their birth changed life (especially in the first case).

How about this plan:

  • We remove Singleton from the class again and merge this PR.
  • Then I open a follow-up PR that adds Singleton back in.

This would remove the time pressure and reduce the likelihood of further merge conflicts. What do you think?

@tscrim
Copy link
Collaborator

tscrim commented Sep 24, 2024

1 week old baby ...

That's really great news, Travis! Congratulations!

My kids are already adults and live more than 200 km away (both happen to be in Munich). But I still remember very well how radically their birth changed life (especially in the first case).

Thank you. Indeed, although this is the second, so we are a bit more seasoned parents.

How about this plan:

* We remove `Singleton` from the class again and merge this PR.
* Then I open a follow-up PR that adds `Singleton` back in. 

This would remove the time pressure and reduce the likelihood of further merge conflicts. What do you think?

-1 Let's get this right the first time. I won't need much time, but I just need to play around a bit see what is happening.

@soehms
Copy link
Member Author

soehms commented Sep 24, 2024

Thank you. Indeed, although this is the second, so we are a bit more seasoned parents.

Yes, the second should be more relaxed (at least in the beginning, before they have their own personality).

-1 Let's get this right the first time. I won't need much time, but I just need to play around a bit see what is happening.

No problem!

@tscrim
Copy link
Collaborator

tscrim commented Sep 25, 2024

This is actually independent of the change in #38155, which somehow just made the bug appear. There are two issues:

  1. This should not inherit from Singleton, which is for classes that have precisely 1 instance. So by definition, it cannot take parameters. Contrast this with UniqueRepresentation, which is 1 instance for each (normalized) input.
  2. The generic IndexedMonoid.__classcall__ was designed with a particular API with the goal to avoid code duplication. We need to work around this.

Here is a diff with the changes that I was able to get the code work:

diff --git a/src/sage/knots/free_knotinfo_monoid.py b/src/sage/knots/free_knotinfo_monoid.py
index acf286fba0b..c8301a4d138 100644
--- a/src/sage/knots/free_knotinfo_monoid.py
+++ b/src/sage/knots/free_knotinfo_monoid.py
@@ -27,7 +27,7 @@ AUTHORS:
 from sage.knots.knotinfo import SymmetryMutant
 from sage.monoids.indexed_free_monoid import IndexedFreeAbelianMonoid, IndexedFreeAbelianMonoidElement
 from sage.misc.cachefunc import cached_method
-from sage.misc.fast_methods import Singleton
+from sage.structure.unique_representation import UniqueRepresentation
 
 
 class FreeKnotInfoMonoidElement(IndexedFreeAbelianMonoidElement):
@@ -89,7 +89,7 @@ class FreeKnotInfoMonoidElement(IndexedFreeAbelianMonoidElement):
         return [P._index_dict[w] for w in wl]
 
 
-class FreeKnotInfoMonoid(IndexedFreeAbelianMonoid, Singleton):
+class FreeKnotInfoMonoid(IndexedFreeAbelianMonoid):
 
     Element = FreeKnotInfoMonoidElement
 
@@ -106,12 +106,15 @@ class FreeKnotInfoMonoid(IndexedFreeAbelianMonoid, Singleton):
             sage: FreeKnotInfoMonoid(5)
             Free abelian monoid of knots with at most 5 crossings
         """
-        return super().__classcall__(cls, max_crossing_number, prefix=prefix, **kwds)
+        if not prefix:
+            prefix = 'KnotInfo'
+        # We skip the IndexedMonoid__classcall__
+        return UniqueRepresentation.__classcall__(cls, max_crossing_number, prefix=prefix, **kwds)
 
     def __init__(self, max_crossing_number, category=None, prefix=None, **kwds):
         r"""
-        Init this monoid with generators belonging to prime knots with at most
-        ``max_crossing_number`` crossings.
+        Initialize ``self`` with generators belonging to prime knots with
+        at most ``max_crossing_number`` crossings.
 
         TESTS:
 
@@ -125,8 +128,6 @@ class FreeKnotInfoMonoid(IndexedFreeAbelianMonoid, Singleton):
         self._set_index_dictionary(max_crossing_number=max_crossing_number)
         from sage.sets.finite_enumerated_set import FiniteEnumeratedSet
         indices = FiniteEnumeratedSet(self._index_dict)
-        if not prefix or prefix == 'F':
-            prefix = 'KnotInfo'
         super().__init__(indices, prefix)
 
     def _from_knotinfo(self, knotinfo, symmetry_mutant):

Note the normalization of the prefix should happen in the __classcall__.

@soehms
Copy link
Member Author

soehms commented Sep 25, 2024

This is actually independent of the change in #38155, which somehow just made the bug appear. There are two issues:

  1. This should not inherit from Singleton, which is for classes that have precisely 1 instance. So by definition, it cannot take parameters. Contrast this with UniqueRepresentation, which is 1 instance for each (normalized) input.
  2. The generic IndexedMonoid.__classcall__ was designed with a particular API with the goal to avoid code duplication. We need to work around this.

Here is a diff with the changes that I was able to get the code work:

....

Note the normalization of the prefix should happen in the __classcall__.

Thank you for your suggestion and the very clear explanations! Locally all tests under knots passed. Let's wait for the bots.

@tscrim
Copy link
Collaborator

tscrim commented Sep 26, 2024

Green bot => setting positive review.

@soehms
Copy link
Member Author

soehms commented Sep 26, 2024

Green bot => setting positive review.

Thanks!

vbraun pushed a commit to vbraun/sage that referenced this pull request Sep 27, 2024
… cover non-prime knots

    
<!-- ^ Please provide a concise and informative title. -->
<!-- ^ Don't put issue numbers in the title, do this in the PR
description below. -->
<!-- ^ For example, instead of "Fixes sagemath#12345" use "Introduce new method
to calculate 1 + 2". -->
<!-- v Describe your changes below in detail. -->
<!-- v Why is this change required? What problem does it solve? -->
<!-- v If this PR resolves an open issue, please link to it here. For
example, "Fixes sagemath#12345". -->

This PR implements a task from the following code-comment in `link.py`:

>        # ToDo: extension to non prime links in which case an element
of the monoid
>        # over :class:`KnotInfo` should be returned

In connection with this I switch the output of `get_knotinfo` to
elements of the new class `FreeKnotInfoMonoid` for all knots (prime or
not) For multi-component links I keep the output as it is, since the
`FreeKnotInfoMonoid` does not make sence here.

Another thing changes: Currently, if the `unique=False` option is used
symmetry mutants may be listed separately, even though they are known to
be isotopic because their symmetry type is reversible or amphicheiral.
Now, if symmetry mutants are known to be isotopic because of their
symmetry type, just the minimal of them is listed (in the order of the
enum).


### 📝 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.
- [x] 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#38254
Reported by: Sebastian Oehms
Reviewer(s): Sebastian Oehms, Travis Scrimshaw
@vbraun vbraun merged commit 6a039da into sagemath:develop Sep 29, 2024
20 checks passed
@soehms soehms deleted the get_knotinfo_non_prime branch September 29, 2024 09:58
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