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

Arithmetic refactor #766

Draft
wants to merge 25 commits into
base: master
Choose a base branch
from
Draft

Arithmetic refactor #766

wants to merge 25 commits into from

Conversation

mmatera
Copy link
Contributor

@mmatera mmatera commented Jan 27, 2023

Well, it took a lot of time, but I think this is now ready for revision. The idea is to move the code associate to the evaluation of arithmetic expressions, especially those expressions composed mainly of numbers and arithmetic operations, to a dedicated module in mathics.core.eval.

There are several motivations to do this. In the first place, the code in master is quite involved, and not very well documented. Also, implies conversions back and forward to SymPy. Moreover, the way in which sympy evals arithmetic expressions is not fully compatible with the expected behavior in WMA. Then, more conversions and hacking over the returned code were needed.
With the code in this branch, I hope the logic is a little bit clearer, and several useless conversions are avoided. Also, the issue with DirectedInfinity that prevents the application of some rules associated with Gudermannian and other complex functions should be fixed now.

}


# Here I used the convention of calling eval_* to functions that can produce a new expression, or None
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rocky, I leave this long comment here about the eval<-> apply semantics. Maybe we want to go over this again before merging 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 looked at this and there is information here that I a do not believe is elsewhere. I think it should be elsewhere but I don't know right now where that elsewhere is.

# Handled already
# "Gudermannian[Infinity]": "Pi/2",
"Gudermannian[DirectedInfinity[1]]": "Pi/2",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Notice that the rule requires to use DirectedInfinity[1] instead of Infinity. This is due to Infinity is evaluated to DirectedInfinity[1] before looking at the Gudermannian set of rules.

Copy link
Member

@rocky rocky Jan 28, 2023

Choose a reason for hiding this comment

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

Ok - Thanks for the explanation. I'd like to understand this more though. What is it that turns Infinity into DirectedInfinity[1] ? Is this SymPy doing this?

As you say below, WMA turns Infinity to DirectedInfinity[1]. I wonder if we should be doing the same thing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We should if we want to get the same result if we apply the same rules as written for WMA.

Copy link
Member

Choose a reason for hiding this comment

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

Ok. If this is simple for now to do, then let's do. Or we can write DirectedInfinity[1] in the rules and come back to this some other time. Your choice - assuming this is a simple fix.

@rocky
Copy link
Member

rocky commented Jan 28, 2023

Thanks for looking at and untangling this. With regards to:

Moreover, the way in which sympy evals arithmetic expressions is not fully compatible with the expected behavior in WMA.

I would like to understand this in more detail. Is this documented somewhere? (I am currently looking myself).

What exactly are the differences?

@rocky
Copy link
Member

rocky commented Jan 28, 2023

With respect to

conversions back and forward to SymPy.

I would like more details here too. Obviously we want to reduce this, but generally the ideal flow is convert into SymPy as much as possible and convert back to Mathics as late or as little as possible. In fact this idea is true for other things as well, like numpy arrays, constant literals that map into Python, etc.

See below for revision to this

if exp.is_zero:
return Integer1
if isinstance(exp, (Integer, Real, Rational)):
sign = eval_sign(base) or Expression(SymbolSign, base)
Copy link
Member

Choose a reason for hiding this comment

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

My Editor tells and flake8 tell me SymbolSign is not defined.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for catching this.

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 have to add more tests for these routines, but at this point, I need a little bit of feedback (not necessarily right now, but before continuing with this).

if isinstance(exp, (Integer, Real, Rational)):
return Integer1
if isinstance(exp, Complex):
return Expression(SymbolExp, exp.imag)
Copy link
Member

Choose a reason for hiding this comment

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

My editor and flake8 tell me that SymbolExp is not defined.

Copy link
Contributor Author

@mmatera mmatera Jan 28, 2023

Choose a reason for hiding this comment

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

With respect to

conversions back and forward to SymPy.

I would like more details here too. Obviously we want to reduce this, but generally the ideal flow is convert into SymPy as much as possible and convert back to Mathics as late or as little as possible. In fact this idea is true for other things as well, like numpy arrays, constant literals that map into Python, etc.

Suppose you are going to evaluate

3^(1/2) +  5 * 3^(-1/2) + 3 * Pi^2 

1/2 is converted to SymPy twice back and forward, to see that the expression didn't change, so 1/2 is kept. Then we have the evaluation of 3^(-1/2) and 3^(-1/2) where 3 and 1/2, and 3^(1/2) are converted again back and forward, to again get the same result. And again in the multiplication by 5, and again in the addition, to get at the end the very same expression.

Copy link
Member

@rocky rocky Jan 28, 2023

Choose a reason for hiding this comment

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

All of this can be addressed by having a special form of Expression that is known to have a SymPy equivalent and that is saved. So 3, 1/2, 3^(1/2) are all Expressions that have SymPy expr object equivalents.

We will probably do the same for literals that have exact Python correspondences, and numerous other things.

If this is the only thing that being addressed, please don't do it this way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The problem is that for certain expressions, the replacement rules in WMA and SymPy are different, which makes that new rules written in WL fails to be applied, like in the case of DirectedInfinity. Actually, I did all of this because it was not possible to fix these rules without disentangling all the arithmetic module.

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 here we need more knowledgeable conversion routines between the Mathics and SymPy representations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Guder

That part of the PR are in this branch: https://github.com/Mathics3/mathics-core/tree/tests_from_arithmetic_refactor

However, the new tests do not pass without the other changes.

Copy link
Member

@rocky rocky Jan 28, 2023

Choose a reason for hiding this comment

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

A number of them do, so those could go in. The ones that don't can be commented out to remind us to address various issues.

Copy link
Member

Choose a reason for hiding this comment

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

@mmatera if you would like me to make a stab and extracting the non-controversial parts, I'll do.

RealOne = Real(1.0)


# Here we store numerical constants with their approximate
Copy link
Member

@rocky rocky Jan 28, 2023

Choose a reason for hiding this comment

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

This smells like a wrong thing to do. I see it is used in test_arithmetic_expr and that is new too, so that is probably not advisable either. What is the history behind these?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In a couple of places in the code, I need to multiply by that constant. The same with RationalMOneHalf. It is just to about avoiding calling the constructor to get the instance.

Copy link
Member

Choose a reason for hiding this comment

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

I didn't mean RealOne, I was referring to SymolE, SymbolEulerGamma, and SymbolPi.

Copy link
Member

Choose a reason for hiding this comment

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

BTW Computing RealOne once and using it many times is fine. And if we find that other modules tend to need RealOne, we can put it in a more global place.

@mmatera
Copy link
Contributor Author

mmatera commented Jan 28, 2023

Thanks for looking untangling this. With regards to:

Moreover, the way in which sympy evals arithmetic expressions is not fully compatible with the expected behavior in WMA.

I would like to understand this in more detail. Is this documented somewhere? (I am currently looking myself).

What exactly are the differences?

The first difference that I noticed is that SymPy rationalizes fractions. For example, (1/3)^(1/2) goes to Sqrt[3]/3, but in WMA, the result is 1/Sqrt[3]. Also, there are other conversions, like what happens with Complex numbers, which in SymPy are expressions, not atoms, and how Infinity quantities are handled. In WMA, Infinity and ComplexInfinity are evaluated as DirectedInfinity[1] and DirectedInfinity[].

return result


def distribute_factor(expr: BaseElement, factor: BaseElement) -> BaseElement:
Copy link
Member

Choose a reason for hiding this comment

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

I am not seeing distribute_factor or distribute_powers used anywhere. Where is this used?

@rocky
Copy link
Member

rocky commented Jan 28, 2023

Thanks for looking untangling this. With regards to:

Moreover, the way in which sympy evals arithmetic expressions is not fully compatible with the expected behavior in WMA.

I would like to understand this in more detail. Is this documented somewhere? (I am currently looking myself).
What exactly are the differences?

The first difference that I noticed is that SymPy rationalizes fractions. For example, (1/3)^(1/2) goes to Sqrt[3]/3, but in WMA, the result is 1/Sqrt[3]. Also, there are other conversions, like what happens with Complex numbers, which in SymPy are expressions, not atoms, and how Infinity quantities are handled. In WMA, Infinity and ComplexInfinity are evaluated as DirectedInfinity[1] and DirectedInfinity[].

This was said above but I think it applies here as well. The real issue is about whether there is loss of information in either representation. If not, then I would first try to get this by doing a better conversion. In a sense this is also related to the Form stuff as well.

It may turn out that this is too tricky, but I strongly encourage starting out with that first (and don't ask me for the reasons why unless you are prepared for the consequences of the answer).

Also, please note that in a Expression that saves the sympy representation, there is no conversion smarts per se. The system has its representation in its nice form and the Sympy expression has its representation in its nice form whether or not we had smarts to know that this is how the two correspond.

@rocky
Copy link
Member

rocky commented Jan 28, 2023

With respect to

conversions back and forward to SymPy.

I would like more details here too. Obviously we want to reduce this, but generally the ideal flow is convert into SymPy as much as possible and convert back to Mathics as late or as little as possible. In fact this idea is true for other things as well, like numpy arrays, constant literals that map into Python, etc.

So based on what has followed, I have to back off here, but only a little. You know how we have this overly complex caching mechanism for Expressions? Well, we might have something like that for Expressions with sympy representations. However the invalidation/update is done only when a rewrite rule kicks in.

@mmatera
Copy link
Contributor Author

mmatera commented Jan 28, 2023

Let me put this in a different way. I am pretty sure that your idea of implementing specialized expressions that stores the SymPy representation would provide a better implementation over what I have write here. However, even with that implementation, I think that it would be better to split the evaluation of arithmetic expressions in a part that could depend on the context (provided by the Evaluation object) and a part that is independent of the context. This is what this or is about. Then you could replace part of the implementation with proper calls to SymPy, and use a smarter class hierarchy, but still I think we want to have a way to do arithmetic without passing throw the standard evaluation process.

@rocky
Copy link
Member

rocky commented Jan 28, 2023

I think that it would be better to split the evaluation of arithmetic expressions in a part that could depend on the context (provided by the Evaluation object) and a part that is independent of the context.

I totally agree. And then this PR can be much much smaller and simpler. Please keep the new tests too - I like those as well!

The other change for now that makes sense is changing the rule with Gudermannian[Infinity] to Gudermannian[DirectedInfinity[1]] and removing the comment above it.

Doing both of these better is something we should keep in mind and do when the opportunity presents itself.

mmatera added a commit that referenced this pull request Jan 28, 2023
This would be the simplest part of #766, including tests and changes in
`Infinity` and `Precision`. Some of the new tests are commented / marked
as xfail because require the other changes.
mmatera added a commit that referenced this pull request Jan 30, 2023
…ts. (#769)

This PR is another fix coming from issues found when I was working on
#766. In particular, the way in which Mathics handles Precision and
Accuracy for real numbers with 0. nominal value. Also, a bug that
prevents parsing Real numbers with fixed accuracy was fixed.
@mmatera
Copy link
Contributor Author

mmatera commented Mar 5, 2023

@rocky, I was trying to merge this with the current state of master, but I found a lot of conflicts. My plan is to take the tests added here, and move the changes to another new branch, and then, take the part of the code needed to pass the tests. It will take a while.

@mmatera
Copy link
Contributor Author

mmatera commented Mar 6, 2023

I completed the rebase, but for some reason, now the performance is lower than in master. Also, I found some extra bugs in non directly related code. I am going to mark them, then propose fixing for them, and then continue over this.

@rocky
Copy link
Member

rocky commented Mar 6, 2023

I completed the rebase, but for some reason, now the performance is lower than in master. Also, I found some extra bugs in non directly related code. I am going to mark them, then propose fixing for them, and then continue over this.

Small PRs are always appreciated over large ones like this.

mmatera added a commit that referenced this pull request Mar 6, 2023
This came up during the #766 rebase: `PrecisionForm` didn't have a
`Builtin` symbol, and the `Makeboxes` rule didn't take into account the
`form` parameter. This PR fixes these two issues.
@mmatera
Copy link
Contributor Author

mmatera commented Mar 6, 2023

@rocky, yes, I am slowly splitting this up.

@mmatera mmatera mentioned this pull request Mar 6, 2023
mmatera added a commit that referenced this pull request Mar 12, 2023
This is based on one part of #766 related to how DirectedInfinity is
handled. To simplify this, I remove part of the `eval_Times` code
handling this kind of expression and replace it with upvalues rules.
Also, some of the new pytests in #766 were included here.

---------

Co-authored-by: rocky <rb@dustyfeet.com>
Co-authored-by: R. Bernstein <rocky@users.noreply.github.com>
@mmatera mmatera changed the base branch from master to arithmetic_test_zero May 26, 2023 18:18
Base automatically changed from arithmetic_test_zero to tidy_up_stuff May 27, 2023 06:38
Base automatically changed from tidy_up_stuff to master May 27, 2023 11:59
mmatera and others added 2 commits July 20, 2023 22:18
This PR implements two new builtins. Also, its low-level implementation
is used to simplify some of the low-level eval functions in arithmetic
and testing expressions.

---------

Co-authored-by: rocky <rb@dustyfeet.com>
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.

None yet

2 participants