Current section

Files

Jump to
exun lib exun_simpl2.ex
Raw

lib/exun_simpl2.ex

defmodule Exun.Simpl do
alias Exun.Unit, as: Un
alias Exun.Eq, as: E
require Integer
@zero {:numb, 0, 1}
@uno {:numb, 1, 1}
@muno {:numb, -1, 1}
@dos {:numb, 2, 1}
@moduledoc """
Simplify expressions
"""
@doc """
Recursively try to simplify expression. Multiple tries are performed.
For a more agressive simplify, use Exun.Collect.coll
"""
def mkrec(ast) when is_tuple(ast) do
if E.eq(nast = mk(ast), ast),
do: nast,
else: mkrec(nast)
end
def mkrec(%Exun{ast: ast, pc: pc}) do
%Exun{ast: mkrec(ast), pc: pc}
end
@doc """
Normalize some structs, mainly {{:m,op},l} so signs can be comparables
"""
def normalize({:numb, n, d}), do: mknum(n, d)
def normalize({:unit, value, {:numb, 1, 1}}), do: normalize(value)
def normalize({:unit, value, units}), do: {:unit, normalize(value), units}
def normalize({:elev, _, @zero}), do: @uno
def normalize({:elev, a, @uno}), do: a
def normalize({:elev, @uno, _}), do: @uno
def normalize({:elev, @zero, _}), do: @zero
def normalize({{:m, :suma}, []}), do: @zero
def normalize({{:m, :suma}, [unique]}), do: normalize(unique)
def normalize({{:m, :mult}, []}), do: @uno
def normalize({{:m, :mult}, [unique]}), do: normalize(unique)
def normalize({:minus, {:minus, a}}), do: normalize(a)
def normalize({:minus, @zero}), do: @zero
def normalize({:minus, {:numb, n, d}}), do: {:numb, -n, d}
def normalize({:deriv, f, v}), do: {:deriv, normalize(f), v}
def normalize({:integ, f, v}), do: {:integ, normalize(f), v}
def normalize({:fcall, name, list}), do: {:fcall, name, Enum.map(list, &normalize/1)}
def normalize({{:m, :suma}, list}) do
list = Enum.map(list, &normalize/1)
list = promote_sublist(:suma, list)
newlist =
Enum.reduce(list, [], fn opand, ac ->
add_opand(:suma, opand, {{:m, :suma}, ac})
end)
|> Enum.sort(&E.smm/2)
case length(newlist) do
0 -> @zero
1 -> List.first(newlist)
_ -> {{:m, :suma}, newlist}
end
end
def normalize({{:m, :mult}, list}) do
{newlist, count_negs} =
Enum.map(list, &normalize/1)
|> Enum.reduce({[], 0}, fn el, {newlist, count_negs} ->
if !signof(el) do
{[chsign(el) | newlist], count_negs + 1}
else
{[el | newlist], count_negs}
end
end)
newlist =
Enum.reduce(newlist, [], fn opand, ac ->
add_opand(:mult, opand, {{:m, :mult}, ac})
end)
|> Enum.sort(&E.smm/2)
newop =
case length(newlist) do
0 -> @uno
1 -> List.first(newlist)
_ -> {{:m, :mult}, newlist}
end
if Integer.is_even(count_negs) do
newop
else
{:minus, newop}
end
end
def normalize({:minus, {{:m, :suma}, list = [h | _]}}) do
if(!signof(h)) do
list = Enum.map(list, fn el -> chsign(el) end)
normalize({{:m, :suma}, list})
else
{:minus, normalize({{:m, :suma}, list})}
end
end
def normalize({:minus, a}), do: {:minus, normalize(a)}
def normalize(ast = {:elev, base, num = {:numb, _, _}}) do
base = normalize(base)
cond do
num == @zero ->
@uno
is_par(num) and is_gtzero(num) and !signof(base) ->
{:elev, chsign(base), num}
!is_par(num) and !is_gtzero(num) and !signof(base) ->
{:minus, {:elev, chsign(base), num}}
!signof(base) ->
{:minus, {:elev, chsign(base), num}}
true ->
ast
end
end
def normalize(other), do: other
# Calculate
def mk(ast) do
case normalize(ast) do
{:minus, a} ->
{:minus, mk(a)}
{:unit, val, @uno} ->
mk(val)
{:unit, val, ut} ->
Un.toSI({:unit, mk(val), mk(ut)})
{:elev, {:numb, n, d}, @muno} ->
{:numb, d, n}
{:elev, {:numb, base, d1}, {:numb, exp, d2}} ->
{:numb, :math.pow(base, exp / d2), :math.pow(d1, exp / d2)}
{:elev, {:elev, base, e1}, e2} ->
{:elev, mk(base), mk(mult(e1, e2))}
{:elev, {:unit, uv, ut}, expon} ->
{:unit, mk({:elev, uv, expon}), mk({:elev, ut, expon})}
{:fcall, name, lst} ->
Exun.Fun.fcall(name, lst)
{:deriv, f, v} ->
Exun.Der.deriv(f, v)
{:integ, f, v = {:vari, _}} ->
Exun.Integral.integ({:integ, f, v})
{{:m, op}, lst} ->
# Simplify each component of the list
lst = Enum.map(lst, &mkrec(&1))
# Promote sublist, so if ther is a element in lst of class {:m,op}
# include sublist in main list
lst = promote_sublist(op, lst)
# Collect numbers and units and simplify
lst = collect_literals({{:m, op}, lst})
# Remove zeroes or ones, 0+any=any, 1*any=any and may be the nil
# introduced by the last command
unity = if op == :suma, do: @zero, else: @uno
lst = Enum.reject(lst, &(&1 == unity or &1 == nil))
# |> IO.inspect(label: "post literals")
# if a multiple mult {:m,:mult} check if zero is a component
if op == :mult and @zero in lst do
@zero
else
case length(lst) do
# No more elements in list, return unity
0 -> unity
# Only one element, replace {{}:m,op},lst} with it
1 -> List.first(lst)
# Let's play, try to extract commons from list
_ -> cfactor({{:m, op}, lst})
end
end
{op, a, b} ->
{op, mk(a), mk(b)}
# throw "Unknown in mk #{U.tostr({op,a,b})}"
tree ->
tree
end
end
defp promote_sublist(op, list) when is_list(list) do
case op do
:suma ->
Enum.reduce(list, [], fn elem, newlist ->
case elem do
{{:m, :suma}, sublist} ->
sublist ++ newlist
{:minus, {{:m, :suma}, sublist}} ->
Enum.map(sublist, &chsign/1) ++ newlist
other ->
[other | newlist]
end
end)
:mult ->
{sign, nlist} =
Enum.reduce(list, {true, []}, fn elem, {sign, newlist} ->
case elem do
{{:m, :mult}, sublist} ->
{sign, sublist ++ newlist}
{:minus, {{:m, :mult}, sublist}} ->
{not sign, sublist ++ newlist}
other ->
{sign, [other | newlist]}
end
end)
if sign do
nlist
else
List.replace_at(nlist, 0, chsign(List.first(nlist)))
end
end
|> Enum.sort(&Exun.Eq.smm/2)
end
@doc """
Reduce literals to one unit or one constant if possible
"""
def collect_literals({{:m, op}, lst}) do
unity = if op == :suma, do: @zero, else: @uno
ufc = if op == :suma, do: &suma/2, else: &mult/2
{n, u, lst} =
Enum.reduce(lst, {unity, nil, []}, fn el, {nd_ac = {:numb, _, _}, u_ac, rest} ->
case el do
nd = {:numb, _, _} ->
{ufc.(nd_ac, nd), u_ac, rest}
u = {:unit, u_nd = {:numb, _, _}, u_t} ->
if u_ac == nil do
{nd_ac, u, rest}
else
{:unit, acu_nd, acu_t} = u_ac
case op do
:mult ->
number_unit = mkrec(mult(acu_nd, u_nd))
tree_unit = mkrec(mult(acu_t, u_t))
case tree_unit do
{:numb, _, _} ->
{ufc.(nd_ac, number_unit), nil, rest}
_ ->
{nd_ac, {:unit, number_unit, tree_unit}, rest}
end
:suma ->
sou =
case Exun.Unit.sum(u_ac, u) do
{:err, msg} -> throw(msg)
{:ok, unit} -> unit
end
{nd_ac, sou, rest}
end
end
other ->
{nd_ac, u_ac, [other | rest]}
end
end)
case {n, u} do
{^unity, nil} -> lst
{^unity, unit} -> [unit | lst]
{number, nil} -> [number | lst]
{nd, {:unit, und, tree}} -> [{:unit, mult(nd, und), mk(tree)} | lst]
end
# |> IO.inspect(label: "final unit,number,lst")
end
@doc """
Change sign of AST
"""
def chsign(ast) do
case ast do
{:minus, a} ->
a
{:unit, a, b} ->
{:unit, chsign(a), b}
{:numb, n, d} ->
{:numb, -n, d}
other ->
{:minus, other}
end
|> normalize()
end
@doc """
Change power sign of AST (expon * -1 or 1/tree)
"""
def chpow(ast) do
case ast do
{:elev, algo, @muno} ->
algo
{:elev, a, b} ->
{:elev, a, chsign(b)}
{:unit, a, b} ->
{:unit, chpow(a), chpow(b)}
{:numb, n, d} ->
{:numb, d, n}
{{:m, :mult}, lst} ->
{{:m, :mult}, Enum.map(lst, &chpow(&1))}
other ->
{:elev, other, @muno}
end
|> normalize()
end
@doc """
Try to keep at least 3 decimals of precission
"""
def mknum(n, d) do
# Preserve sign on numerator, if denominator<0 change both
# signs
{n, d} =
if d < 0 do
{-n, -d}
else
{n, d}
end
# If are integers, substirute floats
f_n = floor(n)
f_d = floor(d)
cond do
f_n == n and f_d == d ->
mcd = Integer.gcd(f_n, f_d)
{:numb, floor(n / mcd), floor(d / mcd)}
f_d == d ->
{:numb, n, f_d}
f_n == n ->
{:numb, f_n, d}
true ->
{:numb, n, d}
end
end
@doc """
For convenience, creates ast {{:m,:mult},[a,b^-1]}
"""
def divi({:numb, n1, d1}, {:numb, n2, d2}), do: mknum(n1 * d2, d1 * n2)
def divi(a, b), do: mult(a, chpow(b))
@doc """
For convenience, creates ast {{:m,:mult},[a,-b]}
"""
def rest({:numb, n1, d1}, {:numb, n2, d2}), do: mknum(n1 * d2 - n2 * d1, d1 * d2)
def rest(a, b), do: suma(a, chsign(b))
@doc """
For convenience, creates ast {:elev,a,b}
"""
def elev({:numb, n1, d1}, {:numb, n2, d2}),
do: {:numb, :math.pow(n1, n2 / d2), :math.pow(d1, n2 / d2)}
def elev(a, b), do: {:elev, a, b}
def minus(a), do: {:minus, a}
def ln(a), do: {:fcall, "ln", [a]}
def exp(a, b), do: {:fcall, "exp", [a, b]}
@doc """
Returns normalized sign of ast
"""
def signof(ast) do
case ast do
{:minus, a} ->
not signof(a)
{:numb, a, b} ->
a / b >= 0
{:elev, a, b} ->
case b do
{:numb, _, _} ->
if is_par(b) and is_gtzero(b) do
true
else
signof(a)
end
_ ->
signof(a)
end
{{:m, _}, _} ->
true
{:unit, a, _} ->
signof(a)
{:fcall, _, _} ->
true
{:deriv, f, _} ->
signof(f)
{:integ, f, _} ->
signof(f)
{:vari, _} ->
true
end
end
def is_par(ast) do
case ast do
{:numb, n, d} ->
num = n / d
if floor(num) == num do
Integer.is_even(floor(num))
else
false
end
_ ->
false
end
end
def is_gtzero({:numb, n, d}) do
if (n > 0 and d > 0) or (n < 0 and d < 0), do: true, else: false
end
@doc """
Extract common factors from mult
"""
def cfactor({{:m, op}, lst}) do
case Enum.reduce(lst, [], fn pivot, nl ->
remain = lst |> List.delete(pivot)
nl ++
Enum.reduce(remain, [], fn opand, subs ->
case subst(op, pivot, opand) do
{:ok, s} -> [{pivot, opand, s} | subs]
_ -> subs
end
end)
end) do
[] ->
{{:m, op}, lst}
[{p, o, s} | _] ->
{{:m, op}, [s | lst |> List.delete(p) |> List.delete(o)]}
|> normalize()
end
end
def subst(op, pivot, opand) do
case {op, pivot, opand} do
{:suma, a, a} ->
{:ok, mult(@dos, a)}
{:suma, a, {:minus, a}} ->
{:ok, @zero}
{:suma, {:elev, base, e1}, {:elev, base, e2}} ->
{:ok, mult({:elev, base, e2}, suma(@uno, {:elev, base, rest(e1, e2)}))}
{:suma, {:elev, base, e1}, {:elev, {:minus, base}, e2}} ->
{:ok, mult({:elev, base, e2}, suma(@muno, {:elev, base, rest(e1, e2)}))}
{:suma, base, {:elev, base, exp}} ->
{:ok, mult(base, suma(@uno, elev(base, rest(exp, @uno))))}
{:suma, {:minus, base}, {:elev, base, exp}} ->
{:ok, mult(base, suma(@muno, elev(base, rest(exp, @uno))))}
{:suma, base, {:minus, {:elev, base, exp}}} ->
{:ok, mult(base, suma(@uno, {:minus, elev(base, rest(exp, @uno))}))}
{:suma, {:minus, {{:m, :mult}, p1}}, {:minus, {{:m, :mult}, p2}}} ->
sumofmults(false, p1, false, p2)
{:suma, {{:m, :mult}, p1}, {:minus, {{:m, :mult}, p2}}} ->
sumofmults(true, p1, false, p2)
{:suma, {{:m, :mult}, p1}, {{:m, :mult}, p2}} ->
sumofmults(true, p1, true, p2)
{:suma, a, {{:m, :mult}, l}} ->
if a in l do
extract = l |> List.delete(a)
{:ok, {{:m, :mult}, [a, suma(@uno, {{:m, :mult}, extract})]}}
else
{:err, nil}
end
{:mult, a, a} ->
{:ok, elev(a, @dos)}
{:mult, a, {:minus, a}} ->
{:ok, {:minus, elev(a, @dos)}}
{:mult, {:elev, base, e1}, {:elev, base, e2}} ->
{:ok, {:elev, base, suma(e1, e2)}}
{:mult, base, {:elev, base, exp}} ->
{:ok, elev(base, suma(exp, @uno))}
_ ->
{:err, nil}
end
end
defp sumofmults(s1, p1, s2, p2) do
allsubst =
Enum.reduce(p1, [], fn op1, ac1 ->
remainl1 = List.delete(p1, op1)
Enum.reduce(p2, [], fn op2, ac2 ->
case {op1, op2} do
{a, a} ->
remainl2 = List.delete(p2, op2)
m1 = {{:m, :mult}, remainl1}
m2 = {{:m, :mult}, remainl2}
coefs =
case {s1, s2} do
{true, true} -> suma(m1, m2)
{true, false} -> rest(m1, m2)
{false, false} -> {:minus, suma(m1, m2)}
end
[{:ok, mult(a, coefs)} | ac2]
{a, {:minus, a}} ->
remainl2 = List.delete(p2, op2)
m1 = {{:m, :mult}, remainl1}
m2 = {{:m, :mult}, remainl2}
coefs =
case {s1, s2} do
{true, true} -> suma(m1, m2)
{true, false} -> rest(m1, m2)
{false, false} -> {:minus, suma(m1, m2)}
end
[{:ok, minus(mult(a, coefs))} | ac2]
{{:elev, base, e1}, {:elev, base, e2}} ->
remainl2 = List.delete(p2, op2)
{e1, remainl1, e2, remainl2} =
case gt(e2, e1) do
{:ok, true} -> {e1, remainl1, e2, remainl2}
{:unknown, _} -> {e1, remainl1, e2, remainl2}
{:ok, false} -> {e2, remainl2, e1, remainl1}
end
aux1 = {{:m, :mult}, remainl1}
aux2 = {{:m, :mult}, [{:elev, base, rest(e2, e1)} | remainl2]}
coefs =
case {s1, s2} do
{true, true} -> suma(aux1, aux2)
{true, false} -> rest(aux1, aux2)
{false, false} -> {:minus, suma(aux1, aux2)}
end
[
{:ok, mult({:elev, base, e1}, coefs)}
| ac2
]
{base, {:elev, base, exp}} ->
remainl2 = List.delete(p2, op2)
aux1 = [{:elev, base, rest(exp, @uno)} | remainl2]
coefs =
case {s1, s2} do
{true, true} -> {{:m, :suma}, [@uno | aux1]}
{true, false} -> rest(@uno, {{:m, :suma}, aux1})
{false, false} -> {:minus, {{:m, :suma}, [@uno | aux1]}}
end
[{:ok, mult(base, coefs)} | ac2]
_ ->
ac2
end
end) ++
ac1
end)
case allsubst do
[] ->
{:err, nil}
_ ->
List.first(allsubst)
end
end
defp gt({:numb, n1, d1}, {:numb, n2, d2}) do
{:ok, n1 / d1 > n2 / d2}
end
defp gt({:elev, a, e1}, {:numb, a, e2}) do
gt(e1, e2)
end
defp gt(_, _) do
{:unknown, nil}
end
def add_opand(:suma, {{:m, :suma}, l1}, {{:m, :suma}, l2}) do
Enum.reduce(l1, l2, fn sumando1, ac ->
add_opand(:suma, sumando1, {{:m, :suma}, ac})
end)
end
def add_opand(:mult, {{:m, :mult}, l1}, {{:m, :mult}, l2}) do
# IO.inspect(binding())
Enum.reduce(l1, l2, fn multando, ac ->
add_opand(:mult, multando, {{:m, :mult}, ac})
# |> IO.inspect()
end)
# |> IO.inspect()
end
def add_opand(:suma, a, {{:m, :suma}, l}) do
{reduced, list} =
Enum.reduce(l, {false, []}, fn sumando, {matched, newlist} ->
if not matched do
case {a, sumando} do
{{:numb, _, _}, {:numb, _, _}} -> {true, [suma(a, sumando) | newlist]}
{{:unit, _, _}, {:unit, _, _}} -> {true, [suma(a, sumando) | newlist]}
{a, a} -> {true, [mult(@dos, a) | newlist]}
{a, {:minus, a}} -> {true, newlist}
{{:minus, a}, a} -> {true, newlist}
{a, {{:m, :mult}, l2}} -> add_opand1(a, l2, sumando, newlist)
{{{:m, :mult}, l2}, a} -> add_opand1(a, l2, sumando, newlist)
_ -> {false, [sumando | newlist]}
end
else
{true, [sumando | newlist]}
end
end)
if reduced,
do: list,
else:
[a | l]
|> Enum.sort(&E.smm/2)
end
def add_opand(:mult, opand, {{:m, :mult}, l}) do
# IO.inspect(binding(), label: "single")
{reduced, list} =
Enum.reduce(l, {false, []}, fn multando, {matched, newlist} ->
if not matched do
case {opand, multando} do
{{:numb, _, _}, {:numb, _, _}} -> {true, [mult(opand, multando) | newlist]}
{{:numb, _, _}, {:unit, _, _}} -> {true, [mult(opand, multando) | newlist]}
{{:unit, _, _}, {:numb, _, _}} -> {true, [mult(opand, multando) | newlist]}
{a, a} -> {true, [{:elev, a, @dos} | newlist]}
{a, {:minus, a}} -> {true, [{:minus, {:elev, a, @dos}} | newlist]}
{{:minus, a}, a} -> {true, [{:minus, {:elev, a, @dos}} | newlist]}
{a, {:elev, a, @muno}} -> {true, [@uno | newlist]}
{{:elev, a, @muno}, a} -> {true, [@uno | newlist]}
{a, {:elev, a, exp}} -> {true, [{:elev, a, suma(exp, @uno)} | newlist]}
{{:elev, a, exp}, a} -> {true, [{:elev, a, suma(exp, @uno)} | newlist]}
{{:elev, a, e1}, {:elev, a, e2}} -> {true, [{:elev, a, suma(e1, e2)} | newlist]}
_ -> {false, [multando | newlist]}
end
else
{true, [multando | newlist]}
end
end)
if reduced do
list
else
[opand | l]
end
|> Enum.sort(&E.smm/2)
# |> IO.inspect(label: "Single result")
end
@doc """
For convenience, creates ast {{:m,:suma},[a,b]}
"""
def suma({:numb, n1, d1}, {:numb, n2, d2}), do: mknum(n1 * d2 + n2 * d1, d1 * d2)
def suma(u1 = {:unit, _, _}, u2 = {:unit, _, _}) do
case Un.sum(u1, u2) do
{:ok, ast} -> ast
{:err, msg} -> throw(msg)
end
end
def suma(a, b), do: {{:m, :suma}, [a, b]}
@doc """
For convenience, creates ast {{:m,:mult},[a,b]}
"""
def mult(@uno, a), do: a
def mult(a, @uno), do: a
def mult({:numb, n1, d1}, {:numb, n2, d2}), do: mknum(n1 * n2, d1 * d2)
def mult(u = {:unit, _, _}, n = {:numb, _, _}), do: mult(n, u)
def mult(n = {:numb, _, _}, {:unit, vu1, au1}), do: {:unit, mult(n, vu1), au1}
def mult({:unit, v1, t1}, {:unit, v2, t2}), do: {:unit, mult(v1, v2), mult(t1, t2)}
def mult(a, {{:m, :mult}, l}), do: {{:m, :mult}, [a | l]}
def mult({{:m, :mult}, l}, a), do: {{:m, :mult}, [a | l]}
def mult(a, b), do: {{:m, :mult}, [a, b]}
defp add_opand1(a, l2, sumando, newlist) do
cond do
a in l2 ->
{true, [mult(a, {{:m, :suma}, [@uno | List.delete(l2, a)]}) | newlist]}
true ->
{false, [sumando | newlist]}
end
end
end