Skip to content

Commit 5f0aed4

Browse files
Merge pull request #808 from JuliaSymbolics/parsing
Add a function to parse Julia expressions into symbolic expressions
2 parents cb7b7ae + a613c10 commit 5f0aed4

File tree

6 files changed

+162
-0
lines changed

6 files changed

+162
-0
lines changed

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ makedocs(
4949
"manual/arrays.md",
5050
"manual/build_function.md",
5151
"manual/functions.md",
52+
"manual/parsing.md",
5253
"manual/io.md",
5354
"manual/sparsity_detection.md",
5455
"manual/types.md",

docs/src/manual/parsing.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Parsing Julia Expressions to Symbolic Expressions
2+
3+
Julia expressions such as `:(y - x)` are fundamentally different from symbolic
4+
expressions as they do not have an algebra defined on them. Thus it can be
5+
very helpful when building domain-specific languages (DSLs) and parsing files
6+
to convert from Julia expressions to Symbolics.jl expressions for further
7+
manipulation. Towards this end is the `parse_expr_to_symbolic` which performs
8+
the parsing.
9+
10+
!!! warn
11+
Take the limitations mentioned in the `parse_expr_to_symbolic` docstrings
12+
seriously! Because Julia expressions contain no symbolic metadata, there
13+
is limited information and thus the parsing requires heuristics in order to
14+
work.
15+
16+
```@docs
17+
parse_expr_to_symbolic
18+
@parse_expr_to_symbolic
19+
```

src/Symbolics.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ export solve_single_eq
142142
export solve_system_eq
143143
export lambertw
144144

145+
include("parsing.jl")
146+
export parse_expr_to_symbolic
147+
145148
# Hacks to make wrappers "nicer"
146149
const NumberTypes = Union{AbstractFloat,Integer,Complex{<:AbstractFloat},Complex{<:Integer}}
147150
(::Type{T})(x::SymbolicUtils.Symbolic) where {T<:NumberTypes} = throw(ArgumentError("Cannot convert Sym to $T since Sym is symbolic and $T is concrete. Use `substitute` to replace the symbolic unwraps."))

src/parsing.jl

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""
2+
```julia
3+
parse_expr_to_symbolic(ex, mod::Module)
4+
```
5+
6+
Applies the `parse_expr_to_symbolic` function in the current module, i.e.
7+
`parse_expr_to_symbolic(ex, mod)` where `mod` is the module of the function
8+
caller.
9+
10+
## Arguments
11+
12+
* `ex`: the expression to parse
13+
* `mod`: the module to apply the parsing in. See the limitations section for details
14+
15+
## Example
16+
17+
```julia
18+
ex = :(y(t) ~ x(t))
19+
parse_expr_to_symbolic(ex,Main) # gives the symbolic expression `y(t) ~ x(t)` in empty Main
20+
21+
# Now do a whole system
22+
23+
ex = [:(y ~ x)
24+
:(y ~ -2x + 3 / z)
25+
:(z ~ 2)]
26+
eqs = parse_expr_to_symbolic.(ex, (Main,))
27+
28+
@variables x y z
29+
ex = [y ~ x
30+
y ~ -2x + 3 / z
31+
z ~ 2]
32+
all(isequal.(eqs,ex)) # true
33+
```
34+
## Limitations
35+
36+
### Symbolic-ness Tied to Environment Definitions
37+
38+
The parsing to a symbolic expression has to be able to recognize the difference between
39+
functions, numbers, and globals defined within one's Julia environment and those that
40+
are to be made symbolic. The way this functionality handles this problem is that it
41+
does not define anything as symbolic that is already defined in the chosen `mod` module.
42+
Thus for example, `f(x,y)` will have `f` as non-symbolic if the function `f` (named `f`)
43+
is defined in `mod`, i.e. if `isdefined(mod,:f)` is true. When the symbol is defined, it
44+
will be replaced by its value. Notably, this means that the parsing behavior changes
45+
depending on the environment that it is applied.
46+
47+
For example:
48+
49+
```julia
50+
parse_expr_to_symbolic(:(x - y),@__MODULE__) # x - y
51+
x = 2.0
52+
parse_expr_to_symbolic(:(x - y),@__MODULE__) # 2.0 - y
53+
```
54+
55+
This is required in order to detect that standard functions like `-` are functions instead of
56+
symbolic symbols. For safety, one should create anonymous modules or other sub-environments
57+
to ensure no stray variables are defined.
58+
59+
### Metadata is Blank
60+
61+
Because all of the variables defined by the expressions are not defined with the standard
62+
`@variables`, there is no metadata that is or can be associated with any of the generated
63+
variables. Instead they all have blank metadata, but are defined in the `Real` domain.
64+
This the variables which come out of this parsing may not evaluate as equal to a symbolic
65+
variable defined elsewhere.
66+
"""
67+
function parse_expr_to_symbolic end
68+
69+
parse_expr_to_symbolic(x::Number, mod::Module) = x
70+
function parse_expr_to_symbolic(x::Symbol, mod::Module)
71+
if isdefined(mod, x)
72+
getfield(mod, x)
73+
else
74+
(@variables $x)[1]
75+
end
76+
end
77+
function parse_expr_to_symbolic(ex, mod::Module)
78+
if ex.head == :call
79+
if isdefined(mod, ex.args[1])
80+
return getfield(mod,ex.args[1])(parse_expr_to_symbolic.(ex.args[2:end],(mod,))...)
81+
else
82+
x = parse_expr_to_symbolic(ex.args[1], mod)
83+
ys = parse_expr_to_symbolic.(ex.args[2:end],(mod,))
84+
return Term{Real}(x,[ys...])
85+
end
86+
end
87+
end
88+
89+
"""
90+
```julia
91+
@parse_expr_to_symbolic ex
92+
```
93+
94+
Applies the `parse_expr_to_symbolic` function in the current module, i.e.
95+
`parse_expr_to_symbolic(ex, mod)` where `mod` is the module of the function
96+
caller.
97+
98+
## Arguments
99+
100+
* `ex`: the expression to parse
101+
102+
## Example
103+
104+
```julia
105+
ex = :(y(t) ~ x(t))
106+
@parse_expr_to_symbolic ex # gives the symbolic expression `y(t) ~ x(t)`
107+
```
108+
109+
## Limitations
110+
111+
The same limitations apply as for the function `parse_expr_to_symbolic`.
112+
See its docstring for more details.
113+
"""
114+
macro parse_expr_to_symbolic(ex)
115+
:(parse_expr_to_symbolic($ex, @__MODULE__))
116+
end

test/parsing.jl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Symbolics, Test
2+
3+
ex = [:(y ~ x)
4+
:(y ~ -2x + 3 / z)
5+
:(z ~ 2)]
6+
eqs = parse_expr_to_symbolic.(ex, (Main,))
7+
8+
@variables x y z
9+
ex = [y ~ x
10+
y ~ -2x + 3 / z
11+
z ~ 2]
12+
@test all(isequal.(eqs,ex))
13+
14+
ex = [:(b(t) ~ a(t))
15+
:(b(t) ~ -2a(t) + 3 / c(t))
16+
:(c(t) ~ 2)]
17+
eqs = parse_expr_to_symbolic.(ex, (Main,))
18+
@variables t a(t) b(t) c(t)
19+
ex = [b ~ a
20+
b ~ -2a + 3 / c
21+
c ~ 2]
22+
@test_broken all(isequal.(eqs,ex))

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ if GROUP == "All" || GROUP == "Core"
2828
@safetestset "Difference Test" begin include("difference.jl") end
2929
@safetestset "Degree Test" begin include("degree.jl") end
3030
@safetestset "Coeff Test" begin include("coeff.jl") end
31+
@safetestset "Parsing Test" begin include("parsing.jl") end
3132
@safetestset "Is Linear or Affine Test" begin include("islinear_affine.jl") end
3233
@safetestset "Linear Solver Test" begin include("linear_solver.jl") end
3334
@safetestset "Algebraic Solver Test" begin include("solver.jl") end

0 commit comments

Comments
 (0)