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

special subs for fractions #37143

Merged
merged 7 commits into from
Feb 13, 2024

Conversation

mantepse
Copy link
Contributor

@mantepse mantepse commented Jan 22, 2024

We create a specialised method subs for FractionFieldElements, for better performance. In particular, the generic Element.subs method insists on replacing all gens, which is a problem in polynomial rings with many variables, such as (potentially) the InfinitePolynomialRing.

Fixes #37122.

@mantepse
Copy link
Contributor Author

@tscrim, the current pull request has a strange problem, when substitute is used. I admit, I do not even know what difference there should be between subs, substitute and __call__. Here is a typical failure:

sage -t --long --warn-long 66.2 --random-seed=201917749899107703140658241975105971597 src/sage/combinat/similarity_class_type.py
**********************************************************************
File "src/sage/combinat/similarity_class_type.py", line 71, in sage.combinat.similarity_class_type
Failed example:
    M.class_card()
Exception raised:
    Traceback (most recent call last):
      File "sage/misc/cachefunc.pyx", line 1962, in sage.misc.cachefunc.CachedMethodCaller.__call__
        return cache[k]
    KeyError: ([1, [1]], ((q,), ()))

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last):
      File "sage/misc/cachefunc.pyx", line 1962, in sage.misc.cachefunc.CachedMethodCaller.__call__
        return cache[k]
    KeyError: ([1, [1]], ((Cached version of <function centralizer_group_cardinality at 0x7f0cc00d3640>, q), ()))

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last):
      File "/home/martin/sage/src/sage/doctest/forker.py", line 712, in _run
        self.compile_and_execute(example, compiler, test.globs)
      File "/home/martin/sage/src/sage/doctest/forker.py", line 1147, in compile_and_execute
        exec(compiled, globs)
      File "<doctest sage.combinat.similarity_class_type[2]>", line 1, in <module>
        M.class_card()
      File "/home/martin/sage/src/sage/combinat/similarity_class_type.py", line 1006, in class_card
        return order_of_general_linear_group(self.size(), q=q) / self.centralizer_group_card(q=q)
      File "/home/martin/sage/src/sage/combinat/similarity_class_type.py", line 883, in centralizer_group_card
        return prod([PT.centralizer_group_card(q=q) for PT in self])
      File "/home/martin/sage/src/sage/combinat/similarity_class_type.py", line 883, in <listcomp>
        return prod([PT.centralizer_group_card(q=q) for PT in self])
      File "sage/misc/cachefunc.pyx", line 1967, in sage.misc.cachefunc.CachedMethodCaller.__call__
        w = self._instance_call(*args, **kwds)
      File "sage/misc/cachefunc.pyx", line 1842, in sage.misc.cachefunc.CachedMethodCaller._instance_call
        return self.f(self._instance, *args, **kwds)
      File "/home/martin/sage/src/sage/combinat/similarity_class_type.py", line 604, in centralizer_group_card
        return self.statistic(centralizer_group_cardinality, q=q)
      File "sage/misc/cachefunc.pyx", line 1967, in sage.misc.cachefunc.CachedMethodCaller.__call__
        w = self._instance_call(*args, **kwds)
      File "sage/misc/cachefunc.pyx", line 1842, in sage.misc.cachefunc.CachedMethodCaller._instance_call
        return self.f(self._instance, *args, **kwds)
      File "/home/martin/sage/src/sage/combinat/similarity_class_type.py", line 581, in statistic
        return q.parent()(func(self.partition()).substitute(q=q**self.degree()))
      File "sage/structure/element.pyx", line 938, in sage.structure.element.Element.substitute
        return self.subs(in_dict,**kwds)
      File "sage/rings/fraction_field_element.pyx", line 486, in sage.rings.fraction_field_element.FractionFieldElement.subs
        num = self._numerator.subs(*args, **kwds)
      File "sage/rings/polynomial/polynomial_element.pyx", line 443, in sage.rings.polynomial.polynomial_element.Polynomial.subs
        return self(*x, **kwds)
      File "sage/rings/polynomial/polynomial_integer_dense_flint.pyx", line 475, in sage.rings.polynomial.polynomial_integer_dense_flint.Polynomial_integer_dense_flint.__call__
        return Polynomial.__call__(self, *x, **kwds)
      File "sage/rings/polynomial/polynomial_element.pyx", line 818, in sage.rings.polynomial.polynomial_element.Polynomial.__call__
        raise TypeError("unsupported mix of keyword and positional arguments")

Experimentally, I replaced substitute with subs in fusion_ring.py, but I have no idea whether I should be doing this or not.

@tscrim
Copy link
Collaborator

tscrim commented Jan 23, 2024

AFAICS, subs and substitute should be the same as the latter redirects (the generic method in element.pyx or is an alias for the former. However, the generic substitute has an extra in_dict argument that gets passed as the first parameter. This is likely causing the issues you noticed (with a lack of uniformity).

For __call__, that should be evaluating the polynomial as a function, which means the input should make sense as a function (e.g., have the same number of arguments as variables when not using named arguments).

Sorry I cannot work much more on this due to preparing to travel back from Okinawa to Sapporo tomorrow. I will try to get to this (and the other PR) soon.

@mantepse
Copy link
Contributor Author

AFAICS, subs and substitute should be the same as the latter redirects (the generic method in element.pyx or is an alias for the former. However, the generic substitute has an extra in_dict argument that gets passed as the first parameter. This is likely causing the issues you noticed (with a lack of uniformity).

Hm. I only see as signatures either in_dict=None, **kwds or *args, **kwds:

data_structures/stream.py:    def subs(self, d):
matrix/matrix2.pyx:    def subs(self, *args, **kwds):
modules/free_module_element.pyx:    def subs(self, in_dict=None, **kwds):
rings/polynomial/polynomial_element.pyx:    def subs(self, *x, **kwds):
rings/polynomial/multi_polynomial_sequence.py:    def subs(self, *args, **kwargs):
rings/polynomial/laurent_polynomial_mpair.pyx:    def subs(self, in_dict=None, **kwds):
rings/polynomial/multi_polynomial_libsingular.pyx:    def subs(self, fixed=None, **kw):
rings/polynomial/multi_polynomial_element.py:    def subs(self, fixed=None, **kw):
rings/polynomial/multi_polynomial_ideal.py:    def subs(self, in_dict=None, **kwds):
rings/polynomial/infinite_polynomial_element.py:    def subs(self, fixed=None, **kwargs):
rings/polynomial/pbori/pbori.pyx:    def subs(self, in_dict=None, **kwds):
rings/fraction_field_FpT.pyx:    def subs(self, *args, **kwds):
structure/element.pyx:    def subs(self, in_dict=None, **kwds):

It would be good to know what the intended signature is.

Sorry I cannot work much more on this due to preparing to travel back from Okinawa to Sapporo tomorrow. I will try to get to this (and the other PR) soon.

No pressure!

@tscrim
Copy link
Collaborator

tscrim commented Jan 29, 2024

I believe the "correct" signature is the first argument is the in_dict=None, but there isn't a precise specification about that. However, all of the implementations should be able to handle their own signatures.

I think the best fix is to follow what polynomial_element.pyx does and declare substitute = subs. This makes the signature and handling consistent. I think this has some slight technical debt as it further exacerbates the lack of a uniform signature. Yet it is in line with the standard Pythonic "pass the parameters" paradigm and it is a very simple solution. So the benefits vastly outweigh any technical debt incurred IMO.

This makes the similarity_class_type.py tests pass for me and you probably don't need to change the fusion ring tests (which fail for the same reason I believe).

@mantepse mantepse requested a review from tscrim January 29, 2024 14:39
@mantepse mantepse marked this pull request as ready for review January 29, 2024 14:39
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.

It seems like that also worked for you. Just a few minor things. Although I would probably remove the INPUT and OUTPUT blocks as they provide no useful information.

@mantepse
Copy link
Contributor Author

The failure in cluster_seed.py is real. I think it happens, because the keys in the dictionary eval_dict are in the fraction field, rather than in the polynomial ring. Should I work around this by forcing the keys into the parent of numerator and denominator, right? Or should this be done in the polynomial ring?

@tscrim
Copy link
Collaborator

tscrim commented Jan 30, 2024

The failure in cluster_seed.py is real. I think it happens, because the keys in the dictionary eval_dict are in the fraction field, rather than in the polynomial ring. Should I work around this by forcing the keys into the parent of numerator and denominator, right? Or should this be done in the polynomial ring?

This seems to be a general issue too. Before, subs was redirecting to __call__ and converting all of its inputs into keyword arguments. So it didn't care whether it was a fraction field element or a polynomial ring element. In fact, any change we do could introduce a backwards incompatibility (only the string needed to match).

That being said, it probably would be better to be more strict about this input keys being convertible (perhaps coerceable?) into the defining ring of the fraction field. Nor can we assume the inputs are in ta polynomial ring. Thus, I think your first option is best: check if the first argument of x is a dict and then convert the keys into the base ring.

…merator and denominator, unify subs signature, allow generator indices as keys consistently
@tscrim
Copy link
Collaborator

tscrim commented Jan 31, 2024

I think this is a step backwards for fraction field elements. Specifically, fraction fields are not just for polynomials; they should not necessarily require subs to have a fixed signature (much less with more general arguments). It also makes it more future-proof (e.g., multivariate polynomial subs allows multiple position arguments).

Definitely +1 on the change to Polynomial.subs though. However, I think this example will no longer work:

sage: R.<x> = QQ[]
sage: x.subs({x^2/x: 2})
2

Also, if not k is True (e.g.: {0: x}), then why should that work?

@mantepse
Copy link
Contributor Author

I think I don't understand yet what you are writing and what you would like to have. Let me try to explain my intentions and how I understand the current situation in sage mainline.

The philosophy I applied is that subs only replaces generators of a ring (with generators). We expect the generators to be substituted to be algebraically independent, but we don't check this. Generators are indexed, the list of generators always has a fixed (but arbitrary) order.

The generators of the fraction field are the generators of the underlying ring, turned into elements of the fraction field.

I don't understand what you have in mind concerning the signature, I thought there are only three different ways to call subs:

1.) either passing a dict (which should be the preferred option in my opinion)
2.) passing keyword arguments
12.) passing both ( somewhat surprisingly)
3.) passing a single value if there is only one generator

For 1.) there is currently at least one constructor that allows the keys to be integers, the indices of the generators instead
For 2.) this is not the preferred option, because the keys are actually strings, so we run into trouble for example in the infinite polynomial ring

The first example you provide still works, essentially because subs only sees the result of x^2/x, which happens to be a generator. The second, p.subs({0:x}) currently raises an error in univariate polynomial rings, but replaces the 0-th generator in multivariate polynomial rings (or some things derived from that), and does nothing in a fraction field, or the LaurentPolynomialRing. I'm not claiming that the current version of the PR changes makes all of these consistent, but I would if we can agree.

(I'm here at #sd125, best wishes from Frederic who is sitting in the sun next to me.)

@tscrim
Copy link
Collaborator

tscrim commented Jan 31, 2024

Ah, sun; here in Hokkaido the sun means icy walkways...

Let me try to expand a bit on my concern. FractionField is a general class that does not have control on its inputs. In particular, any subclass of Element could implement their own version of subs that is consistent with Element.subs. In particular, such an implementation could allow more (unnamed) arguments. We don't lose anything by having and passing along an *args. Does that help explain my previous comment? (Of course, we don't currently have this AFAIK, but it is a simple future-proofing thing.)

Note in vanilla Sage:

sage: R.<x> = QQ[]
sage: x.subs({0: x^2})
TypeError: keys do not match self's parent

Actually, this seems to be inconsistent with the multivariate case. So big +1 on allowing.

So my first "example" for the univariate case works but not for the multivariate:

sage: R.<x,y> = QQ[]
sage: x.subs({x^2/x: x^2})
TypeError: keys do not match self's parent

Note that x^2/x belongs to the fraction field of R, hence the failure. Either polynomial rings should try to convert to generators (but this opens up a huge can of worms involving coercions between polynomial rings or things that "look like" the generators) or we just require the user to be careful (my preference).

I would say the (2) is the most natural, and the infinite poly ring should handle it properly. Although that is a separate discussion.

Looking forward to seeing both of you in March.

@mantepse
Copy link
Contributor Author

I think I understand your concern, but I would not know what to do with args then, if I don't know what's in them.

I must translate the generators passed in a dict to the underlying ring, I don't see a way out of that. (I would also rather not use coercion to do that. If I understand correctly, we agree here).

Of course, I could check for each argument whether it is a dictionary, and then replace the keys of these. Is this what you have in mind? I don't see the benefit, I must say. I might also misunderstand you because you said before that in_dict=None, **kwds would be the 'correct' signature.

Frédéric just noticed that in Matrix.subs we probably do want the signature *args, **kwds, because of the special subs in SR. Slightly off off topic, it does do surprising things.

sage: var("x y z")
(x, y, z)
sage: m = matrix(SR, [[e^x, x/y, z*x]])
sage: m.subs(x/y == 1)
[e^x   1 x*z]
sage: m.subs(x/y == 1, z == 1)
[e^x   1   x]
sage: m.subs(x/y == 1, z == 1, x == 1)
[  e 1/y   1]

However, I do not see the benefit of this signature over

sage: m.subs({x/y: 1, z: 1, x: 1})
[  e 1/y   1]

So to summarize, I need to know what to do if args is a list of length greater than one.

@tscrim
Copy link
Collaborator

tscrim commented Jan 31, 2024

I think I understand your concern, but I would not know what to do with args then, if I don't know what's in them.

You would have

def subs(in_dict=None, *args, **kwds):
    if isinstance(in_dict, dict):
        # parse
    num = self._numerator.subs(in_dict, *args, **kwds)

I must translate the generators passed in a dict to the underlying ring, I don't see a way out of that. (I would also rather not use coercion to do that. If I understand correctly, we agree here).

You actually are secretly using coercion by index, which calls == and uses coercion. Coercion is likely too weak too as there isn't a coercion from rational functions to the corresponding polynomial ring. You're really best doing a conversion and bailing out if that errors out.

Of course, I could check for each argument whether it is a dictionary, and then replace the keys of these. Is this what you have in mind? I don't see the benefit, I must say. I might also misunderstand you because you said before that in_dict=None, **kwds would be the 'correct' signature.

That's not quite what I said. Just that in_dict should be the first argument.

Frédéric just noticed that in Matrix.subs we probably do want the signature *args, **kwds, because of the special subs in SR. Slightly off off topic, it does do surprising things.
[snip]
So to summarize, I need to know what to do if args is a list of length greater than one.

You can just pass them along.

@mantepse mantepse added the sd125 sage days 125 label Jan 31, 2024
Comment on lines +434 to +435
if not in_dict:
return self(*args, **kwds)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Note that this is now a subtle change in behavior. Before, the empty dict {} would perform no substitutions if extra arguments were passed. Now it will do a substitution based on the inputs. The behavior is not well-defined in either case (mixing two input methods), but this will now agree with the multivariate case. I am just pointing this out this behavior change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree. Note that, a priori, subs is not disallowing mixing of substitution methods:

sage: R.<x,y> = QQ[]
sage: p = x + y
sage: p.subs({0: 1}, y=2)
3

Comment on lines 479 to 485
def to_R(m):
try:
mi = gens.index(m)
except ValueError:
return m
return mi
in_dict = {to_R(m): v for m, v in in_dict.items()}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This can be stronger than doing conversion (quite possibly more expensive too) but it could also be independent. The issue is that equality check that is done as part of index() goes through coercion. This means that it could potentially construct a pushout and do the comparison in the larger ring. So it might allow for unrelated variables than a strict conversion R(m) check would not allow. On the other than, conversion can be much more general depending on the implementation, but I would imagine it covers most reasonable pushout cases that would result in an equality. It is likely to be the most consistent as well.

Also, this implementation might rely on equal elements having equal hashes (which Python says should be true, but coercion and pushouts can lead to messy cases where this unintentionally does not hold).

Because of this, I think you are best simply doing

R = self.parent()
in_dict = {R(m): v for m,. v in in_dict.items()}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we want to restrict keys to generators and indices of generators. I think your suggestion does not allow indices, although that should be easy to fix. However, I would also like to look at a few concrete examples to make up my mind. My experience with the constructors is that they are quite careless in some cases, which is why I prefer to avoid them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I was very imprecise and misleading. What I meant to say is, that we want to restrict keys to things that the user regards as generators. More to the point: I think we don't want to allow for things like {x/y: 1} in a ring of rational functions. On the other hand, maybe we should, I don't really know.

The idea to allow integers is to have a possibility of speedup: if the key is an integer, it should be possible to avoid expensive operations completely. I am well aware of the fact that the current code does not take advantage of this possibility.

It surprises me that conversion might be more consistent than checking equality. Are you sure about that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To summarize: would you think

R = self.parent()
in_dict = {m if isinstance(m, (Integer, int)) else R(m): v for m, v in in_dict.items()}

is better?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, that is good after a slight variant:

R = self.parent()
in_dict = {ZZ(m) if m in ZZ else R(m): v for m, v in in_dict.items()}

to catch things like {2/2: x+y}. My previous proposal, in principle, does allow for integers, but it would break over finite fields in a subtle way.

With regards to your other comments, things like {x/y: 1} would fail, but perhaps at a later stage when it compares with the generators. For what a user looks like a generator can be very broad. Consider the following example that generates a pushout:

sage: R.<x,t> = QQ[]
sage: S.<x> = ZZ['a'][]
sage: x + t
x + t
sage: _.parent()
Multivariate Polynomial Ring in x, t over Univariate Polynomial Ring in a over Rational Field
sage: S.gen() == R.gen(0)
True

Is the generator of S a generator of R and vice versa? The conversion does work in this case. However, what about the string 'x'?

To put it another way, I think each ring should be responsible for what it considers as input and is the best thing to decide that. Yes, some are imprecise/not-well-coded, but the specifications about what is an element (and hence might be a generator) is more clear.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately, this does not work:

sage -t --warn-long 81.0 --random-seed=222573162246150205167095728432903358574 src/sage/rings/fraction_field_element.pyx
**********************************************************************
File "src/sage/rings/fraction_field_element.pyx", line 374, in sage.rings.fraction_field_element.FractionFieldElement.__hash__
Failed example:
    ((x+1)/(x^2+1)).subs({x:1})
Exception raised:
    Traceback (most recent call last):
      File "/home/martin/sage-trac/src/sage/doctest/forker.py", line 712, in _run
        self.compile_and_execute(example, compiler, test.globs)
      File "/home/martin/sage-trac/src/sage/doctest/forker.py", line 1147, in compile_and_execute
        exec(compiled, globs)
      File "<doctest sage.rings.fraction_field_element.FractionFieldElement.__hash__[15]>", line 1, in <module>
        ((x+Integer(1))/(x**Integer(2)+Integer(1))).subs({x:Integer(1)})
      File "sage/rings/fraction_field_element.pyx", line 481, in sage.rings.fraction_field_element.FractionFieldElement.subs
        num = self._numerator.subs(in_dict, *args, **kwds)
      File "sage/rings/polynomial/multi_polynomial_libsingular.pyx", line 3559, in sage.rings.polynomial.multi_polynomial_libsingular.MPolynomial_libsingular.subs
        raise TypeError("keys do not match self's parent")
    TypeError: keys do not match self's parent
**********************************************************************
File "src/sage/rings/fraction_field_element.pyx", line 474, in sage.rings.fraction_field_element.FractionFieldElement.subs
Failed example:
    p.subs({v: 100})
Exception raised:
    Traceback (most recent call last):
      File "/home/martin/sage-trac/src/sage/doctest/forker.py", line 712, in _run
        self.compile_and_execute(example, compiler, test.globs)
      File "/home/martin/sage-trac/src/sage/doctest/forker.py", line 1147, in compile_and_execute
        exec(compiled, globs)
      File "<doctest sage.rings.fraction_field_element.FractionFieldElement.subs[7]>", line 1, in <module>
        p.subs({v: Integer(100)})
      File "sage/rings/fraction_field_element.pyx", line 481, in sage.rings.fraction_field_element.FractionFieldElement.subs
        num = self._numerator.subs(in_dict, *args, **kwds)
      File "sage/rings/polynomial/multi_polynomial_libsingular.pyx", line 3559, in sage.rings.polynomial.multi_polynomial_libsingular.MPolynomial_libsingular.subs
        raise TypeError("keys do not match self's parent")
    TypeError: keys do not match self's parent
**********************************************************************
2 items had failures:
   1 of  32 in sage.rings.fraction_field_element.FractionFieldElement.__hash__
   1 of   9 in sage.rings.fraction_field_element.FractionFieldElement.subs
    [295 tests, 2 failures, 0.72 s]
----------------------------------------------------------------------
sage -t --warn-long 81.0 --random-seed=222573162246150205167095728432903358574 src/sage/rings/fraction_field_element.pyx  # 2 doctests failed
----------------------------------------------------------------------

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am guessing that this is because the other instances of subs use equality, not conversion. But I don't know.

Copy link
Collaborator

Choose a reason for hiding this comment

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

No, they aren’t unless I am missed something. Sorry, I forgot where this was supposed to live; it should have been R = self.parent().base() as it should be a polynomial ring (more generally, the defining domain) generator.

Copy link

github-actions bot commented Feb 5, 2024

Documentation preview for this PR (built with commit 3bd05ab; changes) is ready! 🎉

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.

Thank you. LGTM. Sorry that this ended up being a bit more complicated.

At some point, we probably should also try to improve the subs for multivariate polynomials. It seems like we could some speed out...

vbraun pushed a commit to vbraun/sage that referenced this pull request Feb 7, 2024
sagemathgh-37143: special subs for fractions
    
We create a specialised method `subs` for `FractionFieldElements`, for
better performance.  In particular, the generic `Element.subs` method
insists on replacing all gens, which is a problem in polynomial rings
with many variables, such as (potentially) the `InfinitePolynomialRing`.

Fixes sagemath#37122.
    
URL: sagemath#37143
Reported by: Martin Rubey
Reviewer(s): Martin Rubey, Travis Scrimshaw
vbraun pushed a commit to vbraun/sage that referenced this pull request Feb 7, 2024
sagemathgh-37210: unify alias substitute for subs
    
We unify the definitions as follows:

* `substitute` is a method in `Element` which calls `self.subs` and
passes all arguments along, and not defined anywhere else.
* `subs` is the method that should be overwritten in the various element
classes.

Dependencies:

sagemath#37143
    
URL: sagemath#37210
Reported by: Martin Rubey
Reviewer(s): Frédéric Chapoton
vbraun pushed a commit to vbraun/sage that referenced this pull request Feb 11, 2024
sagemathgh-37210: unify alias substitute for subs
    
We unify the definitions as follows:

* `substitute` is a method in `Element` which calls `self.subs` and
passes all arguments along, and not defined anywhere else.
* `subs` is the method that should be overwritten in the various element
classes.

Dependencies:

sagemath#37143
    
URL: sagemath#37210
Reported by: Martin Rubey
Reviewer(s): Frédéric Chapoton
@vbraun vbraun merged commit dd9abe4 into sagemath:develop Feb 13, 2024
19 of 20 checks passed
@mkoeppe mkoeppe added this to the sage-10.3 milestone Mar 7, 2024
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.

performance bug in subs of fraction field
4 participants