# AUTOGENERATED FILE! PLEASE DON'T EDIT HERE. EDIT THE SOURCE NOTEBOOKS INSTEAD
"""This module is for creating dynamic graphs using plain old
equations. For example::
    from k1lib.imports import *
    x = graphEqn.Variable()
    y = x * 3 + 5
    z = y ** 5
    z(2) # returns 161051 (from (2 * 3 + 5) ** 5)
Point is, ``x`` is an unknown, ``y`` is a "function" of ``x``. ``z`` depends on
``y``, but is also a function of ``x``.
Remember that you can go pretty wild with this::
    x2 = k1lib.inverse(z)
    x2(161051) # returns 2.0
Here, ``x2`` is actually a function x(z).
For simple functions like this, it should take 200us to solve it. You can also
declare a bunch of variables early on, and then resolve them one by one like
this::
    a = Variable(); b = Variable()
    c = a + b + 2; a.value = 6
    c(5) # returns 13
    b.value = 7
    c() # returns 15
"""
from typing import Callable as _Callable, Union as _Union, Iterator as _Iterator
import k1lib as _k1lib
__all__ = ["Variable"]
F = _Callable[[float, float], float]
class Expression:                                                                # Expression
    def __init__(self, a:"Variable", b:"Variable", operation:F):                 # Expression
        self.a = a; self.b = b; self.operation = operation                       # Expression
    @property                                                                    # Expression
    def resolved(self):                                                          # Expression
        """Whether this expression has been resolved (both internal variables are
resolved)."""                                                                    # Expression
        return self.a.resolved and self.b.resolved                               # Expression
    @property                                                                    # Expression
    def value(self):                                                             # Expression
        """Value of the expression."""                                           # Expression
        return self.operation(self.a._value, self.b._value)                      # Expression
    def applyF(self, f:_Callable[["Variable"], None]):                           # Expression
        self.a._applyF(f); self.b._applyF(f)                                     # Expression
def _op2(a, b, operation:F):                                                     # _op2
    a = a if isinstance(a, Variable) else _Constant(a)                           # _op2
    b = b if isinstance(b, Variable) else _Constant(b)                           # _op2
    answer = Variable(); answer.expr = Expression(a, b, operation)               # _op2
    if answer.expr.resolved: answer._value = answer.expr.value                   # _op2
    return answer                                                                # _op2
[docs]class Variable:                                                                  # Variable
    _idx = 0                                                                     # Variable
    def __init__(self):                                                          # Variable
        self.__class__._idx += 1; self.variableName = f"V{self.__class__._idx}"  # Variable
        self.expr:Expression = None                                              # Variable
        self._value:float = None # if not None, then already resolved            # Variable
        self.isConstant = False # to know if the value above is resolved, or is truely a literal number # Variable
        self.trial:int = 0 # current resolve trial number                        # Variable
    @property                                                                    # Variable
    def value(self) -> _Union[float, None]:                                      # Variable
        """Actual float value of :class:`Variable`. When setting this, if the
new value's not None, the object would act like a constant in every future
equations. To turn it back into a :class:`Variable`, simply set this to
:data:`None`."""                                                                 # Variable
        return self._value                                                       # Variable
    @value.setter                                                                # Variable
    def value(self, v):                                                          # Variable
        """Sets the value of variable. If it's an actual value, """              # Variable
        if v is None: self._value = None; self.isConstant = False                # Variable
        else: self._value = v; self.isConstant = True                            # Variable
    def _reset(self): self._value = self._value if self.isConstant else None     # Variable
    @property                                                                    # Variable
    def resolved(self):                                                          # Variable
        """Whether this variable has been resolved or not."""                    # Variable
        return self._value != None                                               # Variable
    def _applyF(self, f:_Callable[["Variable"], None]): # apply an operation to variable and its dependencies # Variable
        f(self)                                                                  # Variable
        if self.expr != None: self.expr.applyF(f)                                # Variable
    @property                                                                    # Variable
    def _leaves(self) -> _Iterator["Variable"]:                                  # Variable
        """Get variables that does not have an expression linked to it. Aka at
the leaf."""                                                                     # Variable
        if self.resolved: return                                                 # Variable
        if self.expr == None: yield self                                         # Variable
        else:                                                                    # Variable
            yield from self.expr.a._leaves                                       # Variable
            yield from self.expr.b._leaves                                       # Variable
    @property                                                                    # Variable
    def leaves(self): return list(set(self._leaves))                             # Variable
[docs]    def __call__(self, x:float=None) -> _Union[float, None]:                     # Variable
        """Tries to solve this variable given the independent variable ``x``.
:param x: if nothing is specified, you have to be sure that all variables already
    have a value."""                                                             # Variable
        return self._solve(x)                                                    # Variable 
    def __add__(self, v): return _op2(self, v, lambda a, b: a + b)               # Variable
    def __sub__(self, v): return _op2(self, v, lambda a, b: a - b)               # Variable
    def __neg__(self): return _op2(_Constant(0), self, lambda a, b: a - b)       # Variable
    def __mul__(self, v): return _op2(self, v, lambda a, b: a * b)               # Variable
    def __truediv__(self, v): return _op2(self, v, lambda a, b: a / b)           # Variable
    def __pow__(self, v): return _op2(self, v, lambda a, b: a**b)                # Variable
    def __radd__(self, v): return _op2(v, self, lambda a, b: a + b)              # Variable
    def __rsub__(self, v): return _op2(v, self, lambda a, b: a - b)              # Variable
    def __rmul__(self, v): return _op2(v, self, lambda a, b: a * b)              # Variable
    def __rtruediv__(self, v): return _op2(v, self, lambda a, b: a / b)          # Variable
    def __rpow__(self, v): return _op2(v, self, lambda a, b: a**b)               # Variable
    def __repr__(self): return f"{self._value}" if self.resolved else f"<Variable {self.variableName}>" # Variable
    def __int__(self): return self._value                                        # Variable
    def __float__(self): return self._value                                      # Variable 
@_k1lib.patch(Variable)                                                          # Variable
def _resolve(self, trial:int) -> bool:                                           # _resolve
    """Attempts to resolve variable. Return true if expression tree under
this Variable changes at all.
:param trial: how many times _resolve() has been called by the originating
    :class:`Variable`? Only updates stuff if in a new trial."""                  # _resolve
    if self.trial >= trial or self.resolved or self.expr == None: return False   # _resolve
    # try to resolve dependencies first                                          # _resolve
    changed = self.expr.a._resolve(trial) or self.expr.b._resolve(trial)         # _resolve
    self.trial = trial                                                           # _resolve
    if self.expr.resolved: self._value = self.expr.value; changed = True         # _resolve
    return changed                                                               # _resolve
@_k1lib.patch(Variable)                                                          # _resolve
def _simplify(self, printStuff:bool=False):                                      # _simplify
    """Simplify system before solving"""                                         # _simplify
    self._applyF(lambda v: setattr(v, "trial", 0)); trial = 2                    # _simplify
    while self._resolve(trial): trial += 1                                       # _simplify
    if printStuff and not self.resolved: print("Can't find a solution")          # _simplify
@_k1lib.patch(Variable)                                                          # _simplify
def _solve(self, x:float) -> _Union[float, None]:                                # _solve
    """Try to solve this expression tree, given value of independent
variable."""                                                                     # _solve
    self._applyF(lambda v: v._reset()); self._simplify(); leaves = self.leaves   # _solve
    if len(leaves) > 1: raise Exception(f"System of equation has {len(leaves)} indenpendent variables. Please constrain system more!") # _solve
    elif len(leaves) == 1: next(iter(leaves))._value = x                         # _solve
    self._simplify(True); return self._value                                     # _solve
class _Constant(Variable):                                                       # _Constant
    def __init__(self, value:float):                                             # _Constant
        """Creates a constant :class:`Variable` with some specified value."""    # _Constant
        super().__init__(); self._value = value; self.isConstant = True          # _Constant