Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RootFinding GSOC project #1192

Merged
merged 104 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from 64 commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
0d6395e
RootFinding GSOC project
n0rbed Jul 25, 2024
3535116
spelling mistakes
n0rbed Jul 25, 2024
e5bee71
detect difficult poly case
n0rbed Jul 26, 2024
9f3dcc7
added g(x) + sqrt(f(x)) + c solution in attract
Jul 26, 2024
de86f9a
made comp_rational not rationalize every float and instead leave it f…
Jul 27, 2024
d20acfc
cleaned and moved postprocess tests
Jul 27, 2024
fd6c8b0
fixed some of alex's comments
Jul 27, 2024
688c71d
seperating form reference
Jul 27, 2024
83564ed
moved includes to top level
Jul 27, 2024
4e2c2d7
change path for includes
Jul 27, 2024
b41f8e5
changed ssqrt and scbrt prints, and added sqrt case poly tests
Jul 28, 2024
96a470a
spelling mistake
Jul 28, 2024
76111e4
solve now supports input of type Equation
Jul 30, 2024
3c302d2
integrate solve_for
Jul 30, 2024
f3e6cf9
reverted solve_for merge
Jul 30, 2024
217cbf7
var conflict prevented
Jul 31, 2024
d91684e
renamed solve -> symbolic_solve and removed most deepcopy()'s
n0rbed Aug 1, 2024
09d5f49
added documentation draft to docs/
Aug 2, 2024
fcf4153
mult -> repeated in kwargs, and updated documentation
Aug 2, 2024
8f77546
alex notes
Aug 2, 2024
6d77b72
added depwarn
Aug 3, 2024
86f85ea
moved Nemo to ext
Aug 3, 2024
8beb6a7
alex comments
Aug 4, 2024
c724a19
solve_for deprecated
Aug 5, 2024
e26328f
BigInt conversion commented out
Aug 6, 2024
95ba446
removed f_numbers calls
Aug 6, 2024
cc39248
fixed n_func_occ bug
Aug 8, 2024
aa25afa
readded better rationalization
Aug 8, 2024
cbb7577
BigInt and rationalization added once again
Aug 8, 2024
ca9f7b0
solver: X^0 => 1, X^1 => X
Aug 8, 2024
979e6b8
Merge branch 'RootFinding' of https://github.com/n0rbed/Symbolics.jl …
Aug 8, 2024
aed90c0
moved solve_for doc and deprecated.
Aug 9, 2024
2a817f5
postprocess radicals of integers
Aug 10, 2024
3107d80
renamed coeffs.jl and made postprocess run till reaches stabilization
Aug 9, 2024
ca8b4a4
Merge remote-tracking branch 'origin/master' into RootFinding
bowenszhu Aug 11, 2024
99c126f
alex comments
Aug 13, 2024
a37724b
Merge branch 'RootFinding' of https://github.com/n0rbed/Symbolics.jl …
Aug 13, 2024
2c3803f
fixed compilation bug
n0rbed Aug 14, 2024
fa864e1
groebner bug and correct solve_for deprecation
n0rbed Aug 14, 2024
5c0c084
bug fix
n0rbed Aug 14, 2024
1ca59a1
updated proj version
Aug 15, 2024
460f0c9
added kwargs when calling solve_for
Aug 15, 2024
c0f5cee
moved new_solver tests to solver
Aug 15, 2024
d9ef174
moved algebraic solver tests to algebraic_solver.jl
Aug 15, 2024
80f6626
touched up documentation
Aug 15, 2024
3bc67cc
rmd one try
Aug 15, 2024
60b49a1
unnecessary conversion of array
Aug 15, 2024
9f4c68d
alex comment
Aug 15, 2024
0bed726
added groebner dependency
Aug 15, 2024
010fd56
warns parameter added and removed throws
Aug 15, 2024
bf5d82b
added Nemo to docs dependancies
Aug 15, 2024
86b3b3e
comment on try catch
Aug 15, 2024
51fac4c
parametric groebner demote draft
n0rbed Aug 15, 2024
b4e4fcf
parametric groebner demote wrapped
n0rbed Aug 15, 2024
9d4b2c6
Merge branch 'RootFinding' of https://github.com/n0rbed/Symbolics.jl …
Aug 15, 2024
b61de0b
removed all try catches
Aug 16, 2024
d952e89
removed Symbolics.
Aug 16, 2024
c571c5a
Update src/Symbolics.jl
n0rbed Aug 16, 2024
cde495d
Merge branch 'RootFinding' of https://github.com/n0rbed/Symbolics.jl …
Aug 15, 2024
aedd693
Merge branch 'master' into RootFinding
n0rbed Aug 16, 2024
9d59d16
Merge branch 'RootFinding' of https://github.com/n0rbed/Symbolics.jl …
Aug 16, 2024
fe30665
sub filter
Aug 17, 2024
803a172
conditions for log and postprocess bug
Aug 17, 2024
c97d4a0
fixed test
Aug 17, 2024
4282601
fixed bug
Aug 17, 2024
c7aae07
algebraic solver rmd and added tests to solver.jl
Aug 17, 2024
26f5415
rmd algebraic solver tests
Aug 17, 2024
1d68f3f
Update src/Symbolics.jl
n0rbed Aug 17, 2024
e060bcc
Merge branch 'master' into RootFinding
ChrisRackauckas Aug 17, 2024
9cad810
docs: specify versions
Aug 17, 2024
788d2c8
fixed bug
Aug 17, 2024
11a5bc1
Merge branch 'RootFinding' of https://github.com/n0rbed/Symbolics.jl …
Aug 17, 2024
fcbadcc
add feature list to solver docs
Aug 17, 2024
734143c
specify Nemo version
Aug 17, 2024
8675b38
bug fixes
Aug 17, 2024
71ad8ec
very small changes
Aug 17, 2024
01d1ef3
export symbolic_linear_solve, solve_for
Aug 17, 2024
ee08310
some stuff
Aug 17, 2024
0895f89
Merge branch 'RootFinding' of https://github.com/n0rbed/Symbolics.jl …
Aug 17, 2024
8be6ee8
fixed bug
Aug 17, 2024
364869f
added slog and friends to known keys
Aug 17, 2024
9834965
fix footnotes
Aug 17, 2024
5522b9a
differential checks
Aug 17, 2024
968fd9e
warnings
Aug 17, 2024
4ae737d
remove some comments in solver
Aug 17, 2024
5500727
remove unused functions
Aug 17, 2024
b5e6a79
add top-level .JuliaFormatter.toml, format sovler
Aug 17, 2024
ccdd3ed
alex comments + bug fix
Aug 17, 2024
216e414
removed prime func
Aug 17, 2024
5107a94
turn off formatting for some s
Aug 17, 2024
67e2c2b
parametric nonlinear solver
Aug 18, 2024
6e490fe
math constants not expanded
Aug 17, 2024
7908bfd
parametric solver (without tests), some fixes
Aug 18, 2024
adcc8f5
GB solver, quickfix
Aug 18, 2024
d402481
now roots_of is an expression
Aug 18, 2024
0a21bbb
Merge branch 'RootFinding' of https://github.com/n0rbed/Symbolics.jl …
Aug 18, 2024
4863017
Merge branch 'RootFinding' of https://github.com/n0rbed/Symbolics.jl …
Aug 17, 2024
7a54f98
add a couple of tests
Aug 18, 2024
3318a51
tests should pass now
Aug 18, 2024
cac281c
transcendence basis and hakuna matata
Aug 18, 2024
c5c27c7
add a test for tr. basis
Aug 18, 2024
cf81126
add missing test for postprocess
Aug 19, 2024
7dc05fa
fixed test
n0rbed Aug 19, 2024
fe245e1
symbolic_solve(expr)
n0rbed Aug 19, 2024
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
5 changes: 4 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3"
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
Primes = "27ebfcd6-29c5-5fa9-bf4b-fb8fc14df3ae"
RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
RuntimeGeneratedFunctions = "7e49a35a-f44a-4d26-94aa-eba1b4ca6b47"
Expand All @@ -43,13 +44,15 @@ TermInterface = "8ea1fca8-c5ef-4a55-8b96-4e9afe9c9a3c"
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
Groebner = "0b43b601-686d-58a3-8a1c-6623616c7cd4"
LuxCore = "bb33d45b-7691-41d6-9220-0943567d0623"
Nemo = "2edaba10-b0f1-5616-af89-8c11ac63239a"
PreallocationTools = "d236fae5-4411-538c-8e31-a6e3d9e00b46"
SymPy = "24249f21-da20-56a4-8eb1-6a02cf4ae2e6"

[extensions]
SymbolicsForwardDiffExt = "ForwardDiff"
SymbolicsGroebnerExt = "Groebner"
SymbolicsLuxCoreExt = "LuxCore"
SymbolicsNemoExt = "Nemo"
SymbolicsPreallocationToolsExt = "PreallocationTools"
SymbolicsSymPyExt = "SymPy"

Expand Down Expand Up @@ -107,4 +110,4 @@ SymPy = "24249f21-da20-56a4-8eb1-6a02cf4ae2e6"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test", "SafeTestsets", "Pkg", "PkgBenchmark", "PreallocationTools", "ForwardDiff", "Groebner", "BenchmarkTools", "ReferenceTests", "SymPy", "Random", "Lux", "ComponentArrays"]
test = ["Test", "SafeTestsets", "Pkg", "PkgBenchmark", "PreallocationTools", "ForwardDiff", "Groebner", "BenchmarkTools", "ReferenceTests", "SymPy", "Random", "Lux", "ComponentArrays", "Nemo"]
2 changes: 2 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b"
Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7"
Groebner = "0b43b601-686d-58a3-8a1c-6623616c7cd4"
Nemo = "2edaba10-b0f1-5616-af89-8c11ac63239a"

[compat]
BenchmarkTools = "1.3"
Expand Down
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ makedocs(
"manual/expression_manipulation.md",
"manual/derivatives.md",
"manual/groebner.md",
"manual/solver.md",
"manual/arrays.md",
"manual/build_function.md",
"manual/functions.md",
Expand Down
54 changes: 54 additions & 0 deletions docs/src/manual/solver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Solver
The main `symbolic` solver for Symbolics.jl is `symbolic_solve`. Symbolic solving
means that it only uses analytical methods and manually solves equations for input variables.
It uses no numerical methods and only outputs exact solutions.
```@docs
Symbolics.symbolic_solve
```

One other symbolic solver is `symbolic_linear_solve` which is limited compared to
`symbolic_solve` as it only solves linear equations.
```@docs
Symbolics.symbolic_linear_solve
```
n0rbed marked this conversation as resolved.
Show resolved Hide resolved

`symbolic_solve` only supports symbolic, i.e. non-floating point computations, and thus prefers equations
where the coefficients are integer, rational, or symbolic. Floating point coefficients are transformed into
rational values and BigInt values are used internally with a potential performance loss, and thus it is recommended
that this functionality is only used with floating point values if necessary. In contrast, `symbolic_linear_solve`
directly handles floating point values using standard factorizations.

### More technical details and examples
#### Technical details
The `symbolic_solve` function uses 4 hidden solvers in order to solve the user's input. Its base,
`solve_univar`, uses analytic solutions up to polynomials of degree 4 and factoring as its method
for solving univariate polynomials. The function's `solve_multipoly` uses GCD on the input polynomials then throws passes the result
to `solve_univar`. The function's `solve_multivar` uses Groebner basis and a separating form in order to create linear equations in the
input variables and a single high degree equation in the separating variable. Each equation resulting from the basis is then passed
to `solve_univar`. We can see that essentially, `solve_univar` is the building block of `symbolic_solve`.if the input is not a valid polynomial and can not be solved by the algorithm above, `symbolic_solve` passes
it to `ia_solve`, which attempts solving by attraction and isolation [^1]. This only works when the input is a single expression
and the user wants the answer in terms of a single variable. Say `log(x) - a == 0` gives us `[e^a]`.

#### Nice examples
```@example solver
using Symbolics, Nemo;
@variables x;
Symbolics.symbolic_solve(9^x + 3^x ~ 8, x)
```

```@example solver
@variables x y z;
Symbolics.symbolic_linear_solve(2//1*x + y - 2//1*z ~ 9//1*x, 1//1*x)
```

```@example solver
using Groebner;
@variables x y z;

eqs = [x^2 + y + z - 1, x + y^2 + z - 1, x + y + z^2 - 1]
Symbolics.symbolic_solve(eqs, [x,y,z])
n0rbed marked this conversation as resolved.
Show resolved Hide resolved
```

# References
[^1]: [R. W. Hamming, Coding and Information Theory, ScienceDirect, 1980](https://www.sciencedirect.com/science/article/pii/S0747717189800070).

111 changes: 111 additions & 0 deletions ext/SymbolicsGroebnerExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,115 @@ function Symbolics.is_groebner_basis(polynomials::Vector{Num}; kwargs...)
Groebner.isgroebner(polynoms; kwargs...)
end

function Symbolics.solve_multivar(eqs::Vector, vars::Vector{Num}; dropmultiplicity=true, warns=true)

# Reference: Rouillier, F. Solving Zero-Dimensional Systems
# Through the Rational Univariate Representation.
# AAECC 9, 433–461 (1999). https://doi.org/10.1007/s002000050114

# Use a new variable to separate the input polynomials (Reference above)
new_var = Symbolics.gen_separating_var(vars)
old_len = length(vars)
push!(vars, new_var)

new_eqs = []
generating = true
n_iterations = 1

while generating
new_eqs = copy(eqs)
eq = new_var
for i = 1:(old_len)
eq += BigInt(rand(-n_iterations:n_iterations))*vars[i]
end

if isequal(eq, new_var)
continue
end

push!(new_eqs, eq)

new_eqs = Symbolics.groebner_basis(new_eqs, ordering=Lex(vars))

if length(new_eqs) <= length(vars)
generating &= false
end

for i in eachindex(new_eqs)[2:end]
generating |= all(Symbolics.degree(var) > 1 for var in Symbolics.get_variables(new_eqs[i]))
end

if isequal(new_eqs[1], new_var)
generating = true
end

n_iterations += 1
end

solutions = []

# handle "unsolvable" cases
if isequal(1, new_eqs[1])
return solutions
end
if length(new_eqs) < length(vars)
warns && (@warn("Infinite number of solutions"); return nothing) || return nothing
n0rbed marked this conversation as resolved.
Show resolved Hide resolved
end

new_eqs = SymbolicUtils.toterm.(new_eqs)

# first, solve any single variable equations
i = 1
while !(i > length(new_eqs))
present_vars = Symbolics.get_variables(new_eqs[i])
for var in vars
if size(present_vars, 1) == 1 && isequal(var, present_vars[1])
new_sols = Symbolics.solve_univar(Symbolics.wrap(new_eqs[i]), var, dropmultiplicity=dropmultiplicity)

if length(solutions) == 0
append!(solutions, [Dict{Num, Any}(var => sol) for sol in new_sols])
else
solutions = Symbolics.add_sol_to_all(solutions, new_sols, var)
end

deleteat!(new_eqs, i)
i = i - 1
break
end
end
i = i + 1
end


# second, iterate over eqs and sub each found solution
# then add the roots of the remaining unknown variables
for eq in new_eqs
solved = false
present_vars = Symbolics.get_variables(eq)
size_of_sub = length(solutions[1])

if size(present_vars, 1) <= (size_of_sub + 1)
while !solved
subbed_eq = eq
for (var, root) in solutions[1]
subbed_eq = Symbolics.substitute(subbed_eq, Dict([var => root]), fold=false)
end

var_tosolve = Symbolics.get_variables(subbed_eq)[1]
new_var_sols = Symbolics.solve_univar(subbed_eq, var_tosolve, dropmultiplicity=dropmultiplicity)
Symbolics.add_sol!(solutions, new_var_sols, var_tosolve, 1)

solved = all(x -> length(x) == size_of_sub+1, solutions)
end
end
end

pop!(vars)
for roots in solutions
delete!(roots, new_var)
end

return solutions
end

end # module
127 changes: 127 additions & 0 deletions ext/SymbolicsNemoExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
module SymbolicsNemoExt
using Nemo

if isdefined(Base, :get_extension)
using Symbolics
using Symbolics: Num, symtype
else
using ..Symbolics
using ..Symbolics: Num, symtype
end

# Map each variable of the given poly.
# Can be used to transform Nemo polynomial to expression.
function nemo_crude_evaluate(poly::Nemo.MPolyRingElem, varmap)
new_poly = 0
for (i, term) in enumerate(Nemo.terms(poly))
new_term = nemo_crude_evaluate(Nemo.coeff(poly, i), varmap)
for var in Nemo.vars(term)
exp = Nemo.degree(term, var)
exp == 0 && continue
new_var = varmap[var]
new_term *= new_var^exp
end
new_poly += new_term
end
new_poly
end

function nemo_crude_evaluate(poly::Nemo.FracElem, varmap)
nemo_crude_evaluate(numerator(poly), varmap) // nemo_crude_evaluate(denominator(poly), varmap)
end

function nemo_crude_evaluate(poly::Nemo.ZZRingElem, varmap)
Rational(poly)
end

# factor(x^2*y + b*x*y - a*x - a*b) -> (x*y - a)*(x + b)
function Symbolics.factor_use_nemo(poly::Num)
Symbolics.check_polynomial(poly)
Symbolics.degree(poly) == 0 && return poly, Num[]
vars = Symbolics.get_variables(poly)
nemo_ring, nemo_vars = Nemo.polynomial_ring(Nemo.QQ, map(string, vars))
sym_to_nemo = Dict(vars .=> nemo_vars)
nemo_to_sym = Dict(v => k for (k, v) in sym_to_nemo)
nemo_poly = Symbolics.substitute(poly, sym_to_nemo)
nemo_fac = Nemo.factor(nemo_poly)
nemo_unit = Nemo.unit(nemo_fac)
nemo_factors = collect(keys(nemo_fac.fac))
sym_unit = Rational(Nemo.coeff(nemo_unit, 1))
sym_factors = map(f -> Symbolics.wrap(nemo_crude_evaluate(f, nemo_to_sym)), nemo_factors)

for (i, fac) in enumerate(sym_factors)
sym_factors[i] = fac^(collect(values(nemo_fac.fac))[i])
end

return sym_unit, sym_factors
end

# gcd(x^2 - y^2, x^3 - y^3) -> x - y
function Symbolics.gcd_use_nemo(poly1::Num, poly2::Num)
Symbolics.check_polynomial(poly1)
Symbolics.check_polynomial(poly2)
vars1 = Symbolics.get_variables(poly1)
vars2 = Symbolics.get_variables(poly2)
vars = vcat(vars1, vars2)
nemo_ring, nemo_vars = Nemo.polynomial_ring(Nemo.QQ, map(string, vars))
sym_to_nemo = Dict(vars .=> nemo_vars)
nemo_to_sym = Dict(v => k for (k, v) in sym_to_nemo)
nemo_poly1 = Symbolics.substitute(poly1, sym_to_nemo)
nemo_poly2 = Symbolics.substitute(poly2, sym_to_nemo)
nemo_gcd = Nemo.gcd(nemo_poly1, nemo_poly2)
sym_gcd = Symbolics.wrap(nemo_crude_evaluate(nemo_gcd, nemo_to_sym))
return sym_gcd
end


function Symbolics.demote(gb, vars::Vector{Num}, params::Vector{Num})
gb = Symbolics.wrap.(SymbolicUtils.toterm.(gb))
Symbolics.check_polynomial.(gb)

all_vars = [vars..., params...]
nemo_ring, nemo_all_vars = Nemo.polynomial_ring(Nemo.QQ, map(string, all_vars))

sym_to_nemo = Dict(all_vars .=> nemo_all_vars)
nemo_to_sym = Dict(v => k for (k, v) in sym_to_nemo)
nemo_gb = Symbolics.substitute(gb, sym_to_nemo)
nemo_gb = Symbolics.substitute(nemo_gb, sym_to_nemo)

nemo_vars = [v for (k, v) in sym_to_nemo if any(isequal(k, var) for var in vars)]
nemo_params = [v for (k, v) in sym_to_nemo if any(isequal(k, param) for param in params)]

ring_flat = parent(nemo_vars[1])
ring_param, params_demoted = Nemo.polynomial_ring(base_ring(ring_flat), map(string, nemo_params))
ring_demoted, vars_demoted = Nemo.polynomial_ring(fraction_field(ring_param), map(string, nemo_vars), internal_ordering=Nemo.internal_ordering(ring_flat))
varmap = Dict((nemo_vars .=> vars_demoted)..., (nemo_params .=> params_demoted)...)
gb_demoted = map(f -> nemo_crude_evaluate(f, varmap), nemo_gb)
result = empty(gb_demoted)
for i in 1:length(gb_demoted)
gb_demoted = map(f -> map_coefficients(c -> c // leading_coefficient(f), f), gb_demoted)
f = gb_demoted[i]
f_nf = Nemo.normal_form(f, result)
if !iszero(f_nf)
push!(result, f_nf)
end
end

sym_to_nemo = Dict(sym => nem for sym in all_vars for nem in [vars_demoted..., params_demoted...] if isequal(string(sym),string(nem)))
nemo_to_sym = Dict(v => k for (k, v) in sym_to_nemo)

final_result = []

for i in eachindex(result)

monoms = collect(Nemo.monomials(result[i]))
coeffs = collect(Nemo.coefficients(result[i]))

poly = 0
for j in eachindex(monoms)
poly += nemo_crude_evaluate(coeffs[j], nemo_to_sym) * nemo_crude_evaluate(monoms[j], nemo_to_sym)
end
push!(final_result, poly)
end

final_result
end

end # module
19 changes: 15 additions & 4 deletions src/Symbolics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ using DocStringExtensions, Markdown

using LinearAlgebra

using Primes

using Reexport

using DomainSets
Expand Down Expand Up @@ -151,10 +153,6 @@ include("plot_recipes.jl")

include("semipoly.jl")

include("solver.jl")
export solve_single_eq
export solve_system_eq
export lambertw

include("parsing.jl")
export parse_expr_to_symbolic
Expand Down Expand Up @@ -197,6 +195,19 @@ for sType in [Pair, Vector, Dict]
@eval substitute(expr::Arr, s::$sType; kw...) = wrap(substituter(s)(unwrap(expr); kw...))
end

# Symbolic solver
include("solver/preprocess.jl")
include("solver/nemo_stuff.jl")
include("solver/solve_helpers.jl")
include("solver/postprocess.jl")
include("solver/univar.jl")
include("solver/ia_helpers.jl")
include("solver/polynomialization.jl")
include("solver/attract.jl")
include("solver/ia_main.jl")
include("solver/main.jl")
export symbolic_solve

function symbolics_to_sympy end
export symbolics_to_sympy

Expand Down
Loading