Skip to content

Commit

Permalink
interpret numbers without decimal as integers
Browse files Browse the repository at this point in the history
  • Loading branch information
gutow committed Jul 9, 2023
1 parent 7a8cb2a commit b88ec74
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 5 deletions.
3 changes: 3 additions & 0 deletions algebra_with_sympy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
__docformat__ = "numpy"

from algebra_with_sympy.algebraic_equation import *

# Set config value for numerics before adjusting with the preparser
algwsym_config.numerics.integers_as_exact = False # adjusted in preparser.
from algebra_with_sympy.preparser import *

# Set the output formatting defaults
Expand Down
70 changes: 69 additions & 1 deletion algebra_with_sympy/algebraic_equation.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from sympy.core.basic import Basic
from sympy.core.evalf import EvalfMixin
from sympy.core.sympify import _sympify
from algebra_with_sympy.preparser import integers_as_exact
import functools
from sympy import *

Expand Down Expand Up @@ -124,6 +125,32 @@ def solve_to_list(self):
"""
return self.solve_to_list

class numerics():

def __init__(self):
"""This class holds settings for how numerical computation and
inputs are handled.
"""
pass

def integers_as_exact(self):
"""**This is a flag for informational purposes and interface
consistency. Changing the value will not change the behavior.**
To change the behavior call:
* `unset_integers_as_exact()` to turn this feature off.
* `set_integers_as_exact()` to turn this feature on (on by
default).
If set to `True` (the default) and if running in an
IPython/Jupyter environment any number input without a decimal
will be interpreted as a sympy integer. Thus, fractions and
related expressions will not evalute to floating point numbers,
but be maintained as exact expressions (e.g. 2/3 -> 2/3 not the
float 0.6666...).
"""
return self.integers_as_exact

def __latex_override__(expr, *arg):
from IPython import get_ipython
show_code = False
Expand Down Expand Up @@ -177,9 +204,50 @@ def __command_line_printing__(expr, *arg):
# " overriding plain text formatter = " + str(old))
else:
# command line
# print("Overiding command line printing of python.")
# print("Overriding command line printing of python.")
sys.displayhook = __command_line_printing__

# Numerics controls
def set_integers_as_exact():
"""This operation uses `sympy.interactive.session,int_to_Integer` which
causes any number input without a decimal to be interpreted as a sympy
integer to pre-parse input cells. It also sets the flag
`algwsym_config.numerics.integers_as_exact = True` This is the default
mode of algebra_with_sympy. To turn this off call
`unset_integers_as_exact()`.
"""
from IPython import get_ipython
if get_ipython():
get_ipython().input_transformers_post.append(integers_as_exact)
algwsym_config.numerics.integers_as_exact = True
return

def unset_integers_as_exact():
"""This operation disables forcing of numbers input without
decimals being interpreted as sympy integers. Numbers input without a
decimal may be interpreted as floating point if they are part of an
expression that undergoes python evaluation (e.g. 2/3 -> 0.6666...). It
also sets the flag `algwsym_config.numerics.integers_as_exact = False`
Call `set_integers_as_exact()` to avoid this conversion of rational
fractions and related expressions to floating point. Algebra_with_sympy
starts with `set_integers_as_exact()` enabled (
`algwsym_config.numerics.integers_as_exact = True`)
"""
from IPython import get_ipython
if get_ipython():
pre = get_ipython().input_transformers_post
# The below looks excessively complicated, but more reliably finds the
# transformer to remove across varying IPython environments.
for k in pre:
if "integers_as_exact" in k.__name__:
pre.remove(k)
algwsym_config.numerics.integers_as_exact = False
return

# Set up numerics behaviors
if ip:
set_integers_as_exact()

class Equation(Basic, EvalfMixin):
"""
This class defines an equation with a left-hand-side (tlhs) and a right-
Expand Down
19 changes: 19 additions & 0 deletions algebra_with_sympy/preparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,25 @@ def algebra_with_sympy_preparser(lines):
new_lines.append(k)
return new_lines


def integers_as_exact(lines):
"""This preparser uses `sympy.interactive.session.int_to_Integer` to
convert numbers without decimal points into sympy integers so that math
on them will be exact rather than defaulting to floating point. This
should not be called directly by the user. It is plugged into the
IPython preparsing sequence when the feature is requested. The default for
Algebra_with_sympy is to use this preparser. This can be turned on and
off with:
* `set_integers_as_exact()`
* `unset_integers_as_exact()`
"""
from sympy.interactive.session import int_to_Integer
string = ''
for k in lines:
string += k + '\n'
string = string[:-1] # remove the last '\n'
return int_to_Integer(string)

from IPython import get_ipython
if get_ipython():
if hasattr(get_ipython(),'input_transformers_cleanup'):
Expand Down
6 changes: 3 additions & 3 deletions dotests.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env bash

echo 'Core tests:'
pytest --ignore='Developer Testing' --ignore-glob='*test_preparser.py'
pytest --ignore='Developer Testing' --ignore-glob='*test_preparser.py' --ignore-glob='*test_numerics.py'
echo 'Doc tests:'
pytest --ignore='tests' --ignore='Developer Testing' --ignore-glob='*old*' --doctest-modules
echo 'Preparser tests:'
ipython -m pytest tests/test_preparser.py
echo 'Preparser and numerics tests (require ipython environment):'
ipython -m pytest tests/test_preparser.py tests/test_numerics.py
34 changes: 34 additions & 0 deletions tests/test_numerics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!ipython

from algebra_with_sympy.preparser import integers_as_exact
from algebra_with_sympy.algebraic_equation import set_integers_as_exact, \
unset_integers_as_exact, algwsym_config
from IPython import get_ipython
from pytest import raises

if not(get_ipython()):
raise EnvironmentError('This test module file must be run in an ipython '
'environment. Use `ipython -m pytest path-to-file`.'
' To avoid running this file in a general test '
'use `pytest --ignore-glob="*test_numerics.py"`')

def test_set_integers_as_exact():
set_integers_as_exact()
assert integers_as_exact in get_ipython().input_transformers_post
assert algwsym_config.numerics.integers_as_exact == True

def test_integers_as_exact():
lines = []
lines.append('1/2*x + 0.333*x')
lines.append('2/3*z + 2.0*y + ln(3*x)')
result = integers_as_exact(lines)
splitlines = result.split('\n')
expectedlines = ['Integer (1 )/Integer (2 )*x +0.333 *x ',
'Integer (2 )/Integer (3 )*z +2.0 *y +ln (Integer (3 )*x )']
for k in range(len(splitlines)):
assert splitlines[k] == expectedlines[k]

def test_unset_integers_as_exact():
unset_integers_as_exact()
assert algwsym_config.numerics.integers_as_exact == False
assert integers_as_exact not in get_ipython().input_transformers_post
12 changes: 12 additions & 0 deletions tests/test_preparser.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!ipython
from algebra_with_sympy.preparser import algebra_with_sympy_preparser as parser
from algebra_with_sympy.preparser import integers_as_exact
from IPython import get_ipython
from pytest import raises

Expand Down Expand Up @@ -48,3 +49,14 @@ def test_parsing_errors():
assert parser(lines) == expected_out
lines.append('eq1 =@ a + b > c/d\n')
raises(ValueError, lambda: parser(lines))

def test_integers_as_exact():
lines = []
lines.append('1/2*x + 0.333*x')
lines.append('2/3*z + 2.0*y + ln(3*x)')
result = integers_as_exact(lines)
splitlines = result.split('\n')
expectedlines = ['Integer (1 )/Integer (2 )*x +0.333 *x ',
'Integer (2 )/Integer (3 )*z +2.0 *y +ln (Integer (3 )*x )']
for k in range(len(splitlines)):
assert splitlines[k] == expectedlines[k]
2 changes: 1 addition & 1 deletion version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.11.0'
__version__ = '0.12.0.dev0'

0 comments on commit b88ec74

Please sign in to comment.