diff --git a/core/src/function_tree/symbols/number.cpp b/core/src/function_tree/symbols/number.cpp index 2f00a2f0d..29404f5da 100644 --- a/core/src/function_tree/symbols/number.cpp +++ b/core/src/function_tree/symbols/number.cpp @@ -61,11 +61,15 @@ void Integer::print(std::ostream & target) const void Complex::print(std::ostream & target) const { // real-valued floats print bare; the complex pair form is reserved for - // genuinely complex values + // genuinely complex values. str(0) prints EVERY digit the stored value + // carries: streaming the number directly would use the ostream's default + // precision (6 significant digits), silently truncating every printed + // system -- a coefficient must round-trip exactly through classic input. if (highest_precision_value_.imag() == 0) - target << highest_precision_value_.real(); + target << highest_precision_value_.real().str(0); else - target << highest_precision_value_; + target << "(" << highest_precision_value_.real().str(0) << "," + << highest_precision_value_.imag().str(0) << ")"; } diff --git a/python/bertini/parse/__init__.py b/python/bertini/parse/__init__.py index 9b6c6b89e..1c4ac5576 100644 --- a/python/bertini/parse/__init__.py +++ b/python/bertini/parse/__init__.py @@ -34,9 +34,52 @@ Parsing functions, taking strings and producing various other things """ +import re as _re from bertini._pybertini import parse as _pybparse from bertini._pybertini.parse import * +# The native SystemParser wants JUST the input body (variable groups + functions); it +# rejects a leading CONFIG...END; block and does not want the INPUT/END; separators that +# `System.to_classic_input()` itself emits -- so `parse.system(a_system.to_classic_input())` +# fails and callers were forced to hand-split the text. Wrap it to tolerate both, so the +# round-trip just works and no caller has to know the parser's quirks. + +_native_system = _pybparse.system + +# to_classic_input() writes complex coefficients as ``(re,im)``, but the FunctionParser +# only accepts ``(re+im*I)`` -- so a complex-coefficient system's own text does not parse +# back. Rewrite ``(re,im)`` -> ``(re+im*I)`` (two numeric tokens in parens) so it does. +_NUM = r'[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?' +_COMPLEX_LITERAL = _re.compile(r'\((' + _NUM + r'),(' + _NUM + r')\)') + + +def _fix_complex_literals(text): + return _COMPLEX_LITERAL.sub(r'(\1+\2*I)', text) + + +def _input_body(text): + """The parseable body of a classic Bertini input: drop a leading CONFIG...END; block + and, if the remainder is wrapped in INPUT...END;, return just the inside. A raw body + (no CONFIG, no INPUT wrapper) is returned unchanged, so this never breaks input the + native parser already accepted.""" + s = text + m = _re.search(r'\bCONFIG\b.*?\bEND\s*;', s, flags=_re.IGNORECASE | _re.DOTALL) + if m: + s = s[:m.start()] + s[m.end():] + m = _re.search(r'\bINPUT\b(.*?)\bEND\s*;', s, flags=_re.IGNORECASE | _re.DOTALL) + if m: + return m.group(1).strip() + return s.strip() + + +def system(text): + """Parse a classic Bertini system from ``text``, tolerating a leading CONFIG section, + INPUT/END; separators, and ``(re,im)`` complex-coefficient literals -- so + ``parse.system(sys.to_classic_input())`` round-trips for any system, real or complex. + """ + return _native_system(_fix_complex_literals(_input_body(text))) + + __all__ = dir(_pybparse) diff --git a/python/test/classes/classic_writer_test.py b/python/test/classes/classic_writer_test.py index a251a9c6e..0b5aa30f2 100644 --- a/python/test/classes/classic_writer_test.py +++ b/python/test/classes/classic_writer_test.py @@ -37,3 +37,32 @@ def test_mptype_and_predictor_are_selectable(): assert 'mptype: 0;' in _circle_line().to_classic_input(mptype=0) # double assert 'odepredictor: 0;' in _circle_line().to_classic_input(odepredictor=0) assert 'odepredictor: 5;' in _circle_line().to_classic_input() # RKF45 default + + +def test_float_coefficients_print_full_precision(): + """A coefficient must round-trip EXACTLY through classic input: streaming at the + ostream default (6 significant digits) silently truncated every printed system + (found via a cellcap bundle whose crit-curve system was a 1e-6 impostor).""" + import bertini + bertini.default_precision(30) + third = bertini.multiprec.real_mp(1) / bertini.multiprec.real_mp(3) + x, = bertini.variables(['x']) + s = bertini.System() + s.add_variable_group([x]) + s.add_function(x - bertini.coefficient(third)) + txt = s.to_classic_input() + assert '0.333333333333333333333333333' in txt, txt # full digits, not 0.333333 + import numpy as np + s2 = bertini.parse.system(txt) + # the reparsed coefficient is the IDENTICAL binary value + val = s2.eval(np.array([bertini.multiprec.complex_mp(third)])) + assert float(abs(complex(np.asarray(val).ravel()[0]))) == 0.0 + + # complex coefficients: both components at full precision through the pair form + c = bertini.multiprec.complex_mp(third, -third) + s3 = bertini.System() + s3.add_variable_group([x]) + s3.add_function(x - bertini.coefficient(c)) + s4 = bertini.parse.system(s3.to_classic_input()) + val = s4.eval(np.array([c])) + assert float(abs(complex(np.asarray(val).ravel()[0]))) == 0.0 diff --git a/python/test/classes/parser_test.py b/python/test/classes/parser_test.py index f50b22f89..ab95ff440 100644 --- a/python/test/classes/parser_test.py +++ b/python/test/classes/parser_test.py @@ -78,6 +78,53 @@ def test_parse_emoji_variable(): assert abs(sys.eval(vals)[0] - (-0.5)) < 1e-12 +def test_parse_tolerates_config_and_input_wrapper(): + # Regression: the native SystemParser wanted just the body; a leading CONFIG...END; + # block or the INPUT/END; separators (which System.to_classic_input EMITS) made it + # fail, so parse.system(sys.to_classic_input()) did not round-trip and callers had to + # hand-split. parse.system now tolerates both forms; a raw body still parses. + body = 'variable_group x, y, z; function f; f = x^2 + y^2 + z^2 - 1;' + wrapped = 'INPUT\n' + body + '\nEND;' + with_config = 'CONFIG\ntracktype: 0;\nmptype: 2;\nEND;\n\nINPUT\n' + body + '\nEND;' + for text in (body, wrapped, with_config): + sys = pp.system(text) + assert sys.num_variables() == 3 + assert len(list(sys.functions())) == 1 + + +def test_parse_complex_coefficient_literals(): + # Regression: to_classic_input writes complex coefficients as (re,im), which the + # FunctionParser could not read (it wants (re+im*I)) -- so a complex-coefficient + # system's own text did not round-trip. parse.system now rewrites (re,im) -> (re+im*I). + s = System() + x, y, z = variables(list('xyz')) + s.add_variable_group([x, y, z]) + c1 = coefficient(complex_mp('0.3', '-0.5')) + c2 = coefficient(complex_mp('1.2', '0.7')) + s.add_function(c1 * x + y**2 - z * c2) + back = pp.system(s.to_classic_input()) + v = np.array((complex(0.5, 0.1), complex(0.5, -0.2), complex(0.3, 0.4))) + a = np.array([complex(w) for w in s.eval(v)]) + b = np.array([complex(w) for w in back.eval(v)]) + assert np.allclose(a, b) + # scientific-notation complex coefficients too + s2 = pp.system('variable_group x; function f; f = (1.5e-3,-2.0e2)*x + 1;') + assert abs(complex(s2.eval(np.array((complex(1, 0),)))[0]) - (1.0015 - 200j)) < 1e-9 + + +def test_to_classic_input_round_trips_through_parse(): + # The full round-trip: a System's own classic-input text parses straight back. + sys = System() + x, y, z = variables(list('xyz')) + sys.add_variable_group([x, y, z]) + sys.add_function(x**2 + y**2 + z**2 - 1) + back = pp.system(sys.to_classic_input()) + assert back.num_variables() == 3 + assert len(list(back.functions())) == 1 + v = np.array((complex(0.5, 0.0), complex(0.25, 0.0), complex(0.0, 0.0))) + assert abs(back.eval(v)[0] - (0.25 + 0.0625 - 1)) < 1e-12 + + def _f_eval(expr, vals): """Parse 'f = ' over x,y,z and evaluate at vals.""" return pp.system(f'function f; variable_group x,y,z; f = {expr};').eval(vals)[0] diff --git a/python_bindings/src/tracker_config_export.cpp b/python_bindings/src/tracker_config_export.cpp index 48b74fb89..acf19a399 100644 --- a/python_bindings/src/tracker_config_export.cpp +++ b/python_bindings/src/tracker_config_export.cpp @@ -25,6 +25,7 @@ namespace bertini{ ; enum_("SuccessCode") + .value("NeverStarted", SuccessCode::NeverStarted) .value("Success", SuccessCode::Success) .value("HigherPrecisionNecessary", SuccessCode::HigherPrecisionNecessary) .value("ReduceStepSize", SuccessCode::ReduceStepSize)