Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions core/src/function_tree/symbols/number.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) << ")";
}


Expand Down
43 changes: 43 additions & 0 deletions python/bertini/parse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

29 changes: 29 additions & 0 deletions python/test/classes/classic_writer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
47 changes: 47 additions & 0 deletions python/test/classes/parser_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <expr>' over x,y,z and evaluate at vals."""
return pp.system(f'function f; variable_group x,y,z; f = {expr};').eval(vals)[0]
Expand Down
1 change: 1 addition & 0 deletions python_bindings/src/tracker_config_export.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ namespace bertini{
;

enum_<SuccessCode>("SuccessCode")
.value("NeverStarted", SuccessCode::NeverStarted)
.value("Success", SuccessCode::Success)
.value("HigherPrecisionNecessary", SuccessCode::HigherPrecisionNecessary)
.value("ReduceStepSize", SuccessCode::ReduceStepSize)
Expand Down
Loading