diff --git a/NEWS b/NEWS index 157eb0aa1..2edaccdf2 100644 --- a/NEWS +++ b/NEWS @@ -26,6 +26,12 @@ octsympy 2.5.1-dev using `sym(0.1, 'f')`. Passing `'r'` gives the current heuristic behaviour. Neither raises a warning. + * The default floating-point heuristics now check for square roots of + integers, so `sym(sqrt(2))` should work. + + * `sym('i')` and `sym('I')` no longer create the imaginary unit: use + `sym(i)` or `sym(1i)`. + * Bug fix: isequal(n) for symfun's correctly tests the argnames. diff --git a/inst/@sym/private/const_to_python_str.m b/inst/@sym/private/const_to_python_str.m deleted file mode 100644 index adf337c2b..000000000 --- a/inst/@sym/private/const_to_python_str.m +++ /dev/null @@ -1,96 +0,0 @@ -%% Copyright (C) 2015-2017 Colin B. Macdonald -%% Copyright (C) 2016 Lagu -%% -%% This file is part of OctSymPy. -%% -%% OctSymPy is free software; you can redistribute it and/or modify -%% it under the terms of the GNU General Public License as published -%% by the Free Software Foundation; either version 3 of the License, -%% or (at your option) any later version. -%% -%% This software is distributed in the hope that it will be useful, -%% but WITHOUT ANY WARRANTY; without even the implied warranty -%% of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See -%% the GNU General Public License for more details. -%% -%% You should have received a copy of the GNU General Public -%% License along with this software; see the file COPYING. -%% If not, see . - -%% -*- texinfo -*- -%% @deftypefun {[@var{s}, @var{flag}] =} const_to_python_str (@var{x}) -%% Recognize constant and return their python string equivalent. -%% -%% Private helper function. -%% -%% Caution: there are two copies of this file for technical -%% reasons: make sure you modify both of them! -%% -%% @seealso{sym, vpa} -%% @end deftypefun - -function [s, flag] = const_to_python_str (x) - - % Table of special doubles and special strings. Format for each row is: - % double value, {list of strings to recognize}, resulting python expr - % Note: case sensitive - % Note: python expr should be in list for identity "sym(sympy(x)) == x" - list = {pi {'pi'} 'S.Pi'; ... - inf {'inf' 'Inf' 'oo'} 'S.Infinity'; ... - nan {'NaN' 'nan'} 'S.NaN'; ... - i {'i' 'I'} 'S.ImaginaryUnit'}; - % Special Sympy constants to recognize - const = {'zoo'}; - - flag = 1; - - if (isa (x, 'double')) % Number comparison - for j = 1:length (list) - if (isequaln (x, list{j, 1})) - s = list{j, 3}; - return - elseif (isequaln (x, -list{j, 1})) - s = ['-' list{j, 3}]; - return - end - end - elseif (isa (x, 'char')) % Char comparison - for j = 1:length (list) - for n = 1:length (list{j, 2}) - if (strcmp (x, list{j, 2}{n}) || strcmp (x, ['+' list{j, 2}{n}])) - s = list{j, 3}; - return - elseif (strcmp (x, ['-' list{j, 2}{n}])) - s = ['-' list{j, 3}]; - return - end - end - end - for j = 1:length (const) % Check if is a python constant - if (strcmp (x, const{j}) || strcmp (x, ['+' const{j}])) - s = const{j}; - return - elseif (strcmp (x, ['-' const{j}])) - s = ['-' const{j}]; - return - end - end - else - error ('Format not supported.') - end - - if (isa (x, 'char')) - flag = 0; - s = x; - return - elseif (isa (x, 'double')) - if ((abs (x) < 1e15) && (mod (x,1) == 0)) - % special treatment for "small" integers - s = num2str (x); % better than sprintf('%d', large) - else - s = ''; - flag = 0; - end - end - -end diff --git a/inst/@sym/private/detect_special_str.m b/inst/@sym/private/detect_special_str.m new file mode 100644 index 000000000..189516fdb --- /dev/null +++ b/inst/@sym/private/detect_special_str.m @@ -0,0 +1,60 @@ +%% Copyright (C) 2015-2017 Colin B. Macdonald +%% Copyright (C) 2016 Lagu +%% +%% This file is part of OctSymPy. +%% +%% OctSymPy is free software; you can redistribute it and/or modify +%% it under the terms of the GNU General Public License as published +%% by the Free Software Foundation; either version 3 of the License, +%% or (at your option) any later version. +%% +%% This software is distributed in the hope that it will be useful, +%% but WITHOUT ANY WARRANTY; without even the implied warranty +%% of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +%% the GNU General Public License for more details. +%% +%% You should have received a copy of the GNU General Public +%% License along with this software; see the file COPYING. +%% If not, see . + +%% -*- texinfo -*- +%% @deftypefun {@var{s} =} detect_special_str (@var{x}) +%% Recognize special constants, return their sympy string equivalent. +%% +%% Private helper function. +%% +%% This function should return a string @var{s} which can be instantiated +%% directly in Python such as @code{['return' @var{s}]}. If no special +%% value is found, it returns the empty string. +%% +%% @seealso{sym, vpa} +%% @end deftypefun + +function s = detect_special_str (x) + + % Table of special strings. Format for each row is: + % {list of strings to recognize}, resulting python expr + % Note: case sensitive + % Note: python expr should be in list for identity "sym(sympy(x)) == x" + table = {{'pi'} 'S.Pi'; ... + {'inf' 'Inf' 'oo'} 'S.Infinity'; ... + {'NaN' 'nan'} 'S.NaN'; ... + {'zoo'} 'S.ComplexInfinity'}; + + s = ''; + + assert (ischar (x)) + + for j = 1:length (table) + for n = 1:length (table{j, 1}) + if (strcmp (x, table{j, 1}{n}) || strcmp (x, ['+' table{j, 1}{n}])) + s = table{j, 2}; + return + elseif (strcmp (x, ['-' table{j, 1}{n}])) + s = ['-' table{j, 2}]; + return + end + end + end + +end diff --git a/inst/private/magic_str_str.m b/inst/@sym/private/double_to_sym_exact.m similarity index 60% rename from inst/private/magic_str_str.m rename to inst/@sym/private/double_to_sym_exact.m index 82fd0ce1b..674b13527 100644 --- a/inst/private/magic_str_str.m +++ b/inst/@sym/private/double_to_sym_exact.m @@ -1,4 +1,4 @@ -%% Copyright (C) 2015 Colin B. Macdonald +%% Copyright (C) 2017 Colin B. Macdonald %% %% This file is part of OctSymPy. %% @@ -17,29 +17,22 @@ %% If not, see . %% -*- texinfo -*- -%% @deftypefun {@var{s} =} magic_str_str (@var{x}) -%% Recognize special string values and substitute others. +%% @deftypefun {@var{y} =} double_to_sym_exact (@var{x}) +%% Convert a double value to the equivalent rational sym %% -%% Private function. +%% Private helper function. %% -%% @seealso{sym, vpa} %% @end deftypefun -function s = magic_str_str(x, varargin) - - if (~ischar(x)) - error('OctSymPy:magic_str_str:notstring', ... - 'Expected a string'); - end - - if (strcmpi(x, 'inf')) || (strcmpi(x, '+inf')) - s = 'oo'; - elseif (strcmpi(x, '-inf')) - s = '-oo'; - elseif (strcmpi(x, 'i')) - s = 'I'; +function y = double_to_sym_exact (x) + if (isnan (x)) + y = python_cmd ('return S.NaN'); + elseif (isinf (x) && x < 0) + y = python_cmd ('return -S.Infinity'); + elseif (isinf (x)) + y = python_cmd ('return S.Infinity'); else - s = x; + %% Rational will exactly convert from a float + y = python_cmd ('return Rational(_ins[0])', x); end - end diff --git a/inst/@sym/private/double_to_sym_heuristic.m b/inst/@sym/private/double_to_sym_heuristic.m new file mode 100644 index 000000000..63168bda5 --- /dev/null +++ b/inst/@sym/private/double_to_sym_heuristic.m @@ -0,0 +1,64 @@ +%% Copyright (C) 2017 Colin B. Macdonald +%% +%% This file is part of OctSymPy. +%% +%% OctSymPy is free software; you can redistribute it and/or modify +%% it under the terms of the GNU General Public License as published +%% by the Free Software Foundation; either version 3 of the License, +%% or (at your option) any later version. +%% +%% This software is distributed in the hope that it will be useful, +%% but WITHOUT ANY WARRANTY; without even the implied warranty +%% of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +%% the GNU General Public License for more details. +%% +%% You should have received a copy of the GNU General Public +%% License along with this software; see the file COPYING. +%% If not, see . + +%% -*- texinfo -*- +%% @deftypefun {@var{y} =} double_to_sym_heuristic (@var{x}, @var{ratwarn}, @var{argnstr}) +%% Convert a double value to a nearby "nice" sym +%% +%% Private helper function. +%% +%% @end deftypefun + +function y = double_to_sym_heuristic (x, ratwarn, argnstr) + assert (isempty (argnstr)) + if (isnan (x)) + y = python_cmd ('return S.NaN'); + elseif (isinf (x) && x < 0) + y = python_cmd ('return -S.Infinity'); + elseif (isinf (x)) + y = python_cmd ('return S.Infinity'); + elseif (isequal (x, pi)) + %% Special case for pi + y = python_cmd ('return S.Pi'); + elseif (isequal (x, -pi)) + y = python_cmd ('return -S.Pi'); + elseif ((abs (x) < flintmax) && (mod (x, 1) == 0)) + y = python_cmd ('return Integer(_ins[0])', int64 (x)); + else + %% Find the nearest rational, rational*pi or sqrt(integer) + if (ratwarn) + warning('OctSymPy:sym:rationalapprox', ... + 'passing floating-point values to sym is dangerous, see "help sym"'); + end + [N1, D1] = rat (x); + [N2, D2] = rat (x / pi); + N3 = round (x^2); + err1 = abs (N1 / D1 - x); + err2 = abs ((N2*pi) / D2 - x); + err3 = abs (sqrt (N3) - x); + if (err1 <= err3) + if (err1 <= err2) + y = python_cmd ('return Rational(*_ins)', int64 (N1), int64 (D1)); + else + y = python_cmd ('return Rational(*_ins)*S.Pi', int64 (N2), int64 (D2)); + end + else + y = python_cmd ('return sqrt(Integer(*_ins))', int64 (N3)); + end + end +end diff --git a/inst/@sym/sym.m b/inst/@sym/sym.m index f8d0214c9..99862c1b1 100644 --- a/inst/@sym/sym.m +++ b/inst/@sym/sym.m @@ -107,9 +107,9 @@ %% @code{sym(1)/10} (and this is true for most decimal expressions). %% It is generally impossible to determine which exact symbolic value the %% user intended. -%% The warning indicates that some heuristics have been applied, -%% namely a preference for ``small'' fractions (and small fractions -%% of π). +%% The warning indicates that some heuristics have been applied +%% (namely a preference for ``small'' fractions, small fractions +%% of π and square roots of integers). %% Further examples include: %% @example %% @group @@ -277,7 +277,6 @@ end asm = {}; - check = true; isnumber = isnumeric (x) || islogical (x); ratwarn = true; ratflag = 'r'; @@ -320,80 +319,52 @@ s = numeric_array_to_sym (x); return - elseif (isa (x, 'double')) % Handle doubles - check = false; - - switch ratflag - case 'f' - % TODO: need something like const_to_python_str (x) for inf, nan, not pi - if (isreal (x)) - s = python_cmd ('return Rational(_ins[0])', x); - else - s = python_cmd ('return Rational(_ins[0]) + I*Rational(_ins[1])', ... - real (x), imag (x)); - end - return - - case 'r' - iscmplx = ~isreal (x); - if (iscmplx) - xx = {real(x); imag(x)}; - else - xx = {x}; - end - ss = cell(2, 1); - - for n = 1:numel (xx) - tmpx = xx{n}; - [ss{n}, flag] = const_to_python_str (tmpx); - if (~flag) - % Allow 1/3 and other "small" fractions. - % Personally, I like a warning here so I can catch bugs. - % Matlab SMT does this (w/o warning). - if (ratwarn) - warning('OctSymPy:sym:rationalapprox', ... - 'passing floating-point values to sym is dangerous, see "help sym"'); - end - [N1, D1] = rat (tmpx); - [N2, D2] = rat (tmpx / pi); - if (10*abs (D2) < abs (D1)) - % use frac*pi if demoninator significantly shorter - ss{n} = sprintf ('Rational(%s, %s)*pi', num2str (N2), num2str (D2)); - else - ss{n} = sprintf ('Rational(%s, %s)', num2str (N1), num2str (D1)); - end - else - ss{n} = sprintf ('S(%s)', ss{n}); - end - end - - if (iscmplx) - x = sprintf ('%s + I*(%s)', ss{1}, ss{2}); - else - x = ss{1}; - end - - otherwise - error ('sym: this case should not be possible') + elseif (isa (x, 'double')) % Handle double/complex + iscmplx = ~isreal (x); + if (iscmplx && isequal (x, 1i)) + s = python_cmd ('return S.ImaginaryUnit'); + return + elseif (iscmplx) + xx = {real(x); imag(x)}; + else + xx = {x}; + end + yy = cell(2, 1); + for n = 1:numel (xx) + x = xx{n}; + switch ratflag + case 'f' + y = double_to_sym_exact (x); + case 'r' + y = double_to_sym_heuristic (x, ratwarn, []); + otherwise + error ('sym: this case should not be possible') + end + yy{n} = y; end + if (iscmplx) + s = yy{1} + yy{2}*1i; + else + s = yy{1}; + end + return + + elseif (isinteger (x)) % Handle integer vealues + s = python_cmd ('return Integer(*_ins)', x); + return elseif (islogical (x)) % Handle logical values - check = false; if (x) - x = 'S.true'; + s = python_cmd ('return S.true'); else - x = 'S.false'; + s = python_cmd ('return S.false'); end - - elseif (isinteger (x)) % Handle integer vealues - check = false; - x = num2str (x, '%ld'); + return end if (isa (x, 'char')) - % We now have a char; need to decide whether to use S() or Symbol() on it. + %% Need to decide whether to use S() or Symbol() - if (check) % TODO: Warning if you try make a sym with the same name of a system function. %symsnotfunc (x); @@ -415,16 +386,17 @@ % end %end - [x, flag] = const_to_python_str (x); - if (flag) - check = false; - end - - isnum = ~isempty (regexp (x, '^[-+]*?\d*\.?\d*(e-?\d+)?$')); % Is Number + y = detect_special_str (x); + if (~ isempty (y)) + assert (isempty (asm), 'Only symbols can have assumptions.') + s = python_cmd (['return ' y]); + return end + isnum = ~isempty (regexp (x, '^[-+]*?\d*\.?\d*(e-?\d+)?$')); + %% Use Symbol() for words, not numbers, not "f(x)". - if (check && (~ isnum) && (~ isempty (regexp (x, '^\w+$')))) + if ((~ isnum) && (~ isempty (regexp (x, '^\w+$')))) cmd = { 'd = dict()' 'x = _ins[0]' @@ -464,12 +436,10 @@ assert (isempty (asm), 'Only symbols can have assumptions.') % TODO: figure version might warn on expression strings - %if (check) % Check if the user try to execute operations from sym %if (~isempty (regexp (xc, '\!|\&|\^|\:|\*|\/|\\|\+|\-|\>|\<|\=|\~'))) % warning ('Please avoid execute operations from sym function.'); %end - %end % Usually want rational output here (i.e., if input was "1.2"). % But if input has words and parentheses it might be raw Sympy code. @@ -804,6 +774,13 @@ %! assert (isequal (sym(inf, 'f'), sym(inf))) %! assert (isequal (sym(-inf, 'f'), sym(-inf))) %! assert (isequaln (sym(nan, 'f'), sym(nan))) +%! assert (isequal (sym(complex(inf, -inf), 'f'), sym(complex(inf, -inf)))) +%! assert (isequaln (sym(complex(nan, inf), 'f'), sym(complex(nan, inf)))) +%! assert (isequaln (sym(complex(-inf, nan), 'f'), sym(complex(-inf, nan)))) + +%!test +%! assert (isequal (sym (sqrt(2), 'r'), sqrt (sym (2)))) +%! assert (isequal (sym (sqrt(12345), 'r'), sqrt (sym (12345)))) %!test %! % symbols with special sympy names @@ -827,6 +804,7 @@ %!warning sym (-1e16); %!warning sym (10.33); %!warning sym (-5.23); +%!warning sym (sqrt (1.4142135623731)); %!error %! x = sym ('x', 'positive2'); diff --git a/inst/private/const_to_python_str.m b/inst/private/const_to_python_str.m deleted file mode 100644 index adf337c2b..000000000 --- a/inst/private/const_to_python_str.m +++ /dev/null @@ -1,96 +0,0 @@ -%% Copyright (C) 2015-2017 Colin B. Macdonald -%% Copyright (C) 2016 Lagu -%% -%% This file is part of OctSymPy. -%% -%% OctSymPy is free software; you can redistribute it and/or modify -%% it under the terms of the GNU General Public License as published -%% by the Free Software Foundation; either version 3 of the License, -%% or (at your option) any later version. -%% -%% This software is distributed in the hope that it will be useful, -%% but WITHOUT ANY WARRANTY; without even the implied warranty -%% of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See -%% the GNU General Public License for more details. -%% -%% You should have received a copy of the GNU General Public -%% License along with this software; see the file COPYING. -%% If not, see . - -%% -*- texinfo -*- -%% @deftypefun {[@var{s}, @var{flag}] =} const_to_python_str (@var{x}) -%% Recognize constant and return their python string equivalent. -%% -%% Private helper function. -%% -%% Caution: there are two copies of this file for technical -%% reasons: make sure you modify both of them! -%% -%% @seealso{sym, vpa} -%% @end deftypefun - -function [s, flag] = const_to_python_str (x) - - % Table of special doubles and special strings. Format for each row is: - % double value, {list of strings to recognize}, resulting python expr - % Note: case sensitive - % Note: python expr should be in list for identity "sym(sympy(x)) == x" - list = {pi {'pi'} 'S.Pi'; ... - inf {'inf' 'Inf' 'oo'} 'S.Infinity'; ... - nan {'NaN' 'nan'} 'S.NaN'; ... - i {'i' 'I'} 'S.ImaginaryUnit'}; - % Special Sympy constants to recognize - const = {'zoo'}; - - flag = 1; - - if (isa (x, 'double')) % Number comparison - for j = 1:length (list) - if (isequaln (x, list{j, 1})) - s = list{j, 3}; - return - elseif (isequaln (x, -list{j, 1})) - s = ['-' list{j, 3}]; - return - end - end - elseif (isa (x, 'char')) % Char comparison - for j = 1:length (list) - for n = 1:length (list{j, 2}) - if (strcmp (x, list{j, 2}{n}) || strcmp (x, ['+' list{j, 2}{n}])) - s = list{j, 3}; - return - elseif (strcmp (x, ['-' list{j, 2}{n}])) - s = ['-' list{j, 3}]; - return - end - end - end - for j = 1:length (const) % Check if is a python constant - if (strcmp (x, const{j}) || strcmp (x, ['+' const{j}])) - s = const{j}; - return - elseif (strcmp (x, ['-' const{j}])) - s = ['-' const{j}]; - return - end - end - else - error ('Format not supported.') - end - - if (isa (x, 'char')) - flag = 0; - s = x; - return - elseif (isa (x, 'double')) - if ((abs (x) < 1e15) && (mod (x,1) == 0)) - % special treatment for "small" integers - s = num2str (x); % better than sprintf('%d', large) - else - s = ''; - flag = 0; - end - end - -end diff --git a/inst/test_bugs.tst b/inst/test_bugs.tst index 2407ae154..26b4926ad 100644 --- a/inst/test_bugs.tst +++ b/inst/test_bugs.tst @@ -242,8 +242,3 @@ %! % FIXME: not same as an freshly created 5 x 6. %! C = sym('B', [5 6]); %! assert (isequal(B, C)) - -%!test -%! f1 = fileread ('private/const_to_python_str.m'); -%! f2 = fileread ('@sym/private/const_to_python_str.m'); -%! assert (strcmp (f1, f2)) diff --git a/inst/vpa.m b/inst/vpa.m index 35b31e412..ac2048b6d 100644 --- a/inst/vpa.m +++ b/inst/vpa.m @@ -62,8 +62,15 @@ 'x, n = _ins' 'return sympy.N(x, n),' }; r = python_cmd (cmd, x, n); - elseif (ischar(x)) - x = magic_str_str(x); + elseif (ischar (x)) + if (strcmp (x, 'inf') || strcmp (x, 'Inf') || strcmp (x, '+inf') || ... + strcmp (x, '+Inf')) + x = 'S.Infinity'; + elseif (strcmp (x, '-inf') || strcmp (x, '-Inf')) + x = '-S.Infinity'; + elseif (strcmp (x, 'I')) + x = 'Symbol("I")'; + end % Want Float if its '2.3' but N if its 'pi' cmd = { 'x, n = _ins' @@ -72,18 +79,24 @@ 'except ValueError:' ' return sympy.N(x, n),' }; r = python_cmd (cmd, x, n); - elseif (isfloat(x) && ~isreal (x)) - r = vpa(real(x), n) + sym('I')*vpa(imag(x), n); + elseif (isfloat (x) && ~isreal (x)) + r = vpa (real (x), n) + sym (1i)*vpa (imag (x), n); elseif (isfloat(x) && isscalar(x) == 1) - [s, flag] = const_to_python_str(x); - if (flag) - r = vpa(s, n); - else - cmd = { - 'x, n = _ins' - 'return sympy.Float(x, n),' }; - r = python_cmd (cmd, x, n); + if (isnan (x)) + x = 'S.NaN'; + elseif (isinf (x) && x < 0) + x = '-S.Infinity'; + elseif (isinf (x)) + x = 'S.Infinity'; + elseif (isequal (x, pi)) + x = 'S.Pi'; + elseif (isequal (x, -pi)) + x = '-S.Pi'; end + cmd = { + 'x, n = _ins' + 'return sympy.N(x, n)' }; + r = python_cmd (cmd, x, n); elseif (isinteger(x) && isscalar(x) == 1) cmd = { 'x, n = _ins' @@ -195,11 +208,14 @@ %! % can pass i directly to vpa %! a = vpa(sym(i)); %! b = vpa(i); -%! c = vpa('i'); -%! d = vpa('I'); -%! assert (isequal (a, b)) -%! assert (isequal (a, c)) -%! assert (isequal (a, d)) + +%!test +%! % 'i' and 'I' just make vars +%! a = vpa(sym(1i)); +%! b = vpa('i'); +%! c = vpa('I'); +%! assert (~isequal (a, b)) +%! assert (~isequal (a, c)) %!test %! % inf/-inf do not become symbol('inf')