Skip to content

Commit b2f0870

Browse files
authored
Merge pull request #231 from JuliaSymbolics/s/rpf
WIP rational polynomial form
2 parents 5fe621f + ff9be21 commit b2f0870

File tree

18 files changed

+552
-227
lines changed

18 files changed

+552
-227
lines changed

Project.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
name = "SymbolicUtils"
22
uuid = "d1185830-fcd6-423d-90d6-eec64667417b"
33
authors = ["Shashi Gowda"]
4-
version = "0.13.4"
4+
version = "0.14.0"
55

66
[deps]
77
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
8+
Bijections = "e2ed5e7c-b2de-5872-ae92-c73ca462fb04"
89
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
910
Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa"
1011
ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9"

page/_layout/sidebar.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ <h1 class="sidebar-title"><a href="/">SymbolicUtils<span style="opacity:0.4">.jl
2828
</style>
2929
<nav class="sidebar-nav" style="opacity: 0.9">
3030
<a class="sidebar-nav-item {{ispage index.html}}active{{end}}" href="/">Getting Started</a>
31+
<a class="sidebar-nav-item {{ispage representation.html}}active{{end}}" href="/representation/">Term representation</a>
3132
<a class="sidebar-nav-item {{ispage rewrite/*}}active{{end}}" href="/rewrite/">Term Rewriting</a>
3233
<a class="sidebar-nav-item {{ispage interface/*}}active{{end}}" href="/interface/">Interfacing</a>
3334
<a class="sidebar-nav-item {{ispage codegen/*}}active{{end}}" href="/codegen/">Code generation</a>

page/index.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ where appropriate -->
2020
- Fast expressions
2121
- A [rule-based rewriting language](/rewrite/#rule-based_rewriting).
2222
- A [combinator library](/rewrite/#composing_rewriters) for making rewriters.
23+
- [Efficient representation](/representation/) of numeric expressions
2324
- Type promotion:
2425
- Symbols (`Sym`s) carry type information. ([read more](#creating_symbolic_expressions))
2526
- Compound expressions composed of `Sym`s propagate type information. ([read more](#expression_interface))
@@ -131,7 +132,7 @@ g(2//5, g(1, β))
131132

132133
## Expression interface
133134

134-
Symbolic expressions are of type `Term{T}`, `Add{T}`, `Mul{T}` or `Pow{T}` and denote some function call where one or more arguments are themselves such expressions or `Sym`s.
135+
Symbolic expressions are of type `Term{T}`, `Add{T}`, `Mul{T}`, `Pow{T}` or `Div{T}` and denote some function call where one or more arguments are themselves such expressions or `Sym`s. See more about the representation [here](/representation/).
135136

136137
All the expression types support the following:
137138

@@ -155,8 +156,10 @@ The rules with which the canonical form of `Symbolic{<:Number}` terms are constr
155156

156157
- `0 + x`, `1 * x` and `x^1` always gives `x`
157158
- `0 * x` always gives `0` and `x ^ 0` gives `1`
158-
- `-x`, `1/x` and `x\1` get transformed into `(-1)*x`, `x^(-1)` and `x^(-1)`.
159+
- `-x` becomes `(-1)*x`
159160
- commutativity and associativity over `+` and `*` are assumed. Re-ordering of terms will be done under a [total order](https://github.com/JuliaSymbolics/SymbolicUtils.jl/blob/master/src/ordering.jl)
161+
- `p/q * x` or `x * p/q` results in `(p*x)/q`
162+
- `p/q * x/y` results in `(p*x)/(q*y)`
160163
- `x + ... + x` will be fused into `n*x` with type `Mul`
161164
- `x * ... * x` will be fused into `x^n` with type `Pow`
162165
- sum of `Add`'s are fused

page/representation.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
## Term representation and simplification
2+
3+
### Preliminary representation
4+
5+
Expressions are stored as a tree where the nodes store their children in some efficient data structure. A node `n` is a non-leaf node `istree(n)` is true. For such nodes, `gethead` and `getargs` must be defined. These return the head and args of the term represented by the subtree `n`.
6+
7+
A generic term is represented with the `Term` type. It simply holds the function `f` being called and a vector of arguments in the order they were passed to `f`. The operators `+` (and `-`), `*`, `/` and `^` create terms with special storage.
8+
9+
Linear combinations such as $\alpha_1 x_1 + \alpha_2 x_2 +...+ \alpha_n x_n$
10+
are represented by `Add(Dict(x_1 => \alpha_1, x_2 => \alpha_2, ..., x_n => \alpha_n))`. Now $x_n$ may themselves be other types mentioned in this section, but may not be an `Add`. When an `Add` is added to an `Add`, we merge their dictionaries and adding up matching coefficients.
11+
12+
Similarly, $x_1^{m_1}x_2^{m_2}\ellipsisx_{m_n}$ is represented by
13+
`Mul(Dict(x_1 => m_1, x_2 => m_2,..., x_n => m_n))`. $x_i$ may not themselves be `Mul`, multiplying a Mul with another Mul returns a flattened Mul.
14+
15+
$p / q$ is represented by `Div(p, q)`. The result of `*` on `Div` is maintainted as a `Div`. For example, `Div(p_1, q_1) * Div(p_2, q_2)` results in `Div(p_1 * p_2, q_1 * q_2)` and so on. The effect is, in `Div(p, q)`, `p` or `q` or, if they are Mul, any of their multiplicands is not a Div. So `Mul`s must always be nested inside a `Div` and can never show up immediately wrapping it.
16+
17+
18+
### Polynomial representation
19+
20+
Packages like DynamicPolynomials.jl provide even more efficient representations for polynomials. They also have efficient operations such as multi-variate polynomial GCD. However, DynamicPolynomials can only represent flat polynomials. For example, `(x-3)*(x+5)` can only be represented as `(x^2) + 15 - 8x`. Moreover, DynamicPolynomials does not have ways to represent generic Terms such as `sin(x-y)` in the tree.
21+
22+
To reconcile these differences while being able to use the efficient representation of DynamicPolynomials we have the `PolyForm` type. This type holds a polynomial and the mappings necessary to present them as a SymbolicUtils expression. The mappings constructed for the conversion are 1) a bijection from DynamicPolynomials PolyVar type to a Symbolics `Sym`, and 2) a mapping from `Sym`s to non-polynomial terms that the `Sym`s stand-in for. These terms may themselves contain PolyForm if there are polynomials inside them. The mappings are transiently global, that is, when all references to the mappings go out of scope, they are released and re-created.
23+
24+
```julia
25+
julia> @syms x y
26+
(x, y)
27+
28+
julia> PolyForm((x-3)*(y-5))
29+
x*y + 15 - 5x - 3y
30+
```
31+
32+
Further, generic terms such as `sin` or `cos` terms are replaced with a symbol when representing the polynomial.
33+
34+
```julia
35+
julia> p = PolyForm((sin(x) + cos(x))^2)
36+
(cos(x)^2) + 2cos(x)*sin(x) + (sin(x)^2)
37+
38+
julia> p.p # this is the actual DynamicPolynomial stored
39+
cos_3658410937268741549² + 2cos_3658410937268741549sin_10720964503106793468 + sin_10720964503106793468²
40+
```
41+
42+
By default, polynomials inside generic terms are not also converted to PolyForm. For example,
43+
44+
```julia
45+
julia> PolyForm(sin((x-3)*(y-5)))
46+
sin((x - 3)*(y - 5))
47+
```
48+
But you can pass in the `recurse=true` keyword argument to get this going.
49+
50+
```julia
51+
julia> PolyForm(sin((x-3)*(y-5)), recurse=true)
52+
sin(x*y + 15 - 5x - 3y)
53+
```
54+
55+
Polynomials are constructed by first turning symbols and generic terms into Polynomial variables and then applying the `+`, `*`, `^` operations on these variables. You can control which of these operations are actually constructed with the `Fs` keyword argument. It is a Union type of the functions to apply. For example, let's say you want to turn terms into polynomials by only applying the `+` and `^` operations, and want to preserve `*` operations as-is, you could pass in `Fs=Union{typeof(+), typeof(^)}`
56+
57+
```julia
58+
julia> PolyForm((x+y)^2*(x-y), Fs=Union{typeof(+), typeof(^)}, recurse=true)
59+
((x - (y))*((x^2) + 2x*y + (y^2)))
60+
```
61+
62+
Note that in this case `recurse=true` was necessary as otherwise the polynomialization would not descend into the `*` operation as it is now considered a generic operation.
63+
64+
### Simplifying fractions
65+
66+
`simplify_fractions(expr)` recurses through `expr` finding `Div`s and simplifying them using polynomial divison.
67+
68+
First the factors of the numerators and the denominators are converted into PolyForm objects, then numerators and denominators are divided by their respective pairwise GCDs. The conversion of the numerator and denominator into PolyForm is set up so that `simplify_fractions` does not result in increase in the expression size due to polynomial expansion. Specifically, the factors are individually converted into PolyForm objects, and any powers of polynomial is not expanded, but the divison process repeatedly divides them as many times as the power.
69+
70+
71+
```julia
72+
julia> simplify_fractions((x*y+5x+3y+15)/((x+3)*(x-4)))
73+
(5.0 + y) / (x - 4)
74+
75+
julia> simplify_fractions((x*y+5x+3y+15)^2/((x+3)*(x-4)))
76+
((5.0 + y)*(15 + 5x + x*y + 3y)) / (x - 4)
77+
78+
julia> simplify_fractions(3/(x+3) + x/(x+3))
79+
1
80+
81+
julia> simplify_fractions(sin(x)/cos(x) + cos(x)/sin(x))
82+
(cos(x)^2 + sin(x)^2) / (cos(x)*sin(x))
83+
84+
julia> simplify(ans)
85+
1 / (cos(x)*sin(x))
86+
```

src/SymbolicUtils.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const MP = MultivariatePolynomials
4545
import DynamicPolynomials
4646
export expand
4747
include("abstractalgebra.jl")
48+
include("polyform.jl")
4849

4950
# Term ordering
5051
include("ordering.jl")

src/abstractalgebra.jl

Lines changed: 0 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -1,181 +0,0 @@
1-
# Polynomial Normal Form
2-
3-
"""
4-
labels!(dict, t)
5-
6-
Find all terms that are not + and * and replace them
7-
with a symbol, store the symbol => term mapping in `dict`.
8-
"""
9-
function labels! end
10-
11-
# Turn a Term into a multivariate polynomial
12-
function labels!(dicts, t::Sym, variable_type::Type)
13-
sym2term, term2sym = dicts
14-
if !haskey(term2sym, t)
15-
sym2term[t] = t
16-
term2sym[t] = t
17-
end
18-
return t
19-
end
20-
21-
function labels!(dicts, t, variable_type::Type)
22-
if t isa Number
23-
return t
24-
elseif istree(t) && (operation(t) == (*) || operation(t) == (+) || operation(t) == (-))
25-
tt = arguments(t)
26-
return similarterm(t, operation(t), map(x->labels!(dicts, x, variable_type), tt), symtype(t))
27-
elseif istree(t) && operation(t) == (^) && length(arguments(t)) > 1 && isnonnegint(arguments(t)[2])
28-
return similarterm(t, operation(t), map(x->labels!(dicts, x, variable_type), arguments(t)), symtype(t))
29-
else
30-
sym2term, term2sym = dicts
31-
if haskey(term2sym, t)
32-
return term2sym[t]
33-
end
34-
if istree(t)
35-
tt = arguments(t)
36-
sym = Sym{symtype(t)}(gensym(nameof(operation(t))))
37-
dicts2 = _dicts(dicts[2])
38-
sym2term[sym] = similarterm(t, operation(t),
39-
map(x->to_mpoly(x, variable_type, dicts)[1], arguments(t)),
40-
symtype(t))
41-
else
42-
sym = Sym{symtype(t)}(gensym("literal"))
43-
sym2term[sym] = t
44-
end
45-
46-
term2sym[t] = sym
47-
48-
return sym
49-
end
50-
end
51-
52-
ismpoly(x) = x isa MP.AbstractPolynomialLike || x isa Number
53-
isnonnegint(x) = x isa Integer && x >= 0
54-
55-
_dicts(t2s=OrderedDict{Any, Sym}()) = (OrderedDict{Sym, Any}(), t2s)
56-
57-
let
58-
mpoly_preprocess = [@rule(identity(~x) => ~x)
59-
@rule(zero(~x) => 0)
60-
@rule(one(~x) => 1)]
61-
62-
simterm(x, f, args;metadata=nothing) = similarterm(x,f,args, symtype(x); metadata=metadata)
63-
mpoly_rules = [@rule(~x::ismpoly - ~y::ismpoly => ~x + -1 * (~y))
64-
@rule(-(~x) => -1 * ~x)
65-
@acrule(~x::ismpoly + ~y::ismpoly => ~x + ~y)
66-
@rule(+(~x) => ~x)
67-
@acrule(~x::ismpoly * ~y::ismpoly => ~x * ~y)
68-
@rule(*(~x) => ~x)
69-
@rule((~x::ismpoly)^(~a::isnonnegint) => (~x)^(~a))]
70-
global const MPOLY_CLEANUP = Fixpoint(Postwalk(PassThrough(RestartedChain(mpoly_preprocess)), similarterm=simterm))
71-
MPOLY_MAKER = Fixpoint(Postwalk(PassThrough(RestartedChain(mpoly_rules)), similarterm=simterm))
72-
73-
global to_mpoly
74-
function to_mpoly(t, variable_type::Type=DynamicPolynomials.PolyVar{true}, dicts=_dicts())
75-
# term2sym is only used to assign the same
76-
# symbol for the same term -- in other words,
77-
# it does common subexpression elimination
78-
t = MPOLY_CLEANUP(t)
79-
sym2term, term2sym = dicts
80-
labeled = labels!((sym2term, term2sym), t, variable_type)
81-
82-
if isempty(sym2term)
83-
return MPOLY_MAKER(labeled), Dict{Sym,Any}()
84-
end
85-
86-
ks = sort(collect(keys(sym2term)), lt=<ₑ)
87-
vars = MP.similarvariable.(variable_type, nameof.(ks))
88-
89-
replace_with_poly = Dict{Sym,eltype(vars)}(zip(ks, vars))
90-
t_poly = substitute(labeled, replace_with_poly, fold=false)
91-
MPOLY_MAKER(t_poly), sym2term
92-
end
93-
end
94-
95-
function to_term(reference, x, dict)
96-
syms = Dict(zip(string.(nameof.(keys(dict))), keys(dict)))
97-
dict = copy(dict)
98-
for (k, v) in dict
99-
dict[k] = _to_term(reference, v, dict, syms)
100-
end
101-
return _to_term(reference, x, dict, syms)
102-
#return substitute(t, dict, fold=false)
103-
end
104-
105-
_to_term(reference, x::Number, dict, syms) = x
106-
_to_term(reference, var::MP.AbstractVariable, dict, syms) = substitute(syms[MP.name(var)], dict, fold=false)
107-
function _to_term(reference, mono::MP.AbstractMonomialLike, dict, syms)
108-
monics = [
109-
begin
110-
t = _to_term(reference, var, dict, syms)
111-
exp == 1 ? t : t^exp
112-
end
113-
for (var, exp) in MP.powers(mono) if !iszero(exp)
114-
]
115-
if length(monics) == 1
116-
return monics[1]
117-
elseif isempty(monics)
118-
return 1
119-
else
120-
return similarterm(reference, *, monics, symtype(reference))
121-
end
122-
end
123-
124-
function _to_term(reference, term::MP.AbstractTermLike, dict, syms)
125-
coef = MP.coefficient(term)
126-
mono = _to_term(reference, MP.monomial(term), dict, syms)
127-
if isone(coef)
128-
return mono
129-
else
130-
return MP.coefficient(term) * mono
131-
end
132-
end
133-
134-
function _to_term(reference, x::MP.AbstractPolynomialLike, dict, syms)
135-
if MP.nterms(x) == 0
136-
return 0
137-
elseif MP.nterms(x) == 1
138-
return _to_term(reference, first(MP.terms(x)), dict, syms)
139-
else
140-
terms = map(MP.terms(x)) do term
141-
_to_term(reference, term, dict, syms)
142-
end
143-
return similarterm(reference, +, terms, symtype(reference))
144-
end
145-
end
146-
147-
function _to_term(reference, x, dict, vars)
148-
if istree(x)
149-
t = similarterm(x, operation(x), _to_term.((reference,), arguments(x), (dict,), (vars,)), symtype(x))
150-
else
151-
if haskey(dict, x)
152-
return dict[x]
153-
else
154-
return x
155-
end
156-
end
157-
end
158-
159-
<(a::MP.AbstractPolynomialLike, b::MP.AbstractPolynomialLike) = false
160-
161-
"""
162-
expand(expr, variable_type::Type=DynamicPolynomials.PolyVar{true})
163-
164-
Expand expressions by distributing multiplication over addition, e.g.,
165-
`a*(b+c)` becomes `ab+ac`.
166-
167-
`expand` uses replace symbols and non-algebraic expressions by variables of type
168-
`variable_type` to compute the distribution using a specialized sparse
169-
multivariate polynomials implementation.
170-
`variable_type` can be any subtype of `MultivariatePolynomials.AbstractVariable`.
171-
"""
172-
function expand(expr, variable_type::Type=DynamicPolynomials.PolyVar{true})
173-
to_term(expr, to_mpoly(expr, variable_type)...)
174-
end
175-
176-
## Hack to fix https://github.com/JuliaAlgebra/MultivariatePolynomials.jl/issues/169
177-
178-
Base.promote_rule(::Type{S}, ::Type{T}) where {S<:Symbolic, T<:MP.AbstractPolynomialLike}= Any
179-
Base.promote_rule(::Type{T}, ::Type{S}) where {S<:Symbolic, T<:MP.AbstractPolynomialLike}= Any
180-
181-
Base.@deprecate polynormalize(x) expand(x)

src/methods.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ end
8080
@number_methods(Add, term(f, a), term(f, a, b), skipbasics)
8181
@number_methods(Mul, term(f, a), term(f, a, b), skipbasics)
8282
@number_methods(Pow, term(f, a), term(f, a, b), skipbasics)
83+
@number_methods(Div, term(f, a), term(f, a, b), skipbasics)
8384

8485
for f in diadic
8586
@eval promote_symtype(::$(typeof(f)),

0 commit comments

Comments
 (0)