Current section

Files

Jump to
exun lib exun_simpl.ex
Raw

lib/exun_simpl.ex

defmodule Exun.Simpl do
alias Exun.Collect, as: C
alias Exun.Unit, as: Un
alias Exun.Eq, as: E
@zero {:numb, 0, 1}
@uno {:numb, 1, 1}
@muno {:numb, -1, 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
nast = mk(ast)
if E.eq(nast, ast),
do: nast,
else: nast |> mkrec
end
def mkrec(%Exun{ast: ast, pc: pc}) do
%Exun{ast: mkrec(ast), pc: pc}
end
# simplify
def mk(ast) do
case ast do
{:numb, n, d} ->
mknum(n, d)
{:minus, {:minus, a}} ->
mk(a)
{:minus, @zero} ->
@zero
{:minus, a} ->
chsign(mk(a))
{:unit, val, @uno} ->
mk(val)
{:unit, val, ut} ->
Un.toSI({:unit, mk(val), mk(ut)})
{:elev, _, @zero} ->
@uno
{:elev, a, @uno} ->
mk(a)
{:elev, @uno, _} ->
@uno
{:elev, {:numb, base, d1}, {:numb, -1, 1}} ->
{:numb, d1, base}
{: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} ->
args = Enum.map(lst, &C.coll/1)
Exun.Fun.fcall(name, args)
{:deriv, f, v} ->
Exun.Der.deriv({:deriv, mk(f), v})
{:integ, f, v = {:vari, _}} ->
Exun.Integral.integ({:integ, mk(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
_ ->
isolate({{:m, op}, lst})
end
end
{op, a, b} ->
{op, mk(a), mk(b)}
tree ->
tree
end
end
def isolate({{:m, op}, list}) do
best_match =
Enum.map(list, fn pivot ->
coefs =
Enum.map(list, fn ast ->
case {op, pivot, ast} do
{op, a, a} when op in [:suma, :mult] ->
{:ok, pivot, @uno}
{op, {:minus, a}, a} when op in [:suma, :mult] ->
{:ok, pivot, @muno}
{op, {:elev, a, e1}, {:elev, a, e2}} ->
case op do
:suma -> {:err, nil, ast}
:mult -> {:ok, pivot, mk(divi(e2, e1))}
end
{op, a, {:elev, a, b}} ->
case op do
:suma -> {:ok, pivot, mk({:elev, a, mk(rest(b, @uno))})}
:mult -> {:ok, pivot, b}
end
{:suma, a, {{:m, :mult}, lst}} ->
if a in lst,
do: {:ok, pivot, {{:m, :mult}, lst |> List.delete(a)}},
else: {:err, nil, ast}
_ ->
{:err, nil, ast}
end
end)
# Count matches for pivot
matches = Enum.count(coefs, fn {res, _, _} -> res == :ok end)
{matches, coefs}
end)
|> Enum.reject(fn {matches, _} -> matches < 2 end)
|> Enum.sort(fn {e1, _}, {e2, _} -> e1 < e2 end)
|> List.first()
case best_match do
{_, coefs} ->
{pivot, iso, notiso} =
Enum.reduce(coefs, {nil, [], []}, fn item, {piv, iso, noiso} ->
case item do
{:ok, pivot, coef} ->
{pivot, [coef | iso], noiso}
{:err, _, ast} ->
{piv, iso, [ast | noiso]}
end
end)
sum_coefs = mk({{:m, :suma}, iso})
case {op, length(iso), length(notiso)} do
{_, 0, 1} ->
List.first(notiso)
{:suma, 0, 0} ->
@zero
{:suma, 1, 0} ->
{{:m, :mult}, [pivot, List.first(iso)]}
{:suma, 0, 1} ->
List.first(notiso)
{:suma, 1, 1} ->
{{:m, :suma}, [{{:m, :mult}, [pivot, List.first(iso)]}, List.first(notiso)]}
{:suma, _, 0} ->
{{:m, :mult}, [pivot, sum_coefs]}
{:suma, 0, _} ->
{{:m, :suma}, notiso}
{:suma, _, _} ->
{{:m, :suma}, [{{:m, :mult}, [pivot, sum_coefs]} | notiso]}
{:mult, 0, 0} ->
@uno
{:mult, 1, 0} ->
{:elev, pivot, List.first(iso)}
{:mult, 0, 1} ->
List.first(notiso)
{:mult, 1, 1} ->
{{:m, :mult}, [{:elev, pivot, List.first(iso)}, List.first(notiso)]}
{:mult, _, 0} ->
{:elev, pivot, sum_coefs}
{:mult, 0, _} ->
{{:m, :mult}, notiso}
{:mult, _, _} ->
{{:m, :mult}, [{:elev, pivot, sum_coefs} | notiso]}
end
|> mk()
nil ->
{{:m, op}, list}
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}} ->
{if(sign, do: false, else: true), 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 une 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 ->
sou = {:unit, mult(acu_nd, u_nd), mult(acu_t, u_t)}
{nd_ac, sou, rest}
: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), 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}
{{:m, :suma}, list} ->
{{:m, :suma}, Enum.map(list, &chsign(&1))}
{{:m, :mult}, list} ->
# sign true is positive, false is negative
{res, nl} =
Enum.reduce(list, {true, []}, fn opand, {sign, nl} ->
case opand do
{:numb, -1, 1} ->
{not sign, nl}
{:minus, minusop} ->
{not sign, [minusop | nl]}
_ ->
{sign, [opand | nl]}
end
end)
nl = nl |> Enum.reverse()
if res do
{:minus, {{:m, :mult}, nl}}
else
{{:m, :mult}, nl}
end
other ->
{:minus, other}
end
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
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]}
"""
def mult({:numb, n1, d1}, {:numb, n2, d2}), do: mknum(n1 * n2, d1 * d2)
def mult(a, b), do: mcompose(:mult, a, b)
@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,:suma},[a,b]}
"""
def suma({:numb, n1, d1}, {:numb, n2, d2}), do: mknum(n1 * d2 + n2 * d1, d1 * d2)
def suma(a, b), do: mcompose(:suma, a, 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]}
defp mcompose(op, a, b) do
C.coll(
case {a, b} do
{{{:m, ^op}, l1}, {{:m, ^op}, l2}} ->
{{:m, op}, l1 ++ l2}
{{{:m, ^op}, l1}, _} ->
{{:m, op}, [b | l1]}
{_, {{:m, ^op}, l2}} ->
{{:m, op}, [a | l2]}
_ ->
{{:m, op}, [a, b]}
end
)
end
end