Skip to content

Commit 4c97304

Browse files
authored
Merge pull request #17 from jverzani/extensions
work on extensions
2 parents 7dec2c0 + e7cf818 commit 4c97304

File tree

8 files changed

+245
-8
lines changed

8 files changed

+245
-8
lines changed

Project.toml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "SymPyPythonCall"
22
uuid = "bc8888f7-b21e-4b7c-a06a-5d9c9496438c"
33
authors = ["jverzani <jverzani@gmail.com> and contributors"]
4-
version = "0.1.0"
4+
version = "0.1.1"
55

66
[deps]
77
CommonEq = "3709ef60-1bee-4518-9f2f-acd86f176c50"
@@ -14,6 +14,14 @@ PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d"
1414
RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
1515
SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b"
1616

17+
[weakdeps]
18+
Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7"
19+
SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b"
20+
21+
[extensions]
22+
SymPyPythonCallSymbolicsExt = "Symbolics"
23+
SymPyPythonCallSymbolicUtilsExt = "SymbolicUtils"
24+
1725
[compat]
1826
julia = "1.6.1"
1927
CommonEq = "0.2"
@@ -26,6 +34,8 @@ SpecialFunctions = "0.8, 0.9, 0.10, 1.0, 2"
2634

2735
[extras]
2836
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
37+
Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7"
38+
SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b"
2939

3040
[targets]
31-
test = ["Test"]
41+
test = ["Symbolics", "Test"]

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,19 @@
66
[![Coverage](https://codecov.io/gh/jverzani/SymPyPythonCall.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/jverzani/SymPyPythonCall.jl)
77

88

9-
This is a start on what is needed to use `PythonCall` instead of `PyCall` for `SymPy.jl`.
10-
At the moment, the expectation is that *if* that change proves desirable, this would become `SymPy`.
9+
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).
1110

12-
For now, there are some small design decisions from `SymPy` reflected here:
11+
(The more established [SymPy.jl](https://github.com/JuliaPy/SymPy.jl) uses [PyCall.jl](https://github.com/JuliaPy/PyCall.jl).)
1312

14-
There would be a few deprecations:
13+
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`.
14+
15+
----
16+
17+
Though nearly the same as `SymPy.jl`, for now, there are some small design decisions differing from `SymPy`:
1518

1619
* `@vars` would be deprecated; use `@syms` only
1720

18-
* `elements` for sets would be removed (convert to a `Set` by default)
21+
* `elements` for sets is deprecated (conversion to a `Set` is the newdefault)
1922

2023
* `sympy.poly` *not* `sympy.Poly`
2124

docs/make.jl

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,18 @@ ENV["GKSwstype"] = "100"
55
using SymPyPythonCall
66
using Documenter
77

8-
makedocs(sitename="My Documentation")
8+
makedocs(
9+
sitename = "SymPyPythonCall",
10+
format = Documenter.HTML(),
11+
modules = [SymPyPythonCall]
12+
)
13+
14+
# Documenter can also automatically deploy documentation to gh-pages.
15+
# See "Hosting Documentation" and deploydocs() in the Documenter manual
16+
# for more information.
17+
deploydocs(
18+
repo = "github.com/jverzani/SymPyPythonCall.jl.git"
19+
)
920

1021

1122
#DocMeta.setdocmeta!(SymPyPythonCall, :DocTestSetup, :(using SymPyPythonCall); recursive=true)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
module SymPyPythonCallSymbolicUtilsExt
2+
3+
import SymPyPythonCall
4+
import SymbolicUtils
5+
6+
#==
7+
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.
8+
==#
9+
function SymbolicUtils.istree(x::SymPyPythonCall.SymbolicObject)
10+
!(convert(Bool, x.is_Atom))
11+
end
12+
13+
#==
14+
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.
15+
function TermInterface.exprhead(x::SymPyPythonCall.SymbolicObject)
16+
:call # this is not right
17+
end
18+
==#
19+
20+
#==
21+
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
22+
==#
23+
function SymbolicUtils.operation(x::SymPyPythonCall.SymbolicObject)
24+
@assert SymbolicUtils.istree(x)
25+
nm = Symbol(SymPyPythonCall.Introspection.funcname(x))
26+
27+
λ = get(SymPyPythonCall.Introspection.funcname2function, nm, nothing)
28+
if isnothing(λ)
29+
return getfield(Main, nm)
30+
else
31+
return λ
32+
end
33+
end
34+
35+
36+
#==
37+
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
38+
==#
39+
function SymbolicUtils.arguments(x::SymPyPythonCall.SymbolicObject)
40+
collect(SymPyPythonCall.Introspection.args(x))
41+
end
42+
43+
#==
44+
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.
45+
==#
46+
function SymbolicUtils.similarterm(t::SymPyPythonCall.SymbolicObject, f, args, symtype=nothing;
47+
metadata=nothing, exprhead=:call)
48+
f(args...) # default
49+
end
50+
51+
52+
end

ext/SymPyPythonCallSymbolicsExt.jl

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
module SymPyPythonCallSymbolicsExt
2+
3+
# from https://github.com/JuliaSymbolics/Symbolics.jl/pull/957/
4+
# by @jClugstor
5+
import SymPyPythonCall
6+
sp = SymPyPythonCall.sympy.py
7+
const PythonCall = SymPyPythonCall.PythonCall
8+
import PythonCall: pyconvert, pyimport, pyisinstance
9+
10+
import Symbolics
11+
import Symbolics: @variables
12+
13+
# rule functions
14+
function pyconvert_rule_sympy_symbolX(::Type{Symbolics.Num}, x)
15+
end
16+
function pyconvert_rule_sympy_symbol(::Type{Symbolics.Num}, x)
17+
if !pyisinstance(x,sp.Symbol)
18+
return PythonCall.pyconvert_unconverted()
19+
end
20+
name = PythonCall.pyconvert(Symbol,x.name)
21+
return PythonCall.pyconvert_return(Symbolics.variable(name))
22+
end
23+
24+
function pyconvert_rule_sympy_pow(::Type{Symbolics.Num}, x)
25+
if !pyisinstance(x,sp.Pow)
26+
return PythonCall.pyconvert_unconverted()
27+
end
28+
expbase = pyconvert(Symbolics.Num,x.base)
29+
exp = pyconvert(Symbolics.Num,x.exp)
30+
return PythonCall.pyconvert_return(expbase^exp)
31+
end
32+
33+
function pyconvert_rule_sympy_mul(::Type{Symbolics.Num}, x)
34+
if !pyisinstance(x,sp.Mul)
35+
return PythonCall.pyconvert_unconverted()
36+
end
37+
mult = reduce(*,PythonCall.pyconvert.(Symbolics.Num,x.args))
38+
return PythonCall.pyconvert_return(mult)
39+
end
40+
41+
function pyconvert_rule_sympy_add(::Type{Symbolics.Num}, x)
42+
if !pyisinstance(x,sp.Add)
43+
return PythonCall.pyconvert_unconverted()
44+
end
45+
sum = reduce(+, PythonCall.pyconvert.(Symbolics.Num,x.args))
46+
return PythonCall.pyconvert_return(sum)
47+
end
48+
49+
function pyconvert_rule_sympy_derivative(::Type{Symbolics.Num}, x)
50+
if !pyisinstance(x,sp.Derivative)
51+
return PythonCall.pyconvert_unconverted()
52+
end
53+
variables = pyconvert.(Symbolics.Num,x.variables)
54+
derivatives = prod(var -> Differential(var), variables)
55+
expr = pyconvert(Symbolics.Num, x.expr)
56+
return PythonCall.pyconvert_return(derivatives(expr))
57+
end
58+
59+
function pyconvert_rule_sympy_function(::Type{Symbolics.Num}, x)
60+
if !pyisinstance(x,sp.Function)
61+
return PythonCall.pyconvert_unconverted()
62+
end
63+
nm = PythonCall.pygetattr(x, "func", nothing)
64+
isnothing(nm) && return PythonCall.pyconvert_unconverted() # XXX
65+
name = pyconvert(Symbol, nm)
66+
args = pyconvert.(Symbolics.Num, x.args)
67+
func = @variables $name(..)
68+
return PythonCall.pyconvert_return(first(func)(args...))
69+
end
70+
71+
function pyconvert_rule_sympy_equality(::Type{Symbolics.Equation}, x)
72+
if !pyisinstance(x,sp.Equality)
73+
return PythonCall.pyconvert_unconverted()
74+
end
75+
rhs = pyconvert(Symbolics.Num,x.rhs)
76+
lhs = pyconvert(Symbolics.Num,x.lhs)
77+
return PythonCall.pyconvert_return(rhs ~ lhs)
78+
end
79+
80+
81+
function __init__()
82+
# added rules
83+
# T = Symbolics.Num
84+
PythonCall.pyconvert_add_rule("sympy.core.symbol:Symbol", Symbolics.Num, pyconvert_rule_sympy_symbol)
85+
86+
PythonCall.pyconvert_add_rule("sympy.core.power:Pow", Symbolics.Num, pyconvert_rule_sympy_pow)
87+
88+
PythonCall.pyconvert_add_rule("sympy.core.mul:Mul", Symbolics.Num, pyconvert_rule_sympy_mul)
89+
90+
PythonCall.pyconvert_add_rule("sympy.core.add:Add", Symbolics.Num, pyconvert_rule_sympy_add)
91+
92+
PythonCall.pyconvert_add_rule("sympy.core.function:Derivative", Symbolics.Num, pyconvert_rule_sympy_derivative)
93+
94+
PythonCall.pyconvert_add_rule("sympy.core.function:Function", Symbolics.Num, pyconvert_rule_sympy_function)
95+
96+
# T = Symbolics.Equation
97+
PythonCall.pyconvert_add_rule("sympy.core.relational:Equality", Symbolics.Equation, pyconvert_rule_sympy_equality)
98+
99+
# core numbers
100+
add_pyconvert_rule(f, cls) = PythonCall.pyconvert_add_rule(cls, Symbolics.Num, f)
101+
102+
add_pyconvert_rule("sympy.core.numbers:Pi") do T::Type{Symbolics.Num}, x
103+
PythonCall.pyconvert_return(Symbolics.Num(pi))
104+
end
105+
add_pyconvert_rule("sympy.core.numbers:Exp1") do T::Type{Symbolics.Num}, x
106+
PythonCall.pyconvert_return(Symbolics.Num(ℯ))
107+
end
108+
add_pyconvert_rule("sympy.core.numbers:Infinity") do T::Type{Symbolics.Num}, x
109+
PythonCall.pyconvert_return(Symbolics.Num(Inf))
110+
end
111+
#= complex numbers and Num needs some workaround
112+
add_pyconvert_rule("sympy.core.numbers:ImaginaryUnit") do T::Type{Symbolics.Num}, x
113+
PythonCall.pyconvert_return(Symbolics.Num(im))
114+
end
115+
add_pyconvert_rule("sympy.core.numbers:ComplexInfinity") do T::Type{Symbolics.Num}, x
116+
PythonCall.pyconvert_return(Symbolics.Num(Inf)) # errors: Complex(Inf,Inf)))
117+
end
118+
=#
119+
end
120+
121+
end

src/introspection.jl

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,36 @@ classname(x::T) where {T <: Union{Sym, Py}} = (cls = class(x); isnothing(cls) ?
5151
# Dict(u=>v for (u,v) in inspect.getmembers(x))
5252
#end
5353

54+
## Map to get function object from type information
55+
const funcname2function = (
56+
Add = +,
57+
Sub = -,
58+
Mul = *,
59+
Div = /,
60+
Pow = ^,
61+
re = real,
62+
im = imag,
63+
Abs = abs,
64+
Min = min,
65+
Max = max,
66+
Poly = identity,
67+
Piecewise = error, # replace
68+
Order = (as...) -> 0,
69+
And = (as...) -> all(as),
70+
Or = (as...) -> any(as),
71+
Less = <,
72+
LessThan = <=,
73+
StrictLessThan = <,
74+
Equal = ==,
75+
Equality = ==,
76+
Unequality = !==,
77+
StrictGreaterThan = >,
78+
GreaterThan = >=,
79+
Greater = >,
80+
conjugate = conj,
81+
atan2 = atan,
82+
TupleArg = tuple,
83+
Heaviside = (a...) -> (a[1] < 0 ? 0 : (a[1] > 0 ? 1 : (length(a) > 1 ? a[2] : NaN))),
84+
)
85+
5486
end

test/runtests.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ include("test-specialfuncs.jl")
1313
#include("test-physics.jl")
1414
#include("test-external-module.jl")
1515
include("test-latexify.jl")
16+
17+
if VERSION >= v"1.9.0-"
18+
@testset "Symbolics integration" begin include("symbolics-integration.jl") end
19+
end

test/symbolics-integration.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
using SymPyPythonCall
2+
import Symbolics
3+
4+
@test isa(SymPyPythonCall.PythonCall.pyconvert(Symbolics.Num, sympy.sympify("x")), Symbolics.Num)

0 commit comments

Comments
 (0)