Skip to content

Commit

Permalink
Merge pull request #652 from karlwessel/master
Browse files Browse the repository at this point in the history
activate doc tests for rewrite.md
  • Loading branch information
bowenszhu authored Sep 27, 2024
2 parents 835fb0d + e4a6baf commit 5bca5b9
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 29 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/Documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@latest
with:
version: '1'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/Downstream.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- {user: SciML, repo: ModelOrderReduction.jl, group: All}

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v1
with:
version: ${{ matrix.julia-version }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/benchmark_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v1
with:
version: "1"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: |
Expand Down
3 changes: 2 additions & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using Documenter, SymbolicUtils

include("pages.jl")
DocMeta.setdocmeta!(SymbolicUtils, :DocTestSetup, :(using SymbolicUtils); recursive=true)

makedocs(
sitename="SymbolicUtils.jl",
authors="Shashi Gowda",
modules=[SymbolicUtils],
clean=true,doctest=false,
clean=true, doctest=true,
warnonly=Documenter.except(
:doctest,
:linkcheck,
Expand Down
100 changes: 82 additions & 18 deletions docs/src/manual/rewrite.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Rewrite rules match and transform an expression. A rule is written using either

Here is a simple rewrite rule, that uses formula for the double angle of the sine function:

```julia:rewrite1
```jldoctest rewrite
using SymbolicUtils
@syms w z α::Real β::Real
Expand All @@ -18,56 +18,83 @@ using SymbolicUtils
r1 = @rule sin(2(~x)) => 2sin(~x)*cos(~x)
r1(sin(2z))
# output
2sin(z)*cos(z)
```

The `@rule` macro takes a pair of patterns -- the _matcher_ and the _consequent_ (`@rule matcher => consequent`). If an expression matches the matcher pattern, it is rewritten to the consequent pattern. `@rule` returns a callable object that applies the rule to an expression.

`~x` in the example is what is a **slot variable** named `x`. In a matcher pattern, slot variables are placeholders that match exactly one expression. When used on the consequent side, they stand in for the matched expression. If a slot variable appears twice in a matcher pattern, all corresponding matches must be equal (as tested by `Base.isequal` function). Hence this rule says: if you see something added to itself, make it twice of that thing, and works as such.

If you try to apply this rule to an expression with triple angle, it will return `nothing` -- this is the way a rule signifies failure to match.
```julia:rewrite2
```jldoctest rewrite
r1(sin(3z)) === nothing
# output
true
```

Slot variable (matcher) is not necessary a single variable

```julia:rewrite3
```jldoctest rewrite
r1(sin(2*(w-z)))
# output
2cos(w - z)*sin(w - z)
```

but it must be a single expression

```julia:rewrite4
```jldoctest rewrite
r1(sin(2*(w+z)*(α+β))) === nothing
# output
true
```

Rules are of course not limited to single slot variable

```julia:rewrite5
```jldoctest rewrite
r2 = @rule sin(~x + ~y) => sin(~x)*cos(~y) + cos(~x)*sin(~y);
r2(sin(α+β))
# output
sin(β)*cos(α) + cos(β)*sin(α)
```

If you want to match a variable number of subexpressions at once, you will need a **segment variable**. `~~xs` in the following example is a segment variable:

```julia:rewrite6
```jldoctest rewrite
@syms x y z
@rule(+(~~xs) => ~~xs)(x + y + z)
# output
3-element view(::Vector{Any}, 1:3) with eltype Any:
z
y
x
```

`~~xs` is a vector of subexpressions matched. You can use it to construct something more useful:

```julia:rewrite7
```jldoctest rewrite
r3 = @rule ~x * +(~~ys) => sum(map(y-> ~x * y, ~~ys));
r3(2 * (w+w+α+β))
# output
4w + 2α + 2β
```

Notice that the expression was autosimplified before application of the rule.

```julia:rewrite8
```jldoctest rewrite
2 * (w+w+α+β)
# output
2(2w + α + β)
```

### Predicates for matching
Expand All @@ -78,7 +105,8 @@ Similarly `~~x::g` is a way of attaching a predicate `g` to a segment variable.

For example,

```julia:pred1
```jldoctest pred
using SymbolicUtils
@syms a b c d
r = @rule ~x + ~~y::(ys->iseven(length(ys))) => "odd terms";
Expand All @@ -87,50 +115,69 @@ r = @rule ~x + ~~y::(ys->iseven(length(ys))) => "odd terms";
@show r(b + c + d)
@show r(b + c + b)
@show r(a + b)
# output
r(a + b + c + d) = nothing
r(b + c + d) = "odd terms"
r(b + c + b) = nothing
r(a + b) = nothing
```


### Associative-Commutative Rules

Given an expression `f(x, f(y, z, u), v, w)`, a `f` is said to be associative if the expression is equivalent to `f(x, y, z, u, v, w)` and commutative if the order of arguments does not matter. SymbolicUtils has a special `@acrule` macro meant for rules on functions which are associate and commutative such as addition and multiplication of real and complex numbers.

```julia:acr
```jldoctest acr
using SymbolicUtils
@syms x y z
acr = @acrule((~a)^(~x) * (~a)^(~y) => (~a)^(~x + ~y))
acr(x^y * x^z)
# output
x^(y + z)
```

although in case of `Number` it also works the same way with regular `@rule` since autosimplification orders and applies associativity and commutativity to the expression.

### Example of applying the rules to simplify expression

Consider expression `(cos(x) + sin(x))^2` that we would like simplify by applying some trigonometric rules. First, we need rule to expand square of `cos(x) + sin(x)`. First we try the simplest rule to expand square of the sum and try it on simple expression
```julia:rewrite9
```jldoctest rewriteex
using SymbolicUtils
@syms x::Real y::Real
sqexpand = @rule (~x + ~y)^2 => (~x)^2 + (~y)^2 + 2 * ~x * ~y
sqexpand((cos(x) + sin(x))^2)
# output
sin(x)^2 + 2sin(x)*cos(x) + cos(x)^2
```

It works. This can be further simplified using Pythagorean identity and check it

```julia:rewrite10
```jldoctest rewriteex
pyid = @rule sin(~x)^2 + cos(~x)^2 => 1
pyid(cos(x)^2 + sin(x)^2) === nothing
# output
true
```

Why does it return `nothing`? If we look at the rule, we see that the order of `sin(x)` and `cos(x)` is different. Therefore, in order to work, the rule needs to be associative-commutative.

```julia:rewrite11
```jldoctest rewriteex
acpyid = @acrule sin(~x)^2 + cos(~x)^2 => 1
acpyid(cos(x)^2 + sin(x)^2 + 2cos(x)*sin(x))
# output
1 + 2sin(x)*cos(x)
```

It has been some work. Fortunately rules may be [chained together](#chaining rewriters) into more sophisticated rewriters to avoid manual application of the rules.
Expand Down Expand Up @@ -175,47 +222,64 @@ Several rules may be chained to give chain of rules. Chain is an array of rules

To check that, we will combine rules from [previous example](#example of applying the rules to simplify expression) into a chain

```julia:composing1
```jldoctest composing
using SymbolicUtils
using SymbolicUtils.Rewriters
@syms x
sqexpand = @rule (~x + ~y)^2 => (~x)^2 + (~y)^2 + 2 * ~x * ~y
acpyid = @acrule sin(~x)^2 + cos(~x)^2 => 1
csa = Chain([sqexpand, acpyid])
csa((cos(x) + sin(x))^2)
# output
1 + 2sin(x)*cos(x)
```

Important feature of `Chain` is that it returns the expression instead of `nothing` if it doesn't change the expression

```julia:composing2
```jldoctest composing
Chain([@acrule sin(~x)^2 + cos(~x)^2 => 1])((cos(x) + sin(x))^2)
# output
(sin(x) + cos(x))^2
```

it's important to notice, that chain is ordered, so if rules are in different order it wouldn't work the same as in earlier example

```julia:composing3
```jldoctest composing
cas = Chain([acpyid, sqexpand])
cas((cos(x) + sin(x))^2)
# output
sin(x)^2 + 2sin(x)*cos(x) + cos(x)^2
```
since Pythagorean identity is applied before square expansion, so it is unable to match squares of sine and cosine.

One way to circumvent the problem of order of applying rules in chain is to use `RestartedChain`

```julia:composing4
```jldoctest composing
using SymbolicUtils.Rewriters: RestartedChain
rcas = RestartedChain([acpyid, sqexpand])
rcas((cos(x) + sin(x))^2)
# output
1 + 2sin(x)*cos(x)
```

It restarts the chain after each successful application of a rule, so after `sqexpand` is hit it (re)starts again and successfully applies `acpyid` to resulting expression.

You can also use `Fixpoint` to apply the rules until there are no changes.

```julia:composing5
```jldoctest composing
Fixpoint(cas)((cos(x) + sin(x))^2)
# output
1 + 2sin(x)*cos(x)
```
7 changes: 1 addition & 6 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,7 @@ DocMeta.setdocmeta!(
recursive=true
)

# Only test one Julia version to avoid differences due to changes in printing.
if v"1.6" VERSION < v"1.7-beta3.0"
doctest(SymbolicUtils)
else
@warn "Skipping doctests"
end
doctest(SymbolicUtils)
SymbolicUtils.show_simplified[] = false

include("utils.jl")
Expand Down

0 comments on commit 5bca5b9

Please sign in to comment.