diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml index e47f93896..fd3ca336b 100644 --- a/.github/workflows/Documentation.yml +++ b/.github/workflows/Documentation.yml @@ -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' diff --git a/.github/workflows/Downstream.yml b/.github/workflows/Downstream.yml index cce627628..0298c8842 100644 --- a/.github/workflows/Downstream.yml +++ b/.github/workflows/Downstream.yml @@ -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 }} diff --git a/.github/workflows/benchmark_pr.yml b/.github/workflows/benchmark_pr.yml index ba2e5f7e9..dea6654fe 100644 --- a/.github/workflows/benchmark_pr.yml +++ b/.github/workflows/benchmark_pr.yml @@ -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" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df20a0a1a..2cfcbb974 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 0 - run: | diff --git a/docs/make.jl b/docs/make.jl index bb8eb61bd..a65e21531 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -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, diff --git a/docs/src/manual/rewrite.md b/docs/src/manual/rewrite.md index 56785957e..047bef71a 100644 --- a/docs/src/manual/rewrite.md +++ b/docs/src/manual/rewrite.md @@ -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 @@ -18,6 +18,9 @@ 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. @@ -25,49 +28,73 @@ The `@rule` macro takes a pair of patterns -- the _matcher_ and the _consequent_ `~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 @@ -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"; @@ -87,6 +115,12 @@ 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 ``` @@ -94,12 +128,16 @@ r = @rule ~x + ~~y::(ys->iseven(length(ys))) => "odd terms"; 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. @@ -107,7 +145,7 @@ although in case of `Number` it also works the same way with regular `@rule` sin ### 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 @@ -115,22 +153,31 @@ using SymbolicUtils 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. @@ -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) ``` diff --git a/test/runtests.jl b/test/runtests.jl index cc8acf554..4159b7d87 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -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")