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

core/containers: no longer create Tuple's with lists as arguments. #22551

Merged
merged 20 commits into from Dec 2, 2021

Conversation

ThePauliPrinciple
Copy link
Contributor

@ThePauliPrinciple ThePauliPrinciple commented Nov 26, 2021

References to other Issues or PRs

Fixes #22550

Brief description of what is fixed or changed

Several classes in SymPy use sympify on their arguments. In some cases, this leads to the creation of Tuple objects that have lists as their arguments. This PR solves those cases as far as they show up in the tests, by explicitly looping over iterables when a mix of tuple and list can be used for the input arguments.

Other comments

Release Notes

NO ENTRY

@sympy-bot
Copy link

sympy-bot commented Nov 26, 2021

Hi, I am the SymPy bot (v162). I'm here to help you write a release notes entry. Please read the guide on how to write release notes.

  • No release notes entry will be added for this pull request.
Click here to see the pull request description that was parsed.
<!-- Your title above should be a short description of what
was changed. Do not include the issue number in the title. -->

#### References to other Issues or PRs
<!-- If this pull request fixes an issue, write "Fixes #NNNN" in that exact
format, e.g. "Fixes #1234" (see
https://tinyurl.com/auto-closing for more information). Also, please
write a comment on that issue linking back to this pull request once it is
open. -->
Fixes #22550

#### Brief description of what is fixed or changed
Several classes in SymPy use `sympify` on their arguments. In some cases, this leads to the creation of `Tuple` objects that have `lists` as their arguments. This PR solves those cases as far as they show up in the tests, by explicitly looping over iterables when a mix of `tuple` and `list` can be used for the input arguments.

#### Other comments


#### Release Notes

<!-- Write the release notes for this release below between the BEGIN and END
statements. The basic format is a bulleted list with the name of the subpackage
and the release note for this PR. For example:

* solvers
  * Added a new solver for logarithmic equations.

* functions
  * Fixed a bug with log of integers.

or if no release note(s) should be included use:

NO ENTRY

See https://github.com/sympy/sympy/wiki/Writing-Release-Notes for more
information on how to write release notes. The bot will check your release
notes automatically to see if they are formatted correctly. -->

<!-- BEGIN RELEASE NOTES -->
NO ENTRY
<!-- END RELEASE NOTES -->

@@ -50,6 +50,12 @@ class Tuple(Basic):
def __new__(cls, *args, **kwargs):
if kwargs.get('sympify', True):
args = (sympify(arg) for arg in args)
#if kwargs.get('denest_lists', False):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is to be removed before merging, but shows that currently there are no longer any Tuple with list args being created during the testing.

@@ -1270,19 +1270,20 @@ def __new__(cls, expr, *variables, **kwargs):
must be supplied to differentiate %s''' % expr))

# Standardize the variables by sympifying them:
variables = list(sympify(variables))
#variables = sympify(list(variables))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is actually not needed here since all variables are eventually stored in Tuple (or Array which again stores it in Tuple) where sympify is also used.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe this should just be deleted.

@@ -212,7 +212,8 @@ def test_containers():
assert julia_code(Tuple(*[1, 2, 3])) == "(1, 2, 3)"
assert julia_code((1, x*y, (3, x**2))) == "(1, x.*y, (3, x.^2))"
# scalar, matrix, empty matrix and empty list
assert julia_code((1, eye(3), Matrix(0, 0, []), [])) == "(1, [1 0 0;\n0 1 0;\n0 0 1], zeros(0, 0), Any[])"
from sympy.codegen.pynodes import List
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should there be sympy/codegen/{julia,maple,octave}nodes.py files defining List or perhaps this means that List should be moved to a more general file in sympy/codegen. Obviously the import should be made top-level but I put it here for convenience of review.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure that List has the same meaning in all languages or if this should be considered an abstract "list" that would have different implementations in different languages.

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 each code printer has its own way to print a list, but since sympify is used on the input to CodePrinters, this line causes the creation of a Tuple, which then has a list in it. Changing this to a List seems to avoid this problem, I'm not sure there is a better way. List is actually just a wrapper around Tuple which gets printed differently (if I understand correctly, it defaults to printing the same as list.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think you know more about this than I do so I can't really advise but I trust your judgement.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bjodah do you have any preference for how this is dealt with?

Copy link
Member

Choose a reason for hiding this comment

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

I'm fine with moving it. I put it in pynodes to signal equivalence with Python lists.

@@ -584,8 +584,10 @@ def test_cse_list():
assert _cse(x) == ([], x)
assert _cse('x') == ([], 'x')
it = [x]
for c in (list, tuple, set, Tuple):
for c in (list, tuple, set):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tuple doesn't do typecasting, the equivalent of tuple(x) is Tuple(*x).

@github-actions
Copy link

github-actions bot commented Nov 29, 2021

Benchmark results from GitHub Actions

Lower numbers are good, higher numbers are bad. A ratio less than 1
means a speed up and greater than 1 means a slowdown. Green lines
beginning with + are slowdowns (the PR is slower then master or
master is slower than the previous release). Red lines beginning
with - are speedups.

Significantly changed benchmark results (PR vs master)

Significantly changed benchmark results (master vs previous release)

       before           after         ratio
     [907895ac]       [4489b4c7]
-      4.23±0.03s         323±20ms     0.08  polygon.PolygonArbitraryPoint.time_bench01
+      3.45±0.2ms       5.78±0.3ms     1.67  solve.TimeMatrixOperations.time_det(4, 2)
+      3.22±0.2ms      6.02±0.06ms     1.87  solve.TimeMatrixOperations.time_det_bareiss(4, 2)
+        37.5±1ms         56.5±2ms     1.51  solve.TimeMatrixSolvePyDySlow.time_linsolve(1)
+      38.4±0.9ms         57.7±3ms     1.50  solve.TimeMatrixSolvePyDySlow.time_solve(1)

Full benchmark results can be found as artifacts in GitHub Actions
(click on checks at the top of the PR).

@@ -50,6 +50,12 @@ class Tuple(Basic):
def __new__(cls, *args, **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably this should use an explicit keyword argument:

def __new__(cls, *args, sympify=False):

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would it be confusing if this gets changed in this PR? Since it solves a different issue. Note also that the current default value is sympify=True.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Turns out that this is not trival. Dict assumes Tuple uses sympify and not _sympify (to convert str to Symbols).

So if you use sympify as a keyword argument, you don't have access to it anymore

args = tuple(args)
for arg in args:
if isinstance(arg, list):
warnings.warn(DeprecationWarning('Tuple',arg))
Copy link
Contributor

Choose a reason for hiding this comment

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

Deprecation warnings in SymPy are usually given like this:

sympy/sympy/core/sympify.py

Lines 453 to 460 in ed0bfd1

SymPyDeprecationWarning(
feature="String fallback in sympify",
useinstead= \
'sympify(str(obj)) or ' + \
'sympy.core.sympify.converter or obj._sympy_',
issue=18066,
deprecated_since_version='1.6'
).warn()

The SymPyDeprecationWarning class is handled specially by the test runner so that it is an error if it is emitted in the test suite.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As far as I am concerned, this warning will be removed before the merge (see also the first review comment by me). If such a warning is to be added, it should be added to Basic when all such issues are resolved. This is for demonstration purposes. (that there are no longer such violations)

@ThePauliPrinciple ThePauliPrinciple changed the title core/containers: raising error when lists are used as arguments in Tuple core/containers: no longer create Tuple's with lists as arguments. Nov 30, 2021
@sympy-bot
Copy link

sympy-bot commented Nov 30, 2021

🟠

Hi, I am the SymPy bot (v162). I've noticed that some of your commits add or delete files. Since this is sometimes done unintentionally, I wanted to alert you about it.

This is an experimental feature of SymPy Bot. If you have any feedback on it, please comment at sympy/sympy-bot#75.

The following commits add new files:

  • 2e98db8:
    • sympy/codegen/abstract_nodes.py

If these files were added/deleted on purpose, you can ignore this message.

@oscarbenjamin
Copy link
Contributor

Is this finished now?

@ThePauliPrinciple
Copy link
Contributor Author

@bjodah Do you have an opinion about the new location of List? I also noticed that CodePrinter actually uses Lists (regardless of language), but only after creating Tuples first.

I think it is possible to have sympify use extra rules, so it might be better to have CodePrinter already convert lists to Lists inside sympify so that the current tests do not have to be changed.

Is abstract_nodes good name for the new file? Or would container_nodes be better?

Copy link
Member

@bjodah bjodah left a comment

Choose a reason for hiding this comment

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

I'm fine with putting the class in .abstract_nodes.

@@ -212,7 +213,7 @@ def test_containers():
assert julia_code(Tuple(*[1, 2, 3])) == "(1, 2, 3)"
assert julia_code((1, x*y, (3, x**2))) == "(1, x.*y, (3, x.^2))"
# scalar, matrix, empty matrix and empty list
assert julia_code((1, eye(3), Matrix(0, 0, []), [])) == "(1, [1 0 0;\n0 1 0;\n0 0 1], zeros(0, 0), Any[])"
Copy link
Member

Choose a reason for hiding this comment

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

this looks like it introduces backwards incompatibility? If so, I'm afraid it needs a deprecation cycle.

@@ -233,8 +234,7 @@ def test_containers():
assert maple_code(Tuple(*[1, 2, 3])) == "[1, 2, 3]"
assert maple_code((1, x * y, (3, x ** 2))) == "[1, x*y, [3, x^2]]"
# scalar, matrix, empty matrix and empty list

assert maple_code((1, eye(3), Matrix(0, 0, []), [])) == \
assert maple_code((1, eye(3), Matrix(0, 0, []), List(*[]))) == \
Copy link
Member

Choose a reason for hiding this comment

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

same thing here and for the octave test too.

#for codeprinting, all lists should be converted to abstract_nodes.Lists.
from sympy.codegen.abstract_nodes import List
store_list_converter=converter.get(list, None)
converter[list]=lambda x: List(*x)
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'm adding list->List conversion to sympify for the purposes of CodePrinting, this will allow current tuple/list nesting without creating lists in Tuples but not change anything for the user. Is there a better way to do this?

Copy link
Member

Choose a reason for hiding this comment

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

This kind of coding is fragile in my humble opinion: importing a mutable object, modifying it in-place, calling a function using it, and then reverting the change. If the converter in .sympify is intended to be modifiable, then I'd rather see a context manager class in .sympify allowing to override the default converters for a region of code, would you agree?

Copy link
Contributor

Choose a reason for hiding this comment

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

This is also not thread-safe. The converters are only really supposed to be registered during start up.

Probably somewhere there should explicit code that checks for list and converts to List rather than depending on sympify to do this implicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Point taken, I'll try to see of sympy.strategies can be of any help.

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 this has been solved.

del converter[list]
else:
converter[list] = store_list_converter

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Restore sympify to normal behaviour. Does it make sense to add this as an option to symplify?

expr = _handle_assign_to(expr, assign_to)
expr = _convert_python_lists(expr)
Copy link
Member

@bjodah bjodah Dec 2, 2021

Choose a reason for hiding this comment

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

This is where the explicit conversion was done previously. (Note that this was added only for backwards compatibility, I would have rather done without it, but it was already being called with [] as can be found in the tests).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It needs to happen before _handle_assign_to (since it will gladly convert ([],) to Tuple([]) in sympify which should not be allowed).

Copy link
Member

Choose a reason for hiding this comment

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

can we simply move the call up?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently _convert_python_lists only goes through lists (so it would stop as soon as it sees the tuple). I could try to change it to loop through tuple too.

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 updated _convert_python_lists to recurse through tuple and moved it forward. I believe this solves all issues.

@bjodah
Copy link
Member

bjodah commented Dec 2, 2021

Thanks! The changes regarding List now looks good to me 🙂👍


class List(Tuple):
"""Represents a (frozen) (Python) list (for code printing purposes)."""
def __eq__(self, other):
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the __eq__ method needed for this class to function or is it just for the tests?

In general it's better if Basic subclass don't override __eq__.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

from sympy import *
class List(Tuple):
    pass
print([1] == List(1))

class List(Tuple):
    """Represents a (frozen) (Python) list (for code printing purposes)."""
    def __eq__(self, other):
        if isinstance(other, list):
            return self == List(*other)
        else:
            return self.args == other
print([1] == List(1))

Shows that the first case is False while the second case is True

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, but why does that matter? Does the List class actually need to be compared (with ==) to the list class in order to function properly in actual use?

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'm not sure, that would have to be tested. Note that I only moved this class. It already had this functionality, so I assume there was a reason (perhaps a bit naive). It was previously in pynodes.py, which should indicate that this is specific to python printers, which it actually is not since it was already being used for all CodePrinters. The 'bug' related to this class was actually that it had to be used earlier in CodePrinter.doprint to avoid converting Tuples with lists as arguments.

Copy link
Contributor

Choose a reason for hiding this comment

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

Note that I only moved this class

Oh, okay. Let's just leave it for now then.

Copy link
Member

@bjodah bjodah Dec 3, 2021

Choose a reason for hiding this comment

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

I think we can remove the __eq__ method and the test if we want. I didn't reflect over implications of overriding __eq__ in a Basic subclass.

@oscarbenjamin
Copy link
Contributor

Looks good. Thanks!

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.

Tuple using sympify instead of _sympify incorrectly supports lists
4 participants