ITADN

[BUG]: Precision parameter not respected for float literals in `TemplateExpressionSpec`

#1141OpenGongJr0 创建于 2026-02-16
bug
G
GongJr0commented
### What happened? In writing an automatic template generator I've come across an issue that where constants inside a template's `combine` string cause type promotion to `Float64`. This may be intended behavior but I wanted to note the behavior's existence here as a bug report. ## Example template: ```python TemplateExpressionSpec( combine="2*x + 1.0 + f1(x,y)", expressions=["f1"], variable_names=["x", "y"], ) ``` ## Small script to reproduce the issue: ```python from pysr import PySRRegressor, TemplateExpressionSpec import numpy as np if __name__ == "__main__": binary_operators = ["+", "-", "*", "/"] unary_operators = ["sin", "asinh"] X = np.random.normal(size=(100, 2)) noise = np.random.normal(scale=0.1, size=X.shape[0]) y = 2*X[:, 0] + 1.0 + np.asinh(X[:, 0]) + np.sin(X[:, 1]) + noise spec = TemplateExpressionSpec( combine="2*x+1.0 + f1(x,y)", expressions=["f1"], variable_names=["x", "y"], ) model = PySRRegressor( binary_operators=binary_operators, unary_operators=unary_operators, expression_spec=spec, niterations=10, precision=32, ) model.fit(X, y) print(model.get_best()) ``` ## Possible Solution In my case, I implemented a `sympy,printing.str.StrPrinter` subclass to wrap any float literal with the appropriate precision. The implementation: 1. Walks the expression as a sympy object and marks float literals by wrapping them with the `JFloat` function. 2. Converts rationals to `p//q` notation to avoid literals (may be redundant) 3. Substitutes all instances of JFloat with the appropriate `f"Float{prec}"` function. The steps above convert the example template (`2*x + 1.0 + f1(x,y)`) to `'x*Float32(2//1) + Float32(1.00000000000000) + f1(x, y)'` for the default precision of 32 bits. Below is an implementation: ```python import sympy as sp from sympy.printing.str import StrPrinter from typing import Any class JFloat(sp.Function): nargs = 1 class JuliaTypedPrinter(StrPrinter): """Printer that emits Julia-ish code and renders JFloat(x) as Float{prec}(x).""" def __init__(self, prec: int, **kwargs: Any) -> None: super().__init__(**kwargs) self.prec = prec def _print_JFloat(self, expr: sp.Expr) -> str: # expr.args[0] is the numeric literal inner = expr.args[0] if isinstance(inner, sp.Float): # Use full precision string SymPy provides lit = sp.sstr(inner) elif isinstance(inner, sp.Rational): # Emit as "p//q" to avoid Julia parsing as Float64. lit = f"{inner.p}//{inner.q}" else: lit = sp.sstr(inner) return f"Float{self.prec}({lit})" def _print_Pow(self, expr: sp.Expr) -> str: # Julia uses ^, SymPy prints ** by default base, exp = expr.as_base_exp() return f"({self._print(base)})^({self._print(exp)})" def _print_Mul(self, expr: sp.Expr) -> str: # SymPy may insert rationals like 1/2*x return "*".join(self._print(a) for a in expr.args) def _print_Add(self, expr: sp.Expr) -> str: return " + ".join(self._print(a) for a in expr.args) def _needs_float_wrap(expr: sp.Expr) -> bool: """Return True for numeric atoms that should be typed as Float{prec}.""" # SymPy Float => definitely wrap if isinstance(expr, sp.Float): return True # Rational => usually wrap to avoid Float64 promotion in Julia if isinstance(expr, sp.Rational): return True return False def wrap_numeric_literals(expr: sp.Expr) -> sp.Expr: """Wrap numeric literals in JFloat(...) where appropriate.""" def repl(e: sp.Expr) -> sp.Expr: if e.is_Number and _needs_float_wrap(e): return JFloat(e) # pyright: ignore return e # Replace numeric atoms bottom-up return expr.replace( lambda e: e.is_Number and _needs_float_wrap(e), repl ) def sympy_to_julia_typed(expr: sp.Expr, prec: int) -> str: expr2 = wrap_numeric_literals(expr) out = JuliaTypedPrinter(prec=prec).doprint(expr2) return out ``` ## Notes My implementation requires the `combine` string to be parsed in python and converted to `sympy` object before getting passed to julia. There might be a more elegant solution on the julia-side. ### Version 1.5.9 ### Operating System Windows ### Package Manager pip ### Interface Script (i.e., `python my_script.py`) ### Relevant log output ```shell juliacall.JuliaError: Element type of `x` is Float64 is different from element type of `y` which is Float32. Stacktrace: [1] error(s::String) @ Base .\error.jl:35 [2] _loss(::Vector{Float64}, ::Vector{Float32}, ::L2DistLoss) @ SymbolicRegression.LossFunctionsModule C:\Users\guney\.julia\packages\SymbolicRegression\L5TJa\src\LossFunctions.jl:25 [3] _eval_loss(tree::TemplateExpression{Float32, TemplateStructure{(:f1,), (), typeof(__sr_template_8838205672071328129), @NamedTuple{f1::Int64}, @NamedTuple{}}, Node{Float32}, ComposableExpression{Float32, Node{Float32}, @NamedTuple{operators::OperatorEnum{Tuple{typeof(+), typeof(-), typeof(*), typeof(/)}, Tuple{typeof(sin), typeof(asinh)}}, variable_names::Nothing, eval_options::EvalOptions{false, false, true, Nothing}}}, @NamedTuple{f1::ComposableExpression{Float32, Node{Float32}, @NamedTuple{operators::OperatorEnum{Tuple{typeof(+), typeof(-), typeof(*), typeof(/)}, Tuple{typeof(sin), typeof(asinh)}}, variable_names::Nothing, eval_options::EvalOptions{false, false, true, Nothing}}}}, @NamedTuple{structure::TemplateStructure{(:f1,), (), typeof(__sr_template_8838205672071328129), @NamedTuple{f1::Int64}, @NamedTuple{}}, operators::OperatorEnum{Tuple{typeof(+), typeof(-), typeof(*), typeof(/)}, Tuple{typeof(sin), typeof(asinh)}}, variable_names::Nothing, parameters::@NamedTuple{}}}, dataset::SymbolicRegression.CoreModule.DatasetModule.BasicDataset{Float32, Float32, Matrix{Float32}, Vector{Float32}, Nothing, @NamedTuple{}, Nothing, Nothing, Nothing, Nothing}, options::Options{SymbolicRegression.CoreModule.OptionsStructModule.ComplexityMapping{Int64, Int64}, OperatorEnum{Tuple{typeof(+), typeof(-), typeof(*), typeof(/)}, Tuple{typeof(sin), typeof(asinh)}}, Node, TemplateExpression, @NamedTuple{structure::TemplateStructure{(:f1,), (), typeof(__sr_template_8838205672071328129), @NamedTuple{f1::Int64}, @NamedTuple{}}}, MutationWeights, false, false, nothing, Nothing, 5}, regularization::Bool) @ SymbolicRegression.LossFunctionsModule C:\Users\guney\.julia\packages\SymbolicRegression\L5TJa\src\LossFunctions.jl:109 [4] eval_loss(tree::TemplateExpression{Float32, TemplateStructure{(:f1,), (), typeof(__sr_template_8838205672071328129), @NamedTuple{f1::Int64}, @NamedTuple{}}, Node{Float32}, ComposableExpression{Float32, Node{Float32}, @NamedTuple{operators::OperatorEnum{Tuple{typeof(+), typeof(-), typeof(*), typeof(/)}, Tuple{typeof(sin), typeof(asinh)}}, variable_names::Nothing, eval_options::EvalOptions{false, false, true, Nothing}}}, @NamedTuple{f1::ComposableExpression{Float32, Node{Float32}, @NamedTuple{operators::OperatorEnum{Tuple{typeof(+), typeof(-), typeof(*), typeof(/)}, Tuple{typeof(sin), typeof(asinh)}}, variable_names::Nothing, eval_options::EvalOptions{false, false, true, Nothing}}}}, @NamedTuple{structure::TemplateStructure{(:f1,), (), typeof(__sr_template_8838205672071328129), @NamedTuple{f1::Int64}, @NamedTuple{}}, operators::OperatorEnum{Tuple{typeof(+), typeof(-), typeof(*), typeof(/)}, Tuple{typeof(sin), typeof(asinh)}}, variable_names::Nothing, parameters::@NamedTuple{}}}, dataset::SymbolicRegression.CoreModule.DatasetModule.BasicDataset{Float32, Float32, Matrix{Float32}, Vector{Float32}, Nothing, @NamedTuple{}, Nothing, Nothing, Nothing, Nothing}, options::Options{SymbolicRegression.CoreModule.OptionsStructModule.ComplexityMapping{Int64, Int64}, OperatorEnum{Tuple{typeof(+), typeof(-), typeof(*), typeof(/)}, Tuple{typeof(sin), typeof(asinh)}}, Node, TemplateExpression, @NamedTuple{structure::TemplateStructure{(:f1,), (), typeof(__sr_template_8838205672071328129), @NamedTuple{f1::Int64}, @NamedTuple{}}}, MutationWeights, false, false, nothing, Nothing, 5}; regularization::Bool, idx::Nothing) @ SymbolicRegression.LossFunctionsModule C:\Users\guney\.julia\packages\SymbolicRegression\L5TJa\src\LossFunctions.jl:155 [5] eval_loss @ C:\Users\guney\.julia\packages\SymbolicRegression\L5TJa\src\LossFunctions.jl:139 [inlined] [6] update_baseline_loss! @ C:\Users\guney\.julia\packages\SymbolicRegression\L5TJa\src\LossFunctions.jl:225 [inlined] [7] _validate_options(datasets::Vector{SymbolicRegression.CoreModule.DatasetModule.BasicDataset{Float32, Float32, Matrix{Float32}, Vector{Float32}, Nothing, @NamedTuple{}, Nothing, Nothing, Nothing, Nothing}}, ropt::SymbolicRegression.SearchUtilsModule.RuntimeOptions{:multithreading, 1, true, Nothing}, options::Options{SymbolicRegression.CoreModule.OptionsStructModule.ComplexityMapping{Int64, Int64}, OperatorEnum{Tuple{typeof(+), typeof(-), typeof(*), typeof(/)}, Tuple{typeof(sin), typeof(asinh)}}, Node, TemplateExpression, @NamedTuple{structure::TemplateStructure{(:f1,), (), typeof(__sr_template_8838205672071328129), @NamedTuple{f1::Int64}, @NamedTuple{}}}, MutationWeights, false, false, nothing, Nothing, 5}) @ SymbolicRegression C:\Users\guney\.julia\packages\SymbolicRegression\L5TJa\src\SymbolicRegression.jl:597 [8] _equation_search(datasets::Vector{SymbolicRegression.CoreModule.DatasetModule.BasicDataset{Float32, Float32, Matrix{Float32}, Vector{Float32}, Nothing, @NamedTuple{}, Nothing, Nothing, Nothing, Nothing}}, ropt::SymbolicRegression.SearchUtilsModule.RuntimeOptions{:multithreading, 1, true, Nothing}, options::Options{SymbolicRegression.CoreModule.OptionsStructModule.ComplexityMapping{Int64, Int64}, OperatorEnum{Tuple{typeof(+), typeof(-), typeof(*), typeof(/)}, Tuple{typeof(sin), typeof(asinh)}}, Node, TemplateExpression, @NamedTuple{structure::TemplateStructure{(:f1,), (), typeof(__sr_template_8838205672071328129), @NamedTuple{f1::Int64}, @NamedTuple{}}}, MutationWeights, false, false, nothing, Nothing, 5}, saved_state::Nothing) @ SymbolicRegression C:\Users\guney\.julia\packages\SymbolicRegression\L5TJa\src\SymbolicRegression.jl:567 [9] equation_search(datasets::Vector{SymbolicRegression.CoreModule.DatasetModule.BasicDataset{Float32, Float32, Matrix{Float32}, Vector{Float32}, Nothing, @NamedTuple{}, Nothing, Nothing, Nothing, Nothing}}; options::Options{SymbolicRegression.CoreModule.OptionsStructModule.ComplexityMapping{Int64, Int64}, OperatorEnum{Tuple{typeof(+), typeof(-), typeof(*), typeof(/)}, Tuple{typeof(sin), typeof(asinh)}}, Node, TemplateExpression, @NamedTuple{structure::TemplateStructure{(:f1,), (), typeof(__sr_template_8838205672071328129), @NamedTuple{f1::Int64}, @NamedTuple{}}}, MutationWeights, false, false, nothing, Nothing, 5}, saved_state::Nothing, runtime_options::Nothing, runtime_options_kws::@Kwargs{niterations::Int64, parallelism::String, numprocs::Nothing, procs::Nothing, addprocs_function::Nothing, heap_size_hint_in_bytes::Nothing, worker_imports::Nothing, runtests::Bool, return_state::Bool, run_id::String, verbosity::Int64, logger::Nothing, progress::Bool, v_dim_out::Val{1}}) @ SymbolicRegression C:\Users\guney\.julia\packages\SymbolicRegression\L5TJa\src\SymbolicRegression.jl:561 [10] equation_search @ C:\Users\guney\.julia\packages\SymbolicRegression\L5TJa\src\SymbolicRegression.jl:542 [inlined] [11] #equation_search#23 @ C:\Users\guney\.julia\packages\SymbolicRegression\L5TJa\src\SymbolicRegression.jl:511 [inlined] [12] equation_search @ C:\Users\guney\.julia\packages\SymbolicRegression\L5TJa\src\SymbolicRegression.jl:456 [inlined] [13] #equation_search#24 @ C:\Users\guney\.julia\packages\SymbolicRegression\L5TJa\src\SymbolicRegression.jl:535 [inlined] [14] pyjlany_call(self::typeof(equation_search), args_::Py, kwargs_::Py) @ PythonCall.JlWrap C:\Users\guney\.julia\packages\PythonCall\avYrV\src\JlWrap\any.jl:44 [15] _pyjl_callmethod(f::Any, self_::Ptr{PythonCall.C.PyObject}, args_::Ptr{PythonCall.C.PyObject}, nargs::Int64) @ PythonCall.JlWrap C:\Users\guney\.julia\packages\PythonCall\avYrV\src\JlWrap\base.jl:73 [16] _pyjl_callmethod(o::Ptr{PythonCall.C.PyObject}, args::Ptr{PythonCall.C.PyObject}) @ PythonCall.JlWrap.Cjl C:\Users\guney\.julia\packages\PythonCall\avYrV\src\JlWrap\C.jl:63 ``` ### Extra Info _No response_
3 条评论