# Working with Symbolic Expressions

## 0.1 Finding an exact derivative with a computer algebra system

## 0.2 Doing symbolic algebra in Python


```python
from math import sin
def f(x):
    return (3*x**2 + x) * sin(x)
```

# 1 Modeling algebraic expressions

## 1.1 Breaking an expression into pieces

## 1.2 Building an expression tree

## 1.3 Translating the expression tree to Python


```python
class Power():
    def __init__(self,base,exponent):
        self.base = base
        self.exponent = exponent
```


```python
class Number():
    def __init__(self,number):
        self.number = number

class Variable():
    def __init__(self,symbol):
        self.symbol = symbol
```

This represents $x^2$:


```python
Power(Variable("x"),Number(2))
```




    <__main__.Power at 0x2d030c86710>




```python
class Product():
    def __init__(self, exp1, exp2):
        self.exp1 = exp1
        self.exp2 = exp2
```

This represents $3x^2$:


```python
Product(Number(3),Power(Variable("x"),Number(2)))
```




    <__main__.Product at 0x2d030c86dd8>




```python
class Sum():
    def __init__(self, *exps): #<1>
        self.exps = exps

class Function(): #<2>
    def __init__(self,name):
        self.name = name

class Apply(): #<3>
    def __init__(self,function,argument):
        self.function = function
        self.argument = argument

f_expression = Product( #<4>
                Sum(
                    Product(
                        Number(3),
                        Power(
                            Variable("x"),
                            Number(2))), 
                    Variable("x")), 
                Apply(
                    Function("sin"),
                    Variable("x")))
```

This represents $\cos(x^3 + -5)$:


```python
Apply(Function("cos"),Sum(Power(Variable("x"),Number("3")), Number(-5)))
```




    <__main__.Apply at 0x2d030ca1390>



## 1.4 Exercises

**Exercise:** Draw the expression $\ln(y^z)$ as a tree built out of elements and combinators from this section.

**Exercise:** Translate the expression from the previous exercise to Python code.  Write it both as a Python function and as a data structure built from elements and combinators.

**Solution:** Here's the ordinary Python function


```python
from math import log
def f(y,z):
    return log(y**z)
```

Here's the data structure:


```python
Apply(Function("ln"), Power(Variable("y"), Variable("z")))
```




    <__main__.Apply at 0x2d030ca17f0>



**Exercise:** Implement a “Quotient” combinator representing one expression divided by another.  How do you represent the following expression? $$\frac{a+b}{2}$$


```python
class Quotient():
    def __init__(self,numerator,denominator):
        self.numerator = numerator
        self.denominator = denominator
```

Here's the representation of $(a+b)/2$:


```python
Quotient(Sum(Variable("a"),Variable("b")),Number(2))
```




    <__main__.Quotient at 0x2d030ca1a20>



**Exercise:** Implement a `Difference` combinator representing one expression subtracted from another.  How can you represent the expression $b^2 - 4ac$?

**Solution:**


```python
class Difference():
    def __init__(self,exp1,exp2):
        self.exp1 = exp1
        self.exp2 = exp2
```

$b^2 - 4ac$ is then represented by:


```python
Difference(
    Power(Variable('b'),Number(2)),
    Product(Number(4),Product(Variable('a'), Variable('c'))))
```




    <__main__.Difference at 0x2d030ca8240>



**Exercise:** Implement a `Negative` combinator, representing the negation of an expression.  For example, the negation of $x^2 + y$  is $-(x^2 + y)$.  Represent the latter expression in code using your new combinator.


```python
class Negative():
    def __init__(self,exp):
        self.exp = exp
```

$-(x^2 + y)$ is represented by:


```python
Negative(Sum(Power(Variable("x"),Number(2)),Variable("y")))
```




    <__main__.Negative at 0x2d030ca83c8>



**Exercise:** Add a Function called `"sqrt"` representing a square root, and use it to encode the following formula:

$$\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$$


```python
A = Variable('a')
B = Variable('b')
C = Variable('c')
Sqrt = Function('sqrt')
```


```python
Quotient(
    Sum(
        Negative(B),
        Apply(
            Sqrt, 
            Difference(
                Power(B,Number(2)),
                Product(Number(4), Product(A,C))))),
    Product(Number(2), A))
```




    <__main__.Quotient at 0x2d030ca8d30>



**Mini-project:** Create an abstract base class called Expression and make all of the elements and combinators inherit from it.  For instance, class Variable() should become class Variable(Expression).  Then, overload the Python arithmetic operations +, -, *, and / so they produce Expression objects.  For instance, the code 2 * Variable(“x”) + 3 should yield: Sum(Product(Number(2), Variable(“x”)), Number(3)).

**Solution:** see "expressions.py" file, and section 2.2 and beyond below.

# 2 Putting a symbolic expression to work

## 2.1 Finding all the variables in an expression


```python
def distinct_variables(exp):
    if isinstance(exp, Variable):
        return set(exp.symbol)
    elif isinstance(exp, Number):
        return set()
    elif isinstance(exp, Sum):
        return set().union(*[distinct_variables(exp) for exp in exp.exps])
    elif isinstance(exp, Product):
        return distinct_variables(exp.exp1).union(distinct_variables(exp.exp2))
    elif isinstance(exp, Power):
        return distinct_variables(exp.base).union(distinct_variables(exp.exponent))
    elif isinstance(exp, Apply):
        return distinct_variables(exp.argument)
    else:
        raise TypeError("Not a valid expression.")
```


```python
distinct_variables(Variable("z"))
```




    {'z'}




```python
distinct_variables(Number(3))
```




    set()




```python
distinct_variables(f_expression)
```




    {'x'}



## 2.2 Evaluating an expression


```python
from abc import ABC, abstractmethod

class Expression(ABC):
    @abstractmethod
    def evaluate(self, **bindings):
        pass
```

Note: we are redefining these classes now.


```python
class Number(Expression):
    def __init__(self,number):
        self.number = number
    def evaluate(self, **bindings):
        return self.number
    
class Variable(Expression):
    def __init__(self,symbol):
        self.symbol = symbol
    def evaluate(self, **bindings):
        try:
            return bindings[self.symbol]
        except:
            raise KeyError("Variable '{}' is not bound.".format(self.symbol))
            
class Product(Expression):
    def __init__(self, exp1, exp2):
        self.exp1 = exp1
        self.exp2 = exp2
    def evaluate(self, **bindings):
        return self.exp1.evaluate(**bindings) * self.exp2.evaluate(**bindings)
```


```python
Product(Variable("x"), Variable("y")).evaluate(x=2,y=5)
```




    10




```python
import math
from math import sin, cos, log

_function_bindings = {
    "sin": math.sin,
    "cos": math.cos,
    "ln": math.log
}

class Apply(Expression):
    def __init__(self,function,argument):
        self.function = function
        self.argument = argument
    def evaluate(self, **bindings):
        return _function_bindings[self.function.name](self.argument.evaluate(**bindings))
```

From the text: *... Similarly, we can add an “evaluate” method to the Sum, Power, Difference, or Quotient combinators....*


```python
class Sum(Expression):
    def __init__(self, *exps):
        self.exps = exps
    def evaluate(self, **bindings):
        return sum([exp.evaluate(**bindings) for exp in self.exps])
    
class Power(Expression):
    def __init__(self,base,exponent):
        self.base = base
        self.exponent = exponent
    def evaluate(self, **bindings):
        return self.base.evaluate(**bindings) ** self.exponent.evaluate(**bindings)
    
class Difference(Expression):
    def __init__(self,exp1,exp2):
        self.exp1 = exp1
        self.exp2 = exp2
    def evaluate(self, **bindings):
        return self.exp1.evaluate(**bindings) - self.exp2.evaluate(**bindings)
    
class Quotient(Expression):
    def __init__(self,numerator,denominator):
        self.numerator = numerator
        self.denominator = denominator
    def evaluate(self, **bindings):
        return self.numerator.evaluate(**bindings) / self.denominator.evaluate(**bindings)
```

Redefine `f_expression` in light of the new class definitions


```python
f_expression = Product( #<4>
                Sum(
                    Product(
                        Number(3),
                        Power(
                            Variable("x"),
                            Number(2))), 
                    Variable("x")), 
                Apply(
                    Function("sin"),
                    Variable("x")))
```


```python
f_expression.evaluate(x=5)
```




    -76.71394197305108




```python
from math import sin
def f(x):
    return (3*x**2 + x) * sin(x)

f(5)
```




    -76.71394197305108



## 2.3 Expanding an expression


```python
class Expression(ABC):
    @abstractmethod
    def evaluate(self, **bindings):
        pass
    @abstractmethod
    def expand(self):
        pass
    
    # Printing expressions legibly in REPL (See first mini project in 2.4)
    @abstractmethod
    def display(self):
        pass
    def __repr__(self):
        return self.display()
```


```python
class Sum(Expression):
    def __init__(self, *exps):
        self.exps = exps
    def evaluate(self, **bindings):
        return sum([exp.evaluate(**bindings) for exp in self.exps])
    def expand(self):
        return Sum(*[exp.expand() for exp in self.exps])
    def display(self):
        return "Sum({})".format(",".join([e.display() for e in self.exps]))
    
class Product(Expression):
    def __init__(self, exp1, exp2):
        self.exp1 = exp1
        self.exp2 = exp2
    def evaluate(self, **bindings):
        return self.exp1.evaluate(**bindings) * self.exp2.evaluate(**bindings)
    def expand(self):
        expanded1 = self.exp1.expand()
        expanded2 = self.exp2.expand()
        if isinstance(expanded1, Sum):
            return Sum(*[Product(e,expanded2).expand() for e in expanded1.exps])
        elif isinstance(expanded2, Sum):
            return Sum(*[Product(expanded1,e) for e in expanded2.exps])
        else:
            return Product(expanded1,expanded2)
    def display(self):
        return "Product({},{})".format(self.exp1.display(),self.exp2.display())
        
class Difference(Expression):
    def __init__(self,exp1,exp2):
        self.exp1 = exp1
        self.exp2 = exp2
    def evaluate(self, **bindings):
        return self.exp1.evaluate(**bindings) - self.exp2.evaluate(**bindings)
    def expand(self):
        return self
    def display(self):
        return "Difference({},{})".format(self.exp1.display(), self.exp2.display())
    
class Quotient(Expression):
    def __init__(self,numerator,denominator):
        self.numerator = numerator
        self.denominator = denominator
    def evaluate(self, **bindings):
        return self.numerator.evaluate(**bindings) / self.denominator.evaluate(**bindings)
    def expand(self):
        return self
    def display(self):
        return "Quotient({},{})".format(self.numerator.display(),self.denominator.display())
    
class Negative(Expression):
    def __init__(self,exp):
        self.exp = exp
    def evaluate(self, **bindings):
        return - self.exp.evaluate(**bindings)
    def expand(self):
        return self
    def display(self):
        return "Negative({})".format(self.exp.display())
    
class Number(Expression):
    def __init__(self,number):
        self.number = number
    def evaluate(self, **bindings):
        return self.number
    def expand(self):
        return self
    def display(self):
        return "Number({})".format(self.number)
    
class Power(Expression):
    def __init__(self,base,exponent):
        self.base = base
        self.exponent = exponent
    def evaluate(self, **bindings):
        return self.base.evaluate(**bindings) ** self.exponent.evaluate(**bindings)
    def expand(self):
        return self
    def display(self):
        return "Power({},{})".format(self.base.display(),self.exponent.display())
    
class Variable(Expression):
    def __init__(self,symbol):
        self.symbol = symbol
    def evaluate(self, **bindings):
        return bindings[self.symbol]
    def expand(self):
        return self
    def display(self):
        return "Variable(\"{}\")".format(self.symbol)
    
class Function():
    def __init__(self,name,make_latex=None):
        self.name = name
        self.make_latex = make_latex
    def latex(self,arg_latex):
        if self.make_latex:
            return self.make_latex(arg_latex)
        else:
            return " \\operatorname{{ {} }} \\left( {} \\right)".format(self.name, arg_latex)
  
class Apply(Expression):
    def __init__(self,function,argument):
        self.function = function
        self.argument = argument
    def evaluate(self, **bindings):
        return _function_bindings[self.function.name](self.argument.evaluate(**bindings))
    def expand(self):
        return Apply(self.function, self.argument.expand())
    def display(self):
        return "Apply(Function(\"{}\"),{})".format(self.function.name, self.argument.display())
```


```python
Y = Variable('y')
Z = Variable('z')
A = Variable('a')
B = Variable('b')
Product(Sum(A,B),Sum(Y,Z))
```




    Product(Sum(Variable("a"),Variable("b")),Sum(Variable("y"),Variable("z")))




```python
Product(Sum(A,B),Sum(Y,Z)).expand()
```




    Sum(Sum(Product(Variable("a"),Variable("y")),Product(Variable("a"),Variable("z"))),Sum(Product(Variable("b"),Variable("y")),Product(Variable("b"),Variable("z"))))




```python
f_expression = Product( #<4>
                Sum(
                    Product(
                        Number(3),
                        Power(
                            Variable("x"),
                            Number(2))), 
                    Variable("x")), 
                Apply(
                    Function("sin"),
                    Variable("x")))
```


```python
f_expression.expand()
```




    Sum(Product(Product(Number(3),Power(Variable("x"),Number(2))),Apply(Function("sin"),Variable("x"))),Product(Variable("x"),Apply(Function("sin"),Variable("x"))))



## 2.4 Exercises

**Exercise:** Write a function `contains(expression, variable)` which checks whether the given expression contains any occurence of the specified variable.


```python
def contains(exp, var):
    if isinstance(exp, Variable):
        return exp.symbol == var.symbol
    elif isinstance(exp, Number):
        return False
    elif isinstance(exp, Sum):
        return any([contains(e,var) for e in exp.exps])
    elif isinstance(exp, Product):
        return contains(exp.exp1,var) or contains(exp.exp2,var)
    elif isinstance(exp, Power):
        return contains(exp.base, var) or contains(exp.exponent, var)
    elif isinstance(exp, Apply):
        return contains(exp.argument, var)
    else:
        raise TypeError("Not a valid expression.")
```

**Exercise:** Write a “distinct_functions” function which takes an expression as an argument and returns the distinct, named functions like “sin” or “ln” that appear in the expression.


```python
def distinct_functions(exp):
    if isinstance(exp, Variable):
        return set()
    elif isinstance(exp, Number):
        return set()
    elif isinstance(exp, Sum):
        return set().union(*[distinct_functions(exp) for exp in exp.exps])
    elif isinstance(exp, Product):
        return distinct_functions(exp.exp1).union(distinct_functions(exp.exp2))
    elif isinstance(exp, Power):
        return distinct_functions(exp.base).union(distinct_functions(exp.exponent))
    elif isinstance(exp, Apply):
        return set([exp.function.name]).union(distinct_functions(exp.argument))
    else:
        raise TypeError("Not a valid expression.")
```

**Exercise:** Write a function contains_sum which takes an expression and returns True if it contains a Sum and returns False otherwise.


```python
def contains_sum(exp):
    if isinstance(exp, Variable):
        return False
    elif isinstance(exp, Number):
        return False
    elif isinstance(exp, Sum):
        return True
    elif isinstance(exp, Product):
        return contains_sum(exp.exp1) or contains_sum(exp.exp2)
    elif isinstance(exp, Power):
        return contains_sum(exp.base) or contains_sum(exp.exponent)
    elif isinstance(exp, Apply):
        return contains_sum(exp.argument)
    else:
        raise TypeError("Not a valid expression.")
```

**NOTE:** For the rest of the mini-projects, consult "expressions.py".

# 3 Finding the derivative of a function

For the rest of the notebook, I'll use the complete implementations from `expressions.py` so I don't have to re-implement every time.


```python
from expressions import *
```


```python
Product(Power(Variable("x"),Number(2)),Apply(Function("sin"),Variable("y")))
```




$$x ^ { 2 } \operatorname{ sin } \left( y \right)$$



## 3.1 Derivatives of powers

## 3.2 Derivatives of transformed functions

## 3.3 Derivatives of some special functions

## 3.4 Derivatives of products and compositions


## 3.5 Exercises

# 4 Taking derivatives automatically

## 4.1 Implementing a derivative method for expressions


```python
Sum(Variable("x"),Variable("c"),Number(1)).derivative(Variable("x"))
```




$$1 + 0 + 0$$



## 4.2 Implementing the product rule and chain rule


```python
Product(Variable("c"),Variable("x")).derivative(Variable("x"))
```




$$c\cdot 1$$




```python
Apply(Function("sin"),Power(Variable("x"),Number(2))).derivative(x)
```




$$1\cdot 2x ^ { 1 } \operatorname{ cos } \left( x ^ { 2 } \right)$$



## 4.3 Implementing the power rule


```python
f_expression = Product( #<4>
                Sum(
                    Product(
                        Number(3),
                        Power(
                            Variable("x"),
                            Number(2))), 
                    Variable("x")), 
                Apply(
                    Function("sin"),
                    Variable("x")))
```


```python
f_expression.derivative(x)
```




$$\left( 3\cdot 1\cdot 2x ^ { 1 } + 1 \right) \operatorname{ sin } \left( x \right) + \left( 3x ^ { 2 } + x \right)\cdot 1 \operatorname{ cos } \left( x \right)$$



## 4.4 Exercises

# 5 Integrating functions symbolically 

## 5.1 Integrals as antiderivatives


## 5.2 Introducing the SymPy library


```python
from sympy import *
from sympy.core.core import *
Mul(Symbol('y'),Add(3,Symbol('x')))
```




    y*(x + 3)




```python
y = Symbol('y')
```


```python
x = Symbol('x')
```


```python
y*(3+x)
```




    y*(x + 3)




```python
y*(3+x).subs(x,1)
```




    4*y




```python
(x**2).diff(x)
```




    2*x




```python
(3*x**2).integrate(x)
```




    x**3



## 5.3 Exercises

**Exercise:** What is the integral of $f(x) = 0$?  Confirm your answer with SymPy, remembering that SymPy does not automatically include a constant of integration.


```python
Integer(0).integrate(x)
```




    0



**Exercise:** What is the integral of $x\cdot \cos(x)$?  Hint: look at the derivative of $x\sin(x)$.  Confirm your answer with SymPy.


```python
(x*cos(x)).integrate(x)
```




    x*sin(x) + cos(x)



**Exercise:** What is the integral of $x^2$?  Confirm your answer with SymPy.


```python
(x**2).integrate(x)
```




    x**3/3



53df6b37cc833b64cf9a9a1c1e79b2fdd8e8093d
ipynb
Jupyter Notebook
Chapter 10/ch10 walkthrough.ipynb
ZhangXinNan/MathForProgrammers
fe720a37390de951cc9d0a7de975ce32cb43bc4b
MIT
Chapter 10/ch10 walkthrough.ipynb
ZhangXinNan/MathForProgrammers
fe720a37390de951cc9d0a7de975ce32cb43bc4b
MIT
Chapter 10/ch10 walkthrough.ipynb
ZhangXinNan/MathForProgrammers
fe720a37390de951cc9d0a7de975ce32cb43bc4b
MIT
Qwen/Qwen-72B
1. YES
2. YES