Skip to content

Commit

Permalink
Merge pull request #808 from JuliaSymbolics/parsing
Browse files Browse the repository at this point in the history
Add a function to parse Julia expressions into symbolic expressions
  • Loading branch information
ChrisRackauckas authored Dec 22, 2022
2 parents cb7b7ae + a613c10 commit 5f0aed4
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ makedocs(
"manual/arrays.md",
"manual/build_function.md",
"manual/functions.md",
"manual/parsing.md",
"manual/io.md",
"manual/sparsity_detection.md",
"manual/types.md",
Expand Down
19 changes: 19 additions & 0 deletions docs/src/manual/parsing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Parsing Julia Expressions to Symbolic Expressions

Julia expressions such as `:(y - x)` are fundamentally different from symbolic
expressions as they do not have an algebra defined on them. Thus it can be
very helpful when building domain-specific languages (DSLs) and parsing files
to convert from Julia expressions to Symbolics.jl expressions for further
manipulation. Towards this end is the `parse_expr_to_symbolic` which performs
the parsing.

!!! warn
Take the limitations mentioned in the `parse_expr_to_symbolic` docstrings
seriously! Because Julia expressions contain no symbolic metadata, there
is limited information and thus the parsing requires heuristics in order to
work.

```@docs
parse_expr_to_symbolic
@parse_expr_to_symbolic
```
3 changes: 3 additions & 0 deletions src/Symbolics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ export solve_single_eq
export solve_system_eq
export lambertw

include("parsing.jl")
export parse_expr_to_symbolic

# Hacks to make wrappers "nicer"
const NumberTypes = Union{AbstractFloat,Integer,Complex{<:AbstractFloat},Complex{<:Integer}}
(::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."))
Expand Down
116 changes: 116 additions & 0 deletions src/parsing.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""
```julia
parse_expr_to_symbolic(ex, mod::Module)
```
Applies the `parse_expr_to_symbolic` function in the current module, i.e.
`parse_expr_to_symbolic(ex, mod)` where `mod` is the module of the function
caller.
## Arguments
* `ex`: the expression to parse
* `mod`: the module to apply the parsing in. See the limitations section for details
## Example
```julia
ex = :(y(t) ~ x(t))
parse_expr_to_symbolic(ex,Main) # gives the symbolic expression `y(t) ~ x(t)` in empty Main
# Now do a whole system
ex = [:(y ~ x)
:(y ~ -2x + 3 / z)
:(z ~ 2)]
eqs = parse_expr_to_symbolic.(ex, (Main,))
@variables x y z
ex = [y ~ x
y ~ -2x + 3 / z
z ~ 2]
all(isequal.(eqs,ex)) # true
```
## Limitations
### Symbolic-ness Tied to Environment Definitions
The parsing to a symbolic expression has to be able to recognize the difference between
functions, numbers, and globals defined within one's Julia environment and those that
are to be made symbolic. The way this functionality handles this problem is that it
does not define anything as symbolic that is already defined in the chosen `mod` module.
Thus for example, `f(x,y)` will have `f` as non-symbolic if the function `f` (named `f`)
is defined in `mod`, i.e. if `isdefined(mod,:f)` is true. When the symbol is defined, it
will be replaced by its value. Notably, this means that the parsing behavior changes
depending on the environment that it is applied.
For example:
```julia
parse_expr_to_symbolic(:(x - y),@__MODULE__) # x - y
x = 2.0
parse_expr_to_symbolic(:(x - y),@__MODULE__) # 2.0 - y
```
This is required in order to detect that standard functions like `-` are functions instead of
symbolic symbols. For safety, one should create anonymous modules or other sub-environments
to ensure no stray variables are defined.
### Metadata is Blank
Because all of the variables defined by the expressions are not defined with the standard
`@variables`, there is no metadata that is or can be associated with any of the generated
variables. Instead they all have blank metadata, but are defined in the `Real` domain.
This the variables which come out of this parsing may not evaluate as equal to a symbolic
variable defined elsewhere.
"""
function parse_expr_to_symbolic end

parse_expr_to_symbolic(x::Number, mod::Module) = x
function parse_expr_to_symbolic(x::Symbol, mod::Module)
if isdefined(mod, x)
getfield(mod, x)
else
(@variables $x)[1]
end
end
function parse_expr_to_symbolic(ex, mod::Module)
if ex.head == :call
if isdefined(mod, ex.args[1])
return getfield(mod,ex.args[1])(parse_expr_to_symbolic.(ex.args[2:end],(mod,))...)
else
x = parse_expr_to_symbolic(ex.args[1], mod)
ys = parse_expr_to_symbolic.(ex.args[2:end],(mod,))
return Term{Real}(x,[ys...])
end
end
end

"""
```julia
@parse_expr_to_symbolic ex
```
Applies the `parse_expr_to_symbolic` function in the current module, i.e.
`parse_expr_to_symbolic(ex, mod)` where `mod` is the module of the function
caller.
## Arguments
* `ex`: the expression to parse
## Example
```julia
ex = :(y(t) ~ x(t))
@parse_expr_to_symbolic ex # gives the symbolic expression `y(t) ~ x(t)`
```
## Limitations
The same limitations apply as for the function `parse_expr_to_symbolic`.
See its docstring for more details.
"""
macro parse_expr_to_symbolic(ex)
:(parse_expr_to_symbolic($ex, @__MODULE__))
end
22 changes: 22 additions & 0 deletions test/parsing.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Symbolics, Test

ex = [:(y ~ x)
:(y ~ -2x + 3 / z)
:(z ~ 2)]
eqs = parse_expr_to_symbolic.(ex, (Main,))

@variables x y z
ex = [y ~ x
y ~ -2x + 3 / z
z ~ 2]
@test all(isequal.(eqs,ex))

ex = [:(b(t) ~ a(t))
:(b(t) ~ -2a(t) + 3 / c(t))
:(c(t) ~ 2)]
eqs = parse_expr_to_symbolic.(ex, (Main,))
@variables t a(t) b(t) c(t)
ex = [b ~ a
b ~ -2a + 3 / c
c ~ 2]
@test_broken all(isequal.(eqs,ex))
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ if GROUP == "All" || GROUP == "Core"
@safetestset "Difference Test" begin include("difference.jl") end
@safetestset "Degree Test" begin include("degree.jl") end
@safetestset "Coeff Test" begin include("coeff.jl") end
@safetestset "Parsing Test" begin include("parsing.jl") end
@safetestset "Is Linear or Affine Test" begin include("islinear_affine.jl") end
@safetestset "Linear Solver Test" begin include("linear_solver.jl") end
@safetestset "Algebraic Solver Test" begin include("solver.jl") end
Expand Down

0 comments on commit 5f0aed4

Please sign in to comment.