diff --git a/Project.toml b/Project.toml index 128e9d0..98aad17 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "SymPyPythonCall" uuid = "bc8888f7-b21e-4b7c-a06a-5d9c9496438c" authors = ["jverzani and contributors"] -version = "0.1.0" +version = "0.1.1" [deps] CommonEq = "3709ef60-1bee-4518-9f2f-acd86f176c50" @@ -14,6 +14,14 @@ PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" +[weakdeps] +Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" +SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b" + +[extensions] +SymPyPythonCallSymbolicsExt = "Symbolics" +SymPyPythonCallSymbolicUtilsExt = "SymbolicUtils" + [compat] julia = "1.6.1" CommonEq = "0.2" @@ -26,6 +34,8 @@ SpecialFunctions = "0.8, 0.9, 0.10, 1.0, 2" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" +SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b" [targets] -test = ["Test"] +test = ["Symbolics", "Test"] diff --git a/README.md b/README.md index fa5b904..9a478b4 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,19 @@ [![Coverage](https://codecov.io/gh/jverzani/SymPyPythonCall.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/jverzani/SymPyPythonCall.jl) -This is a start on what is needed to use `PythonCall` instead of `PyCall` for `SymPy.jl`. -At the moment, the expectation is that *if* that change proves desirable, this would become `SymPy`. +This package allows access to the [SymPy](https://www.sympy.org/en/index.html) Python library to `Julia` users through [PythonCall](https://github.com/cjdoris/PythonCall.jl). -For now, there are some small design decisions from `SymPy` reflected here: +(The more established [SymPy.jl](https://github.com/JuliaPy/SymPy.jl) uses [PyCall.jl](https://github.com/JuliaPy/PyCall.jl).) -There would be a few deprecations: +At the moment, the expectation is that *if* that change proves desirable, this would become `SymPy`, but for now this is a standalone package. This may be or interest for those having difficulty installing the underlying `sympy` library using `PyCall`. + +---- + +Though nearly the same as `SymPy.jl`, for now, there are some small design decisions differing from `SymPy`: * `@vars` would be deprecated; use `@syms` only -* `elements` for sets would be removed (convert to a `Set` by default) +* `elements` for sets is deprecated (conversion to a `Set` is the newdefault) * `sympy.poly` *not* `sympy.Poly` diff --git a/docs/make.jl b/docs/make.jl index 9f3f8d6..69cc6ab 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -5,7 +5,18 @@ ENV["GKSwstype"] = "100" using SymPyPythonCall using Documenter -makedocs(sitename="My Documentation") +makedocs( + sitename = "SymPyPythonCall", + format = Documenter.HTML(), + modules = [SymPyPythonCall] +) + +# Documenter can also automatically deploy documentation to gh-pages. +# See "Hosting Documentation" and deploydocs() in the Documenter manual +# for more information. +deploydocs( + repo = "github.com/jverzani/SymPyPythonCall.jl.git" +) #DocMeta.setdocmeta!(SymPyPythonCall, :DocTestSetup, :(using SymPyPythonCall); recursive=true) diff --git a/ext/SymPyPythonCallSymbolicUtilsExt.jl b/ext/SymPyPythonCallSymbolicUtilsExt.jl new file mode 100644 index 0000000..cac0afe --- /dev/null +++ b/ext/SymPyPythonCallSymbolicUtilsExt.jl @@ -0,0 +1,52 @@ +module SymPyPythonCallSymbolicUtilsExt + +import SymPyPythonCall +import SymbolicUtils + +#== +Check if x represents an expression tree. If returns true, it will be assumed that operation(::T) and arguments(::T) methods are defined. Definining these three should allow use of SymbolicUtils.simplify on custom types. Optionally symtype(x) can be defined to return the expected type of the symbolic expression. +==# +function SymbolicUtils.istree(x::SymPyPythonCall.SymbolicObject) + !(convert(Bool, x.is_Atom)) +end + +#== +f x is a term as defined by istree(x), exprhead(x) must return a symbol, corresponding to the head of the Expr most similar to the term x. If x represents a function call, for example, the exprhead is :call. If x represents an indexing operation, such as arr[i], then exprhead is :ref. Note that exprhead is different from operation and both functions should be defined correctly in order to let other packages provide code generation and pattern matching features. +function TermInterface.exprhead(x::SymPyPythonCall.SymbolicObject) + :call # this is not right +end +==# + +#== +Returns the head (a function object) performed by an expression tree. Called only if istree(::T) is true. Part of the API required for simplify to work. Other required methods are arguments and istree +==# +function SymbolicUtils.operation(x::SymPyPythonCall.SymbolicObject) + @assert SymbolicUtils.istree(x) + nm = Symbol(SymPyPythonCall.Introspection.funcname(x)) + + λ = get(SymPyPythonCall.Introspection.funcname2function, nm, nothing) + if isnothing(λ) + return getfield(Main, nm) + else + return λ + end +end + + +#== +Returns the arguments (a Vector) for an expression tree. Called only if istree(x) is true. Part of the API required for simplify to work. Other required methods are operation and istree +==# +function SymbolicUtils.arguments(x::SymPyPythonCall.SymbolicObject) + collect(SymPyPythonCall.Introspection.args(x)) +end + +#== +Construct a new term with the operation f and arguments args, the term should be similar to t in type. if t is a SymbolicUtils.Term object a new Term is created with the same symtype as t. If not, the result is computed as f(args...). Defining this method for your term type will reduce any performance loss in performing f(args...) (esp. the splatting, and redundant type computation). T is the symtype of the output term. You can use SymbolicUtils.promote_symtype to infer this type. The exprhead keyword argument is useful when creating Exprs. +==# +function SymbolicUtils.similarterm(t::SymPyPythonCall.SymbolicObject, f, args, symtype=nothing; + metadata=nothing, exprhead=:call) + f(args...) # default +end + + +end diff --git a/ext/SymPyPythonCallSymbolicsExt.jl b/ext/SymPyPythonCallSymbolicsExt.jl new file mode 100644 index 0000000..7f5076f --- /dev/null +++ b/ext/SymPyPythonCallSymbolicsExt.jl @@ -0,0 +1,121 @@ +module SymPyPythonCallSymbolicsExt + +# from https://github.com/JuliaSymbolics/Symbolics.jl/pull/957/ +# by @jClugstor +import SymPyPythonCall +sp = SymPyPythonCall.sympy.py +const PythonCall = SymPyPythonCall.PythonCall +import PythonCall: pyconvert, pyimport, pyisinstance + +import Symbolics +import Symbolics: @variables + +# rule functions +function pyconvert_rule_sympy_symbolX(::Type{Symbolics.Num}, x) +end +function pyconvert_rule_sympy_symbol(::Type{Symbolics.Num}, x) + if !pyisinstance(x,sp.Symbol) + return PythonCall.pyconvert_unconverted() + end + name = PythonCall.pyconvert(Symbol,x.name) + return PythonCall.pyconvert_return(Symbolics.variable(name)) +end + +function pyconvert_rule_sympy_pow(::Type{Symbolics.Num}, x) + if !pyisinstance(x,sp.Pow) + return PythonCall.pyconvert_unconverted() + end + expbase = pyconvert(Symbolics.Num,x.base) + exp = pyconvert(Symbolics.Num,x.exp) + return PythonCall.pyconvert_return(expbase^exp) +end + +function pyconvert_rule_sympy_mul(::Type{Symbolics.Num}, x) + if !pyisinstance(x,sp.Mul) + return PythonCall.pyconvert_unconverted() + end + mult = reduce(*,PythonCall.pyconvert.(Symbolics.Num,x.args)) + return PythonCall.pyconvert_return(mult) +end + +function pyconvert_rule_sympy_add(::Type{Symbolics.Num}, x) + if !pyisinstance(x,sp.Add) + return PythonCall.pyconvert_unconverted() + end + sum = reduce(+, PythonCall.pyconvert.(Symbolics.Num,x.args)) + return PythonCall.pyconvert_return(sum) +end + +function pyconvert_rule_sympy_derivative(::Type{Symbolics.Num}, x) + if !pyisinstance(x,sp.Derivative) + return PythonCall.pyconvert_unconverted() + end + variables = pyconvert.(Symbolics.Num,x.variables) + derivatives = prod(var -> Differential(var), variables) + expr = pyconvert(Symbolics.Num, x.expr) + return PythonCall.pyconvert_return(derivatives(expr)) +end + +function pyconvert_rule_sympy_function(::Type{Symbolics.Num}, x) + if !pyisinstance(x,sp.Function) + return PythonCall.pyconvert_unconverted() + end + nm = PythonCall.pygetattr(x, "func", nothing) + isnothing(nm) && return PythonCall.pyconvert_unconverted() # XXX + name = pyconvert(Symbol, nm) + args = pyconvert.(Symbolics.Num, x.args) + func = @variables $name(..) + return PythonCall.pyconvert_return(first(func)(args...)) +end + +function pyconvert_rule_sympy_equality(::Type{Symbolics.Equation}, x) + if !pyisinstance(x,sp.Equality) + return PythonCall.pyconvert_unconverted() + end + rhs = pyconvert(Symbolics.Num,x.rhs) + lhs = pyconvert(Symbolics.Num,x.lhs) + return PythonCall.pyconvert_return(rhs ~ lhs) +end + + +function __init__() + # added rules + # T = Symbolics.Num + PythonCall.pyconvert_add_rule("sympy.core.symbol:Symbol", Symbolics.Num, pyconvert_rule_sympy_symbol) + + PythonCall.pyconvert_add_rule("sympy.core.power:Pow", Symbolics.Num, pyconvert_rule_sympy_pow) + + PythonCall.pyconvert_add_rule("sympy.core.mul:Mul", Symbolics.Num, pyconvert_rule_sympy_mul) + + PythonCall.pyconvert_add_rule("sympy.core.add:Add", Symbolics.Num, pyconvert_rule_sympy_add) + + PythonCall.pyconvert_add_rule("sympy.core.function:Derivative", Symbolics.Num, pyconvert_rule_sympy_derivative) + + PythonCall.pyconvert_add_rule("sympy.core.function:Function", Symbolics.Num, pyconvert_rule_sympy_function) + + # T = Symbolics.Equation + PythonCall.pyconvert_add_rule("sympy.core.relational:Equality", Symbolics.Equation, pyconvert_rule_sympy_equality) + + # core numbers + add_pyconvert_rule(f, cls) = PythonCall.pyconvert_add_rule(cls, Symbolics.Num, f) + + add_pyconvert_rule("sympy.core.numbers:Pi") do T::Type{Symbolics.Num}, x + PythonCall.pyconvert_return(Symbolics.Num(pi)) + end + add_pyconvert_rule("sympy.core.numbers:Exp1") do T::Type{Symbolics.Num}, x + PythonCall.pyconvert_return(Symbolics.Num(ℯ)) + end + add_pyconvert_rule("sympy.core.numbers:Infinity") do T::Type{Symbolics.Num}, x + PythonCall.pyconvert_return(Symbolics.Num(Inf)) + end + #= complex numbers and Num needs some workaround + add_pyconvert_rule("sympy.core.numbers:ImaginaryUnit") do T::Type{Symbolics.Num}, x + PythonCall.pyconvert_return(Symbolics.Num(im)) + end + add_pyconvert_rule("sympy.core.numbers:ComplexInfinity") do T::Type{Symbolics.Num}, x + PythonCall.pyconvert_return(Symbolics.Num(Inf)) # errors: Complex(Inf,Inf))) + end + =# +end + +end diff --git a/src/introspection.jl b/src/introspection.jl index 627e4b0..f0cd70f 100644 --- a/src/introspection.jl +++ b/src/introspection.jl @@ -51,4 +51,36 @@ classname(x::T) where {T <: Union{Sym, Py}} = (cls = class(x); isnothing(cls) ? # Dict(u=>v for (u,v) in inspect.getmembers(x)) #end +## Map to get function object from type information +const funcname2function = ( + Add = +, + Sub = -, + Mul = *, + Div = /, + Pow = ^, + re = real, + im = imag, + Abs = abs, + Min = min, + Max = max, + Poly = identity, + Piecewise = error, # replace + Order = (as...) -> 0, + And = (as...) -> all(as), + Or = (as...) -> any(as), + Less = <, + LessThan = <=, + StrictLessThan = <, + Equal = ==, + Equality = ==, + Unequality = !==, + StrictGreaterThan = >, + GreaterThan = >=, + Greater = >, + conjugate = conj, + atan2 = atan, + TupleArg = tuple, + Heaviside = (a...) -> (a[1] < 0 ? 0 : (a[1] > 0 ? 1 : (length(a) > 1 ? a[2] : NaN))), +) + end diff --git a/test/runtests.jl b/test/runtests.jl index a4d56b2..d8d065d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -13,3 +13,7 @@ include("test-specialfuncs.jl") #include("test-physics.jl") #include("test-external-module.jl") include("test-latexify.jl") + +if VERSION >= v"1.9.0-" + @testset "Symbolics integration" begin include("symbolics-integration.jl") end +end diff --git a/test/symbolics-integration.jl b/test/symbolics-integration.jl new file mode 100644 index 0000000..5146356 --- /dev/null +++ b/test/symbolics-integration.jl @@ -0,0 +1,4 @@ +using SymPyPythonCall +import Symbolics + +@test isa(SymPyPythonCall.PythonCall.pyconvert(Symbolics.Num, sympy.sympify("x")), Symbolics.Num)