Source code for cake.core.variables

from __future__ import annotations
from abc import ABC, abstractmethod

from cake import (
    IVariable,
    Expression,
    BasicExpression,
    BasicNode,
    Comparity, ComparitySymbol,
    Add, Divide, Multiply, Power, FloorDiv,
    Modulo,
    utils
)
from cake.basic import OtherType
from .numbers import Number, Integral
from typing import Any, Generic, TypeVar, Union
from operator import mul
from functools import reduce

from .expressions.binaries import *

U = TypeVar('U', bound=IVariable)
ResultType = Union[IVariable, BasicExpression, U]


''' Meths implemented
__pos__
__floordiv__, __rfloordiv__, __ifloordiv__
__mod__, __rmod__, __imod__,
__lshift__, __rlshift__, __ilshift__
__rshift__, __rrshift__, __irshift__
__and__, __rand__, __iand__
__xor__, __rxor__, __ixor__
__or__, __ror__, __ior__
__eq__, __ne__, __lt__, __le__, __gt__, __ge__

Unimplementable Methods
- __divmod__ : 
    needs to return a tuple, cannot really do
- __int__, __complex__, __float__, __bool__ : 
    can't convert with no existing value
- __round__, __trunc__, __floor__, __ceil__ :
    same reason as above
- __invert__ : 
    Dont see the point in it
'''
class BasicVariable(ABC):
    ''' Holds methods which will be the same for every type of Variable'''

    @abstractmethod
    def copy(self) -> U:
        raise NotImplemented

    def __pos__(self) -> U:
        return self.copy()

    def __abs__(self) -> U:
        if hasattr(self, 'coefficient'):
            if self.coefficient > 0:
                return self.copy()
            copy = self.copy()
            copy.coefficient = -copy.coefficient
            return copy
        return self

    def __floordiv__(self, other) -> Expression:
        return Expression(FloorDiv(self.copy(), other))

    def __rfloordiv__(self, other) -> Expression:
        return Expression(FloorDiv(other, self.copy()))

    __ifloordiv__ = __floordiv__

    def __mod__(self, other) -> Expression:
        return Expression(Modulo(self.copy(), other))

    def __rmod__(self, other) -> Expression:
        return Expression(Modulo(other, self.copy()))

    __imod__ = __mod__

    ''' Misc '''

    def __lshift__(self, other: OtherType) -> Expression:
        return Expression(LeftShift(self.copy(), other))

    def __rlshift__(self, other: OtherType) -> Expression:
        return Expression(LeftShift(other, self.copy()))

    __ilshift__ = __lshift__

    def __rshift__(self, other: OtherType) -> Expression:
        return Expression(RightShift(self.copy(), other))

    def __rrshift__(self, other: OtherType) -> Expression:
        return Expression(RightShift(other, self.copy()))

    __irshift__ = __rshift__

    def __and__(self, other: OtherType) -> Expression:
        return Expression(And(self.copy(), other))

    def __rand__(self, other: OtherType) -> Expression:
        return Expression(And(other, self.copy()))

    __iand__ = __and__

    def __xor__(self, other: OtherType) -> Expression:
        return Expression(Xor(self.copy(), other))

    def __rxor__(self, other: OtherType) -> Expression:
        return Expression(Xor(other, self.copy()))

    __ixor__ = __xor__

    def __or__(self, other: OtherType) -> Expression:
        return Expression(LeftShift(self.copy(), other))

    def __ror__(self, other: OtherType) -> Expression:
        return Expression(LeftShift(other, self.copy()))

    __ior__ = __or__

    ''' Comparitive methods '''

    def __eq__(self, other: OtherType) -> Union[Comparity, bool]:
        if self.__class__ != other.__class__:
            return False

        if isinstance(self, RaisedVariable):
            return Comparity(self, other, ComparitySymbol.EQUAL_TO)

        ## Variable('x', coefficient=2) -> 2x
        ## Variable('x', coefficient=3) -> 3x
        ## '2x' == '3x' == False -> False is returned.
        return str(self) == str(other)

    def __ne__(self, other: OtherType) -> Union[Comparity, bool]:
        r = (self == other)
        if isinstance(r, ComparitySymbol):
            r.symbol = ComparitySymbol.NOT_EQUAL_TO
        else:
            r = not r

        return r

    def __lt__(self, other: OtherType) -> Comparity:
        return Comparity(self.copy(), other, ComparitySymbol.LESS_THAN)

    def __le__(self, other: OtherType) -> Comparity:
        return Comparity(self.copy(), other, ComparitySymbol.LESS_OR_EQUAL_TO)

    def __gt__(self, other: OtherType) -> Comparity:
        return Comparity(self.copy(), other, ComparitySymbol.GREATER_THAN)

    def __ge__(self, other: OtherType) -> Comparity:
        return Comparity(self.copy(), other, ComparitySymbol.GREATER_OR_EQUAL_TO)


''' Meths implemented
__add__, __radd__, __iadd__
__sub__, __rsub__, __isub__
__mul__, __rmul__, __imul__, __call__
__truediv__, __rtruediv__, __itruediv__
__pow__, __rpow__, __ipow__
__neg__
'''
[docs]class Variable(IVariable, BasicVariable): ## Give a more impressive trig example later ''' An object which represents an Variable number/value, this class be integrated with other components within the cake library. .. note:: Using comparity operators such as ``>`` will not return a boolean value, however, ``==`` and ``!=`` will return a bool value when comparing unknowns. .. code-block:: py >>> x = Variable('x') >>> x > 9 Comparity(left=x, right=9, symbol='>') >>> (x > 9).fits(x=10) True >>> (x > 9).fits(x=5) False >>> (5 < x < 10).fits(x=10) False >>> (5 < x < 10).fits(x=7) True .. tip:: Using Variables in functions .. code-block:: py from cake import Sin, Variable f = Sin(Variable('x')) # Can be shown as f(x) = sin x # Can be extended to expressions not just limited to a single variable. print(f.evaluate(x=90)) # 1 ''' def __new__(cls, repr: str, coefficient: Any = 1, power: Any = 1) -> Union[Number, Variable]: if power == 0: return Integral(1) * coefficient elif coefficient == 0: return Integral(0) return super(Variable, cls).__new__(cls)
[docs] def copy(self) -> Variable: ''' Returns a shallow copy of the variable ''' return Variable(self.representation, self.coefficient, self.power)
[docs] @staticmethod def is_similar(x: Variable, y: Variable) -> bool: ''' Returns whether 2 Variables can interact with one another, so ``Variable.is_similar(3x, 4x)`` is True but ``Variable.is_similar(4y, 3x)`` is False. ''' if not (x.representation == y.representation): return False elif not (x.power == y.power): return False return True
[docs] def solve(self, value: OtherType = None, **_v) -> ResultType: ''' Solves the variable with provided values .. code-block:: py >>> x = Variable('x') >>> x *= 3 >>> x **= 2 >>> x 3x**2 >>> x.solve(5) 75 >>> x.solve(x=5) 75 >>> x = Variable('x', coefficient=Variable('y', power=2)) >>> x y**2x >>> x.solve(2, y=3) 18 >>> x.solve(x=2, y=3) 18 ''' v = value if v is None: v = _v.pop(self.representation, None) v = v or getattr(v, 'value', None) if v is None: raise ValueError('No value provided') # self.coefficient * (v ** self.power) return utils.solve_if_possible(self.coefficient, **_v) * (v ** utils.solve_if_possible(self.power, **_v))
def __add__(self, other: OtherType) -> ResultType: if isinstance(other, Variable) and self.is_similar(self, other): co = self.coefficient + other.coefficient return Variable(self.representation, co, self.power) elif isinstance(other, BasicExpression): return Expression(Add(self.copy(), other.exp)) return Expression(Add(self.copy(), other)) __radd__ = __add__ __iadd__ = __add__ def __sub__(self, other: OtherType) -> ResultType: if isinstance(other, Variable) and self.is_similar(self, other): co = self.coefficient - other.coefficient return Variable(self.representation, co, self.power) return Expression(Add(self.copy(), -other)) def __rsub__(self, other: OtherType) -> ResultType: if isinstance(other, Variable) and self.is_similar(self, other): co = other.coefficient - self.coefficient return Variable(self.representation, co, self.power) return Expression(Add(-self, other)) __isub__ = __sub__ def __mul__(self, other: OtherType) -> ResultType: if isinstance(other, BasicExpression): return other * self elif isinstance(other, Variable): if self.representation == other.representation: co = self.coefficient * other.coefficient po = self.power + other.power return Variable(self.representation, co, po) co_ef = self.coefficient * other.coefficient if c := getattr(other, '_to_type', None): ## Constant was used other = c(1, other.power) return VariableGroup(co_ef, self, other) else: return Variable(self.representation, self.coefficient * other, self.power) __rmul__ = __mul__ __imul__ = __mul__ __call__ = __mul__ __neg__ = lambda self: self * -1 def __truediv__(self, other: OtherType) -> ResultType: if isinstance(other, BasicExpression): return Expression(Divide(self.copy(), other)) elif isinstance(other, Variable) and other.representation == self.representation: coefficient = self.coefficient / other.coefficient power = self.power - other.power return Variable(self.representation, coefficient, power) elif isinstance(other, VariableGroup): return other.__rtruediv__(self) return Expression(Divide(self.copy(), other)) def __rtruediv__(self, other: OtherType) -> ResultType: if isinstance(other, BasicExpression): return Expression(Divide(other, self.copy())) elif isinstance(other, Variable) and other.representation == self.representation: coefficient = other.coefficient - self.coefficient power = other.power - self.power return Variable(self.representation, coefficient, power) elif isinstance(other, VariableGroup): return other.__truediv__(self) return Expression(Divide(other, self.copy())) __itruediv__ = __truediv__ def __pow__(self, other: OtherType, *modulo) -> ResultType: power = self.power * other coefficient = self.coefficient ** other r = Variable(self.representation, coefficient, power) if modulo: return Expression(Modulo(r, modulo[0])) return r def __rpow__(self, other: OtherType, *modulo) -> ResultType: if modulo: return RaisedVariable(base=other, power=self.copy()).__mod__(modulo[0]) return RaisedVariable(base=other, power=self.copy()) __ipow__ = __pow__
# Variables as a power ''' Meths implemented __add__, __radd__, __iadd__ __sub__, __rsub__, __isub__ __mul__, __rmul__, __imul__, __call__ __truediv__, __rtruediv__, __itruediv__ __pow__, __rpow__, __ipow__ __neg__ '''
[docs]class RaisedVariable(Generic[U], BasicNode, BasicVariable): ''' A raised Variable is where a literal or Variable value is raised to another Variable value, we use this class as it maintains logic within the library. .. note:: Most operations which are ran on this object will more then likely return expressions! .. warning:: Unlike standard variables, raised variables will always return a comparity class when comparing, to check if 2 raised variables are the same use :meth:`RaisedVariable.is_similar`. .. code-block:: py >>> I = Integral(3) >>> X = Variable('x') >>> R = I ** X RaisedVariable(3 ** x) >>> R * 2 Expression(Multiply(3 ** x, 2)) ''' def __init__(self, base: Any, power: Any = 1) -> None: self.base = base self.power = power
[docs] def copy(self) -> RaisedVariable: ''' Returns a shallow copy of the class ''' return RaisedVariable(self.base, self.power)
[docs] @staticmethod def is_similar(x: RaisedVariable, y: RaisedVariable) -> bool: ''' Checks if 2 raised variables are similar ''' if (x.base == y.base) and (x.power == y.power): return True return False
def solve(self, **kwds) -> ResultType: base = utils.solve_if_possible(self.base, **kwds) power = utils.solve_if_possible(self.power, **kwds) return base ** power def __repr__(self) -> str: return f'RaisedVariable(base={self.base}, power={self.power})' def __str__(self) -> str: return f'{self.base} ** {self.power}' def __add__(self, other: OtherType) -> Expression: return Expression(Add(self, other)) __radd__ = __add__ __iadd__ = __add__ def __sub__(self, other: OtherType) -> Expression: return Expression(Add(self.copy(), -other)) def __rsub__(self, other: OtherType) -> Expression: return Expression(Add(-self, other)) __isub__ = __sub__ def __mul__(self, other: OtherType) -> Expression: return Expression(Multiply(self.copy(), other)) __rmul__ = __mul__ __imul__ = __mul__ __call__ = __mul__ def __neg__(self) -> RaisedVariable: return RaisedVariable(-self.base, self.power) def __truediv__(self, other: OtherType) -> Expression: return Expression(Divide(self.copy(), other)) def __rtruediv__(self, other: OtherType) -> Expression: return Expression(Divide(other, self.copy())) __itruediv__ = __truediv__ def __pow__(self, other: OtherType, *modulo) -> RaisedVariable: if modulo: return Expression(Modulo(RaisedVariable(self.base, self.power * other), modulo[0])) return RaisedVariable(self.base, self.power * other) def __rpow__(self, other: OtherType, *modulo) -> ResultType: if hasattr(other, 'power'): other = getattr(other, 'copy', lambda: other)() other.power = other.power * self return other if modulo: return Expression(Modulo(Power(other, self), modulo[0])) return Expression(Power(other, self)) __ipow__ = __pow__
# Variable groups ''' Meths Implemented __add__, __radd__, __iadd__ __sub__, __rsub__, __isub__ __mul__, __rmul__, __imul__, __neg__ __truediv__, __rtruediv__, __itruediv__ __pow__, __rpow__, __ipow__ '''
[docs]class VariableGroup(Generic[U], BasicNode, BasicVariable): ''' An Variable group is used where multiple Variables make up a single Variable value, So, ``5x`` is an Variable whereas ``5x * y`` would be an VariableGroup as theres 2 values. .. warning:: Reading coffecients from :attr:`VariableGroup.groups` will always be one, instead use :attr:`VariableGroup.coefficient`. .. note:: Groups generally shouldn't need to be made manually, they can easily be made by manipulating standard Variables. .. code-block:: py >>> x, y = Variable.gen_many('x', 'y') >>> g = x * y >>> g VariableGroup(xy) >>> g.groups [Variable('x'), Variable('y')] >>> g * y VariableGroup(xy**2) # Groups == [Variable('x'), Variable('y', power=2, ...)] >>> g + y Expression(xy + y) ''' def __new__(cls, coefficient: Any, *Variables) -> None: if coefficient == 0 or len(Variables) == 0: return Integral(0) elif len(Variables) == 1: return Variables[0] return super(VariableGroup, cls).__new__(cls) def __init__(self, coefficient: Any, *Variables) -> None: self.coefficient = coefficient self.power = None self.groups = [] for group in Variables: if isinstance(group, Variable): group = Variable(group.representation, 1, group.power) self.groups.append(group) else: self.coefficient *= group def __repr__(self) -> str: return f'VariableGroup({self.__str__()})' def __str__(self) -> str: return (str(self.coefficient) if self.coefficient != 1 else '') + ''.join(map(lambda x: f'{x.representation}{f"**{x.power}" if x.power != 1 else ""}', self.groups))
[docs] @staticmethod def is_similar(x: VariableGroup, y: VariableGroup) -> bool: ''' Checks whether 2 variable groups are similar, meaning they can interact with each other. This interaction includes adding, subtracting, division and more. .. code-block:: py >>> x, y, z = Variable.many('x', 'y', 'z') >>> VariableGroup.is_similar(x * y, x * z) False >>> VariableGroup.is_similar(x * y, x * y) True >>> VariableGroup.is_similar(x * x * y, x * y) False Parameters ---------- x, y: :class:`VariableGroup` 2 Groups to be compared. ''' if len(x.groups) != len(y.groups): return False x = {f'{i.representation}**{i.power}' for i in x.groups} y = {f'{i.representation}**{i.power}' for i in x.groups} return x == y
[docs] @classmethod def is_roughly_similar(x: VariableGroup, y: VariableGroup) -> bool: ''' Checks if 2 variable groups are roughly similar, meaning they can broadly interact. This interaction doesn't allow for adding and subtracting. .. code-block:: py >>> x, y, z = Variable.many('x', 'y', 'z') >>> VariableGroup.is_roughly_similar(x * y, x * y) True >>> VariableGroup.is_roughly_similar(x * x * y, x * y) True >>> VariableGroup.is_roughly_similar(x * y, x * z) False Parameters ---------- x, y: :class:`VariableGroup` 2 Groups to be compared. ''' x_k = set(x.as_mapping().keys()) y_k = set(y.as_mapping().keys()) return x_k == y_k
@property def representation(self) -> str: ''' Returns how the group is represented as, without the group coefficient. ''' return ''.join(i.representation for i in self.groups)
[docs] def as_mapping(self) -> dict: ''' Generates a mapping of the group. .. code-block:: py >>> g = x * y >>> g.as_mapping() { 'x': Unknown('x') 'y': Unknown('y') } >>> g *= 2 >>> g.as_mapping() { 'x': Unknown('x', coefficient=2), 'y': Unknown('y', coefficient=2) } ''' d = dict() for u in self.groups: if u.representation not in d: d[u.representation] = u else: d[u.representation] *= u return d
[docs] def copy(self) -> VariableGroup: ''' Returns a shallow copy of the group ''' return VariableGroup(self.coefficient, *self.groups)
[docs] def solve(self, **values) -> ResultType: ''' Generates a value for the group using inputted values. .. code-block:: py >>> g = x * y >>> g.solve(x=2, y=2) 4 >>> g.solve(x=2) 2y >>> g.solve() xy ''' results = [] for node in self.groups: if node.representation in values: results.append(values[node.representation] ** node.power) return Number.convert(reduce(mul, results)) * self.coefficient
''' Wrapped methods for handling these groups ''' def __add__(self, other: OtherType) -> ResultType: if isinstance(other, VariableGroup): if not self.is_similar(self, other): return Expression(Add(self.copy(), other.copy())) return VariableGroup(self.coefficient + other.coefficient, *self.groups) return Expression(Add(self.copy(), other)) __radd__ = __add__ __iadd__ = __add__ def __sub__(self, other: OtherType) -> ResultType: if isinstance(other, VariableGroup): if not self.is_similar(self, other): return Expression(Add(self.copy(), -other)) return VariableGroup(self.coefficient - other.coefficient, *self.groups) return Expression(Add(self.copy(), -other)) def __rsub__(self, other: OtherType) -> ResultType: if isinstance(other, VariableGroup): if not self.is_similar(self, other): return Expression(Add(-self, other.copy())) return VariableGroup(-self.coefficient + other.coefficient, *self.groups) return Expression(Add(-self, other)) __isub__ = __sub__ def __mul__(self, other: OtherType) -> None: if isinstance(other, VariableGroup): coefficient = self.coefficient * other.coefficient mapping = self.as_mapping() for co in other.groups: co.coefficient = 1 if rep := co.representation in mapping: mapping[rep] = mapping[rep] * co else: mapping[rep] = co nodes = mapping.values() return VariableGroup(coefficient, *nodes) elif isinstance(other, Variable): coefficient = self.coefficient * other.coefficient mapping = self.as_mapping() for node in mapping: if other.representation == node: mapping[node] = mapping[node] * other break else: mapping[other.representation] = other return VariableGroup(coefficient, *mapping.values()) coefficient = self.coefficient * other return VariableGroup(coefficient, *self.groups) __rmul__ = __mul__ __imul__ = __mul__ __call__ = __mul__ def __neg__(self) -> VariableGroup: return VariableGroup(self.coefficient * -1, *self.groups) def __truediv__(self, other: OtherType) -> ResultType: if isinstance(other, VariableGroup): coefficient = self.coefficient / other.coefficient mapping = self.as_mapping() remaining = [] for group in other.groups: if group.representation in mapping: g1 = mapping[group.representation] g2 = g1 / group mapping[group.representation] = g2 else: remaining.append(group) top = VariableGroup(coefficient, *mapping.values()) if not remaining: return top r = remaining.pop(0) while remaining: r *= remaining.pop(0) return Expression(Divide(top, r)) elif isinstance(other, Variable): mapping = self.as_mapping() if other.representation in mapping: g = mapping[other.representation] / other mapping[other.representation] = g coefficient = self.coefficient / other.coefficient return VariableGroup(coefficient, *mapping.values()) return Expression(Divide(self.copy(), other)) elif isinstance(other, BasicExpression): return Expression(Divide(self.copy(), other)) return VariableGroup(self.coefficient / other, *self.groups) def __rtruediv__(self, other: OtherType) -> ResultType: if isinstance(other, VariableGroup): coefficient = other.coefficient / self.coefficient mapping = other.as_mapping() remaining = [] for group in self.groups: if group.representation in mapping: g1 = mapping[group.representation] g2 = g1 / group mapping[group.representation] = g2 else: remaining.append(group) top = VariableGroup(coefficient, *mapping.values()) if not remaining: return top r = remaining.pop(0) while remaining: r *= remaining.pop(0) return Expression(Divide(top, r)) elif isinstance(other, Variable): mapping = self.as_mapping() if other.representation in mapping: g = other / mapping[other.representation] if isinstance(g, Number): mapping.pop(other.representation) bottom = VariableGroup(self.coefficient, *mapping.values()) return Expression(Divide(g, bottom)) return Expression(Divide(other, self.copy())) elif isinstance(other, BasicExpression): return Expression(Divide(other, self.copy())) return VariableGroup(other / self.coefficient, *self.groups) __itruediv__ = __truediv__ def __pow__(self, other: OtherType, *modulo) -> VariableGroup: mapping = self.as_mapping() coefficient = self.coefficient ** other result = VariableGroup(coefficient, *map(lambda x: x ** other, mapping.values())) if modulo: return result % modulo[0] return result def __rpow__(self, other: OtherType, *modulo) -> Expression: if modulo: return Expression(Modulo(Power(other, self.copy()), modulo[0])) return Expression(Power(other, self.copy())) __ipow__ = __pow__