From af44a983e39f06de995150e68cc947b1d29a9b0e Mon Sep 17 00:00:00 2001 From: silviana amethyst Date: Tue, 21 Jul 2026 11:25:45 +0000 Subject: [PATCH 1/4] fix(parse): parse.system tolerates a CONFIG section and INPUT/END; separators The native SystemParser accepts only the bare input body (variable groups + functions). A leading CONFIG...END; block, or the INPUT/END; separators that System.to_classic_input() itself emits, made it fail -- so parse.system(sys.to_classic_input()) did NOT round-trip and every caller had to hand-split the text first. Wrap parse.system in the Python layer to strip a leading CONFIG block and unwrap INPUT/END; before handing the body to the native parser; a raw body (what the parser already accepted) is passed through unchanged, so this only widens what parses. Now parse.system(a_system.to_classic_input()) just works. Regression tests: parse tolerates all three forms, and the full to_classic_input -> parse.system round-trip. Co-Authored-By: Claude Opus 4.8 (1M context) --- python/bertini/parse/__init__.py | 32 ++++++++++++++++++++++++++++++ python/test/classes/parser_test.py | 27 +++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/python/bertini/parse/__init__.py b/python/bertini/parse/__init__.py index 9b6c6b89e..0efc8dd41 100644 --- a/python/bertini/parse/__init__.py +++ b/python/bertini/parse/__init__.py @@ -34,9 +34,41 @@ 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 + + +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 + and INPUT/END; separators (so ``parse.system(sys.to_classic_input())`` round-trips). + """ + return _native_system(_input_body(text)) + + __all__ = dir(_pybparse) diff --git a/python/test/classes/parser_test.py b/python/test/classes/parser_test.py index f50b22f89..8a3be88d8 100644 --- a/python/test/classes/parser_test.py +++ b/python/test/classes/parser_test.py @@ -78,6 +78,33 @@ 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_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] From 04e871c0e63864e4e969ed383df8074c43333c48 Mon Sep 17 00:00:00 2001 From: silviana amethyst Date: Tue, 21 Jul 2026 12:42:27 +0000 Subject: [PATCH 2/4] fix(parse): parse.system reads (re,im) complex coefficient literals 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 did not round-trip (parse.system(sys.to_classic_input()) failed for any system with complex coefficients, e.g. a randomized tracking system). Rewrite (re,im) -> (re+im*I) in the parse wrapper so the round-trip holds for every system, real or complex (scientific notation included). Regression test covers eval-match round-trip + scientific-notation coefficients. Co-Authored-By: Claude Opus 4.8 (1M context) --- python/bertini/parse/__init__.py | 17 ++++++++++++++--- python/test/classes/parser_test.py | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/python/bertini/parse/__init__.py b/python/bertini/parse/__init__.py index 0efc8dd41..1c4ac5576 100644 --- a/python/bertini/parse/__init__.py +++ b/python/bertini/parse/__init__.py @@ -47,6 +47,16 @@ _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 @@ -64,10 +74,11 @@ def _input_body(text): def system(text): - """Parse a classic Bertini system from ``text``, tolerating a leading CONFIG section - and INPUT/END; separators (so ``parse.system(sys.to_classic_input())`` round-trips). + """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(_input_body(text)) + return _native_system(_fix_complex_literals(_input_body(text))) __all__ = dir(_pybparse) diff --git a/python/test/classes/parser_test.py b/python/test/classes/parser_test.py index 8a3be88d8..ab95ff440 100644 --- a/python/test/classes/parser_test.py +++ b/python/test/classes/parser_test.py @@ -92,6 +92,26 @@ def test_parse_tolerates_config_and_input_wrapper(): 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() From f373d3a09a4f499804bdfff7c0e011d5c86c3baa Mon Sep 17 00:00:00 2001 From: silviana amethyst Date: Thu, 23 Jul 2026 11:07:57 +0000 Subject: [PATCH 3/4] fix(io): float coefficients print at FULL precision in classic input Complex::print streamed mp values at the ostream default -- 6 significant digits -- so every printed system with computed real coefficients was a ~1e-6 impostor of the true polynomials (found when a records bundle's crit-curve det system replayed differently than the in-run system; the System itself always held exact coefficients -- only the printer truncated). str(0) prints every digit the stored value carries, bare for real, (re,im) pair for complex; parse.system reconstructs the identical binary value (regression test asserts exact round-trip for real and complex coefficients). Co-Authored-By: Claude Fable 5 --- core/src/function_tree/symbols/number.cpp | 10 +++++--- python/test/classes/classic_writer_test.py | 29 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) 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/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 From 1e7f2188e68ea060c779ff41608bac8f447d5d3c Mon Sep 17 00:00:00 2001 From: silviana amethyst Date: Fri, 24 Jul 2026 17:08:13 +0000 Subject: [PATCH 4/4] fix(bindings): name SuccessCode::NeverStarted -- the -1 that meant 'this path was never tracked' The enum bound every value except NeverStarted = -1, so a path whose metadata was never filled printed as the anonymous 'SuccessCode(-1)' -- exactly the code showing up on preimage-curve regeneration moves in half of all whitney gauntlet runs. Now it says what it means: the path never started. Co-Authored-By: Claude Fable 5 --- python_bindings/src/tracker_config_export.cpp | 1 + 1 file changed, 1 insertion(+) 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)