I did not manage to find an elegant solution, but you can simplify the expression using some custom rules.
There seem to be two problems.
-
Symbolics.jl does not cancel out common Rational
factors.
julia> @variables x y z;
julia> simplify((2//1 * x + 4//1 * y) / (6//1 * x + 8//1 * z))
((2//1)*x + (4//1)*y) / ((6//1)*x + (8//1)*z)
julia> simplify((2 * x + 4 * y) / (6 * x + 8 * z))
(x + 2y) / (3x + 4z)
This seems like a bug or just something that still needs to be implemented. In any case, in our situation all Rational
s in U_D_sol
have denominator 1
, so we can ‘convert’ them to Int
s, as a workaround.
rdiv1 = @rule ~x => (~x).num where (~x isa Rational && (~x).den == 1)
-
Mathematica gives a nice factorised result, but this is not the case for Symbolics.jl. In my opinion this is not a fault of the package, but simply a matter of taste with respect to what is considered ‘simplified’. If we want more factorisation, we will have to add factorisation rules. This turns out to be quite difficult. For example, I struggled to come up with a rule which simplifies 2a - 2b to 2(a - b). The difficulty here is that 2a - 2b seems to be represented as 2 \cdot a + (-2) \cdot b, so there is no factor 2 to match. As another example, (~t)^(~n)
will not match for n = 1
, as in that case you do not write the exponent. In any case, after a lot of effort, I came up with the factorisation rules below. They almost certainly contain redundancies (especially given the use of @acrule
and ~~
s), but every time I leave something out, an example breaks.
Code
Note that this will take a while to run as we have 512 (+1) rules.
using Symbolics
using SymbolicUtils.Rewriters
@variables U_S U_D a λ α Bq_κ
@variables B D E F G H
L1 = 2*D - α^2*a^2*B - α^3*a^3*U_D
L2 = 2*H - α^2*a^2*F + 2*α^3*a^3*G - α^3*a^3*U_D + 1//5*α^5*a^5*E
L3 = 2*H - a^2*F + 2*a^3*G - a^3*U_S + 1//5*a^5*E
L4 = H + 1//2*a^2*F - 2*a^3*G + a^3*U_S - 2//5*a^5*E
L5 = - D + H - 1//2*α^2*a^2*B + 1//2*α^2*a^2*F - 2*α^3*a^3*G - 2//5*α^5*a^5*E
L6 = - 6*D - 6*Bq_κ*D + 6*H*λ + α^2*a^2*B*Bq_κ + 3//5*α^5*a^5*E*λ
L7 = - B + F*λ
solutions = symbolic_linear_solve([L1 ~ 0, L2 ~ 0, L3 ~ 0, L4 ~ 0, L5 ~ 0, L6 ~ 0, L7 ~ 0], [B, D, E, G, F, H, U_D]);
U_D_sol = simplify(solutions[7])
println("Symbolics, original:\n", U_D_sol, "\n")
rdiv1 = @rule ~x => (~x).num where (~x isa Rational && (~x).den == 1) # n//1 => n
s = Postwalk(PassThrough(rdiv1))(U_D_sol) |> simplify # Not the same as simplify(U_D_sol, rdiv1 |> PassThrough |> Postwalk), which somehow just returns U_D_sol
println("Symbolics, after simplifying coefficients:\n", s, "\n")
function generate_factorisation_rules()
rules = []
for left_sign in (true, false), # + or - ('nicest' case first)
right_sign in (true, false),
left_power in (false, true), # 1 (which is not written) or >= 2 (/ <= -1)
right_power in (false, true),
include_x in (false, true),
include_y in (false, true),
include_a in (false, true),
include_b in (false, true),
s_t_equal in (true, false)
# Create all variations of
# ~~x * (~t)^(~n) * ~~y + ~~a * (~t)^(~m) * ~~b => *(~~x..., (~t)^(~n - min(~n, ~m)), ~~y...) + *(~~a..., (~t)^(~m - min(~n, ~m)), ~~b...)) * (~t)^min(~n, ~m)
# But use ~s and ~t and require them to be equal or opposite: necessary for 2a - 2b (i.e. 2a + (-2)b) => 2(a - b)
# (It seems only ... * -... (i.e. ... * (-1) * ...) and ... * ... need to be considered,
# i.e. left_sign == false is not needed.)
left_expr_factors = []
left_sign || push!(left_expr_factors, :(-1))
include_x && push!(left_expr_factors, :(~~x))
if left_power
push!(left_expr_factors, :((~s)^(~n)))
n = :(~n)
else
push!(left_expr_factors, :(~s))
n = 1
end
include_y && push!(left_expr_factors, :(~~y))
if length(left_expr_factors) >= 2
left_expr = :(*($(left_expr_factors...)))
else
left_expr = first(left_expr_factors) # avoid unitary *
end
# Completely similar for the original second term
# (You could probably unify the code for left and right.)
right_expr_factors = []
right_sign || push!(right_expr_factors, :(-1))
include_a && push!(right_expr_factors, :(~~a))
if right_power
push!(right_expr_factors, :((~t)^(~m)))
m = :(~m)
else
push!(right_expr_factors, :(~t))
m = 1
end
include_b && push!(right_expr_factors, :(~~b))
if length(right_expr_factors) >= 2
right_expr = :(*($(right_expr_factors...)))
else
right_expr = first(right_expr_factors)
end
new_left_expr_factors = []
left_sign || push!(new_left_expr_factors, :(-1))
include_x && push!(new_left_expr_factors, :(~~x...))
push!(new_left_expr_factors, :((~s)^($n - min($n, $m))))
include_y && push!(new_left_expr_factors, :(~~y...))
if length(new_left_expr_factors) >= 2
new_left_expr = :(*($(new_left_expr_factors...)))
else
new_left_expr = first(new_left_expr_factors)
end
new_right_expr_factors = []
right_sign || push!(new_right_expr_factors, :(-1))
include_a && push!(new_right_expr_factors, :(~~a...))
push!(new_right_expr_factors, :((~t)^($m - min($n, $m))))
s_t_equal || push!(new_right_expr_factors, :((-1)^min($n, $m)))
include_b && push!(new_right_expr_factors, :(~~b...))
if length(new_right_expr_factors) >= 2
new_right_expr = :(*($(new_right_expr_factors...)))
else
new_right_expr = first(new_right_expr_factors)
end
new_common_factor_expr = :((~s)^min($n, $m))
predicate = s_t_equal ? :(~t === ~s) : :(~t === -~s)
rule = :(($left_expr + $right_expr) => (($new_left_expr + $new_right_expr) * $new_common_factor_expr) where $predicate)
push!(rules, rule)
end
return rules
end
factorisation_rules = [eval(:(@acrule $frule)) for frule in generate_factorisation_rules()]
s = Fixpoint(Postwalk(Chain(factorisation_rules)))(s) # Don't simplify, as this just undoes the factorisation
s = Postwalk(PassThrough(rdiv1))(s) # For some reason both Fixpoint(... rdiv1) and ...(Chain([factorisation_rules; rdiv1])) keep an n//1
println("Symbolics, after factorisation:\n", s, "\n")
u_D_numerator_Mathematica = λ*(-5*α^3 + 4*(-1 + λ) + α^5*(9 + 6*λ) + 4*(-1 + α^5)*Bq_κ)*U_S
u_D_denominator_Mathematica = (9*α*(-1 + λ) - 10*α^3*(-1 + λ) + 4*(-1 + λ)^2 + α^6*(4 + 6*λ) + 3*α^5*(-3 + λ + 2*λ^2) + ( (-1 + α)^4*(4 + α*(7 + 4*α)) + 4*(-1 + α^5)*λ)*Bq_κ )
u_D_Mathematica = u_D_numerator_Mathematica // u_D_denominator_Mathematica
println("Mathematica:\n", u_D_Mathematica, "\n")
println("Same outcome if output below is 0:")
println(simplify(s - u_D_Mathematica; expand=true))
Output
Symbolics, original:
(-(384//1)U_Sλ - (384//1)Bq_κU_Sλ + (384//1)U_S(λ^2) - (480//1)U_S(α^3)λ + (864//1)U_S(α^5)λ + (384//1)Bq_κU_S(α^5)λ + (576//1)U_S(α^5)(λ^2)) / ((384//1) + (384//1)Bq_κ - (864//1)α - (768//1)λ - (864//1)Bq_κα - (384//1)Bq_κλ + (864//1)αλ + (384//1)(λ^2) + (960//1)(α^3) + (960//1)Bq_κ(α^3) - (960//1)(α^3)λ - (864//1)(α^5) - (864//1)Bq_κ(α^5) + (384//1)(α^6) + (288//1)(α^5)λ + (384//1)Bq_κ(α^6) + (384//1)Bq_κ(α^5)λ + (576//1)(α^6)λ + (576//1)(α^5)(λ^2))
Symbolics, after simplifying coefficients:
(-4U_Sλ - 4Bq_κU_Sλ + 4U_S(λ^2) - 5U_S*(α^3)λ + 9U_S(α^5)λ + 4Bq_κU_S*(α^5)λ + 6U_S(α^5)(λ^2)) / (4 + 4Bq_κ - 9α - 8λ - 9Bq_κα - 4Bq_κλ + 9αλ + 4(λ^2) + 10(α^3) + 10Bq_κ*(α^3) - 10(α^3)λ - 9(α^5) - 9Bq_κ(α^5) + 4(α^6) + 3(α^5)λ + 4Bq_κ(α^6) + 4Bq_κ*(α^5)*λ + 6(α^6)λ + 6(α^5)(λ^2))
Symbolics, after factorisation:
((4(-1 + λ + Bq_κ*(-1 + α^5)) + (-5 + (α^2)(9 + 6λ))(α^3))U_Sλ) / (α*(10(1 + Bq_κ - λ)(α^2) - 9(1 - λ + α^4)) + 4(1 + (1 + Bq_κ)(α^6)) + Bq_κ*(4(1 - λ) + α*(-9 + (α^4)(-9 + 4λ))) + (-8 + 4λ + (3 + 6(α + λ))(α^5))*λ)
Mathematica:
((4(-1 + λ) - 5(α^3) + 4Bq_κ*(-1 + α^5) + (α^5)(9 + 6λ))U_Sλ) / (9α(-1 + λ) + 4((-1 + λ)^2) - 10(α^3)(-1 + λ) + Bq_κ(4(-1 + α^5)λ + (4 + α(7 + 4α))((-1 + α)^4)) + (α^6)(4 + 6λ) + 3(α^5)*(-3 + λ + 2(λ^2)))
Same outcome if output below is 0:
0
The final Symbolics.jl output is indeed nicely factorised and more compact than initially. I would still slightly prefer the Mathematica result, but again this is just a matter of taste.
A note on some weird simplify behaviour
For some reason using simplify
seems to sometimes simply return the input, ignoring the rewriting that happens internally. Using a rewriter directly might work as you would expect (as in the code above), or not (as in the code below). For example,
julia> using Symbolics, SymbolicUtils.Rewriters
julia> @variables a b c
julia> rdiv1 = @rule ~x => (~x).num where (~x isa Rational && (~x).den == 1)
~x => ((~x).num where ~x isa Rational && (~x).den == 1)
julia> rdiv1(2//1)
2
julia> simplify(2//1 * a + 3//1 * b, Postwalk(PassThrough(rdiv1)))
(2//1)*a + (3//1)*b
julia> Postwalk(PassThrough(rdiv1))(2//1 * a + 3//1 * b)
(2//1)*a + (3//1)*b
If we ‘debug’ we can see that in the simplify
case we are actually rewriting to 2a + 3b
but for some reason we do not return it. In the direct rewriter case, it seems that no decomposition is taking place.
julia> rprint = @rule ~x => println(~x)
~x => println(~x)
julia> simplify(2//1 * a + 3//1 * b, Postwalk(RestartedChain([rprint, rdiv1]))) # Last line is return value, printed automatically in the REPL (unless ;)
2//1
2
a
2a
3//1
3
b
3b
2a + 3b
(2//1)*a + (3//1)*b
julia> Postwalk(Chain([rdiv1, rprint]))(2//1 * a + 3//1 * b)
(2//1)*a + (3//1)*b
(2//1)*a + (3//1)*b
Honestly, none of this makes sense to me…