# Solve Variable and Expression Problems in Python Assignment Solution.

## Instructions

Objective
Write a program to solve variable and expression problems in python.

## Requirements and Specifications

Question 1: Evaluating expressions with respect to a variable valuation.
The function simplify above can perform all numerical computations, but stops whenever it encounters a variable. It cannot do any better, in fact, because it does not know the values of variables.
If we specify values for variables, we can then use those values in the computation, replacing each variable whose value is specified with the value itself.
A _variable valuation_ is a mapping from variables to their values; we can represent it simply as a dictionary associating to each variable a number:
Question 2: Variable Occurrences
Let us start by writing the function variables such that, if e is an expression, variables(e) is the set of variables that appear in it.
Question 3: Value equality
Now we let you write the value_equality method.
Given the two expressions $e$ and $f$, first compute the set of variables $V$ that appear in either $e$ or $f$.
Then, the idea consists in performing num_sample times the following test for equality:
* First, produce a variable assignment (a dictionary) mapping each variable in $V$ to a random value. Choose these random values from the gaussian distribution centered around 0 and with standard deviation 10 (for instance; any continuous distribution with infinite domain would work). You can obtain such numbers using [random.gauss(0, 10)](https://docs.python.org/3/library/random.html#random.gauss).
* Then, compute the values of $e$ and $f$ with respect to that variable evaluation. If the values are closer than a specified tolerance tolerance, you consider $e$ and $f$ equal (for that variable valuation). Otherwise, you can stop and return that $e$ and $f$ are different.
If you can repeat the process num_sample times, and $e$ and $f$ are considered equal every time, then you declare them equal.
Source Code
# -*- coding: utf-8 -*-
"""Copy of Homework_9_test.ipynb
Automatically generated by Colaboratory.
Original file is located at
Before you turn this problem in, make sure everything runs as expected. First, **restart the kernel** (in the menubar, select Kernel$\rightarrow$Restart) and then **run all cells** (in the menubar, select Cell$\rightarrow$Run All).
Make sure you fill in any place that says YOUR CODE HERE or "YOUR ANSWER HERE", as well as your name and collaborators below:
"""
NAME = ""
COLLABORATORS = ""
"""---
# CSE 30 Winter 2022 - Homework 9
### Instructions
Please disregard the YOUR NAME and COLLABORATORS above. They are put there atomatically by the grading tool.
You can find instructions on how to work on a homework on Canvas. Here is a short summary:
* First, click on "Runtime > Restart and run all", and check that you get no errors. This enables you to catch any error you might have introduced, and not noticed, due to your running cells out of order.
You can submit multiple times; the last submission before the deadline is the one that counts. This homework is due by **11:59pm on Wednesday 9 February, 2022**.
### Homework format
For each question in this notebook, there is:
* A text description of the problem.
* One or more places where you have to insert your solution. You need to complete every place marked:
# YOUR CODE HERE
and you should not modify any other place.
* One or more test cells. Each cell is worth some number of points, marked at the top. You should not modify these tests cells. The tests pass if no error is printed out: when there is a statement that says, for instance:
assert x == 2
then the test passes if x has value 2, and fails otherwise. You can insert a print(x) (for this case!) somewhere if you want to debug your work; it is up to you.
### Notes:
* Your code will be tested both according to the tests you can see (the assert statements you can see), _and_ additional tests. This prevents you from hard-coding the answer to the particular questions posed. Your code should solve the _general_ intended case, not hard-code the particular answer for the values used in the tests.
* **Please do not delete or add cells!** The test is autograded, and if you modify the test by adding or deleting cells, even if you re-add cells you delete, you may not receive credit.
* **Please do not import modules that are not part of the [standard library](https://docs.python.org/3/library/index.html).** You do not need any, and they will likely not available in the grading environment, leading your code to fail.
* **If you are inactive too long, your notebook might get disconnected from the back-end.** Your work is never lost, but you have to re-run all the cells before you continue.
* You can write out print statements in your code, to help you test/debug it. But remember: the code is graded on the basis of what it outputs or returns, not on the basis of what it prints.
* **TAs and tutors have access to this notebook,** so if you let them know you need their help, they can look at your work and give you advice.
Each cell where there are tests is worth a certain number of points. You get the points allocated to a cell only if you pass _all_ the tests in the cell.
The tests in a cell include both the tests you can see, and other, similar, tests that are used for grading only. Therefore, you cannot hard-code the solutions: you really have to solve the essence of the problem, to receive the points in a cell.
### Code of Conduct
* Work on the test yourself, alone.
* You can search documentation on the web, on sites such as the Python documentation sites, Stackoverflow, and similar, and you can use the results.
* You cannot share your work with others or solicit their help.
We will develop a data structure to represent arithmetic expressions containing variables, such as $3 + 4$ or $2 + x * (1 - y)$.
What is an expression? An expression consists of one of these:
1.  A number
2.  A variable
3.  If $e_1$ and $e_2$ are expressions, then $e_1 + e_2$, $e_1 - e_2$, $e_1 * e_2$, and $e_1 / e_2$ are also expressions.
Formally, the set of expressions is the _least_ set constructed according to the rules above.
Thus, an expression can be either a constant, representing numbers and variables, or a composite expression, consisting of an operator, a left expression, and a right expression.
There are (at least) two ways of representing expressions. The simplest way is to represent expressions as trees, and define operations on them.
The more sophisticated way consists in representing expressions via classes: there will be one class for variable and constants, and one class representing composite expressions; both of these classes will be subclasses of a generic "expression" class.
In this chapter, we will represent expression as trees, to gain experience with writing recursive functions on trees; in the next chapter, we will show how to represent them more elegantly as classes.
We will represent expressions as trees. A number will be represented via a number; a variable via a string, and the expression $e_1 \odot e_2$ via the tuple $(\odot, e_1, e_2)$, for $\odot \in \{+, -, *, / \}$.
For example, we will represent $2 * (x + 1)$ via:
('*', 2, ('+', 'x', 1))
"""
e = ('*', 2, ('+', 'x', 1))
"""In particular, we will consider expressions built out of the four arithmetic operators "+", "-", "*", "/".
### A compute function
Let us define a function compute() that takes one such expression, and returns the expression obtained by performing all possible numerical computation.
We consider first the simple case of an expression where the only operators that can appear are + and -, and where there are no variables.
Let us create an exception to raise when we cannot interpret an expression.
"""
class IllegalOperator(Exception):
pass
"""Let us define a helper function calc, which takes as argument an operator and two numbers, and computes the required operation. It will make it easier to write the rest of the code. """
def calc(op, left, right):     if op == "+":         return left + right     elif op == "-":         return left - right     elif op == "*":         return left * right     elif op == "/":         return left / right     else:         raise IllegalOperator(op) """With this, we can write our compute method as follows. """ def compute(e):     if isinstance(e, tuple):         # We have an expression.         op, l, r = e         # We compute the subexpressions.         ll = compute(l)         rr = compute(r)         # And on the basis of those, the whole expression.         return calc(op, ll, rr)     else:         # base expression; just return the number.         return e compute(("+", 4, 5)) compute(("+", ("-", 3, 1), ("+", 4, 9))) """## Expressions with variables 
If an expression can have variables, we can distinguish three types of expressions:
• Numbers
• Variables
• Composite expressions.
To facilitate writing code, let us define for you three helper functions that tell us the type of an expression.
"""
from numbers import Number # The mother class of all numbers.
def isnumber(e):
return isinstance(e, Number)
def isvariable(e):
return isinstance(e, str)
def iscomposite(e):
return isinstance(e, tuple)
"""The idea we use to simplify an expression is the following:
* If the expression is a Number, you return a number: it's already simplified.
* If the expression is a variable, you return the variable (that is, the expression unchanged); there is nothing to be done.
* If the expression is an operation, such as "+", "-", ..., then you consider the right and left children, and you reason:
* If all the two children are numbers, then you can compute the operation and return the result.
* Otherwise, again, there is nothing that can be done, and you return the expression unchanged.
""" def simplify(e):     if isinstance(e, tuple):         op, l, r = e         # We simplify the children expressions.         ll = simplify(l)         rr = simplify(r)         # We compute the expression if we can.         if isnumber(ll) and isnumber(rr):             return calc(op, ll, rr)         else:             return (op, ll, rr)     else:         # Leaf. No simplification is possible.         return e """Let's see how this works.""" simplify(3) simplify(("+", "x", 1)) """Yes, there was nothing we could simplify. Let's try now with something we can simplify: """ simplify(('-', 5, 4)) """Can we simplify bigger expressions? """ simplify( ('+', 6, ('-', 7, 2)) ) 
"""## Question 1: Evaluating expressions with respect to a variable valuation.
The function simplify above can perform all numerical computations, but stops whenever it encounters a variable. It cannot do any better, in fact, because it does not know the values of variables.
If we specify values for variables, we can then use those values in the computation, replacing each variable whose value is specified with the value itself.
A _variable valuation_ is a mapping from variables to their values; we can represent it simply as a dictionary associating to each variable a number:
"""
varval = {'x': 3, 'y': 8}
"""You can extend the evaluation function to take as input a variable valuation. The idea is that, when you find a variable, you try to see whether its value is specified in the variable valuation. If it is, you can replace the variable with the value, and carry on. If it is not, you leave the variable as it is, since you cannot evaluate it.
To check if a variable (a string) s is in a dictionary d, you can test
s in d
and to get the value, in case it is present, you can just do d[s] of course. We let you develop the code.
"""
### Evaluating an expression with respect to a variable evaluation
def compute(e, varval={}):
if isinstance(e, tuple):
        op, l, r = e         ll = compute(l, varval)         rr = compute(r, varval)         if isnumber(ll) and isnumber(rr):             return calc(op, ll, rr)         else:             return (op, ll, rr)     else:         if isinstance(e, str) and e in varval:             return varval[e]         return e """Let us play with this. """ ## You can modify this cell to test your code. e = ('*', 2, ('+', 'x', ('-', 3, 2))) print(compute(e)) print(compute(e, varval={'x': 6})) 
"""If we provide the values for only some of the variables, the compute function defined above, will plug in the values for those variables and perform all computations possible. Of course, if the expression contains variables for which the valuation does not specify a value, the resulting expression will still contain those variables: it will not be simply a number. In computer science, evaluating an expression as far as possible using the values for a subset of the variables is knwon as _partial evaluation_."""
## Tests for compute. 10 points. e = 'x' assert compute(e) == 'x' assert compute(e, varval={'x': 3}) == 3 e = ('*', 2, ('+', 'x', ('-', 3, 2))) assert compute(e) == ('*', 2, ('+', 'x', 1)) assert compute(e, varval={'x': 6}) == 14 assert compute(e, varval={'y': 10}) == ('*', 2, ('+', 'x', 1)) e = ('+', ('-', 'yy', 3), ('*', 'x', 4)) assert compute(e, varval={'x': 2}) == ('+', ('-', 'yy', 3), 8) assert compute(e, varval={'yy': 3}) == ('+', 0, ('*', 'x', 4)) assert compute(e, varval={'x': 2, 'yy': 3}) == 8 ### Hidden tests for compute. 10 points. """## When are two expressions equal? > _Or: it's better to be lucky than to be smart._ > _Or: if you don't know how to do it right, do it at random._ > _Or: the power of randomization._ 
We now consider the following problem: given two expressions $e$ and $f$, how can we decide whether they are equal in value, that is, whether they yield always the same value for all values of the variables?
This _"value equality"_ is a different notion from the structural equality we defined before. For instance, the two expressions ("+", "x", 1) and ("-", (*, 2, "x"), "x") are not structurally equal, but they are equal in values.
How can we test for value equality of expressions? There are two ways: the high road, and the pirate road. Of course, we take the pirate road.
The high-road approach consists in trying to demonstrate, in some way, that the two expressions are equal. One way of doing so would be to define a set of [rewriting rules](https://en.wikipedia.org/wiki/Rewriting) for expressions, that try to transform one expression into the other; this would mimick the process often done by hand to show that two expressions are equal. Another way would be to use theorem provers that can reason about expressions and real numbers, such as [PVS](https://pvs.csl.sri.com). The problem is that these approaches are a lot of work. Is there a way to be lazy, and still get the job done?
There is, it turns out. Suppose you have two expressions $f, g$ containing variable $x$ only. The idea is that if $f$ and $g$ are built with the usual operators of algebra, it is exceedingly unlikely for $f$ and $g$ to give the same value many values of $x$, and yet not be always equal. This would not be true if our expressions could contain if-then-else statements, but for the operators we defined so far, it holds. Indeed, one could be more precise, and try to come up with a theorem of the form:
> If $f$ and $g$ have "zerosity" $n$, and are equal for $n+1$ values of $x$, then they are equal for all values of $x$.
We could then try to define the "zerosity" of an expression to make this hold: for example, for two polynomials of degree at most $d$, once you show that they are equal for $d+1$ points, they must be equal everywhere ([why?](https://en.wikipedia.org/wiki/Fundamental_theorem_of_algebra)). But this again would be a smart approach, and we are trying to see if we can solve the problem while being as stupid as possible. So our idea will simply be: pick 1000 values of $x$ at random; if the two expressions are equal for all the values, then they must be equal everywhere. This is a somewhat special case of a [Monte Carlo method](https://en.wikipedia.org/wiki/Monte_Carlo_method), a method used to estimate the probability of complex phenomena (where expression equality is our phenomenon).
There are only two wrinkles with this. The first is that an expression can contain many variables, and we have to try to value assignments for all of the variables. This is easy to overcome; we just need some helper function that gives us the set of variables in a function. The second wrinkle is: how do we generate the possible value assignments? How big do these values need to be on average? According to what probability distribution? We could dive into a lot of theory and reasoning about how to compute appropriate probability distributions, but since our goal is to be stupid, we will use one of the simplest distributions with infinite domain: the Gaussian one.
## Question 2: Variable Occurrences
Let us start by writing the function variables such that, if e is an expression, variables(e) is the set of variables that appear in it.
""" ### Exercise: define variables def variables(e):     if isinstance(e, tuple):         op, l, r = e         ll = variables(l)         rr = variables(r)         return ll | rr     else:         res = set()         if isinstance(e, str):             res.add(e)         return res ### Here you can write your own test code. ### 10 points. Tests for Expr.variables e = ('*', ('+', 'x', 2), ('/', 'x', 'yay')) assert variables(e) == {'x', 'yay'} """## Question 3: Value equality Now we let you write the value_equality method. 
Given the two expressions $e$ and $f$, first compute the set of variables $V$ that appear in either $e$ or $f$.
Then, the idea consists in performing num_sample times the following test for equality:
* First, produce a variable assignment (a dictionary) mapping each variable in $V$ to a random value. Choose these random values from the gaussian distribution centered around 0 and with standard deviation 10 (for instance; any continuous distribution with infinite domain would work). You can obtain such numbers using [random.gauss(0, 10)](https://docs.python.org/3/library/random.html#random.gauss).
* Then, compute the values of $e$ and $f$ with respect to that variable evaluation. If the values are closer than a specified tolerance tolerance, you consider $e$ and $f$ equal (for that variable valuation). Otherwise, you can stop and return that $e$ and $f$ are different.
If you can repeat the process num_sample times, and $e$ and $f$ are considered equal every time, then you declare them equal.
""" ### Exercise: implementation of value equality import random def value_equality(e, f, num_samples=1000, tolerance=1e-6):     """Return True if the two expressions self and other are numerically     equivalent. Equivalence is tested by generating     num_samples assignments, and checking that equality holds     for all of them. Equality is checked up to tolerance, that is,     the values of the two expressions have to be closer than tolerance.     It can be done in less than 10 lines of code."""     ve = variables(e)     vf = variables(f)     vs = ve | vf     for _ in range(num_samples):         assignment = {}         for v in vs:             assignment[v] = random.uniform(0.001, 1000000)         res_e = compute(e, assignment)         res_f = compute(f, assignment)         if abs(res_e - res_f) > tolerance:             return False     return True ### Here you can test your solution. ### 5 points: Tests for value equality assert value_equality('x', 'x') assert value_equality(3, 3) assert not value_equality(3, 'x') e1 = ('+', ('*', 'x', 1), ('*', 'y', 0)) e2 = 'x' assert value_equality(e1, e2) e3 = ('/', ('*', 'x', 'x'), ('*', 'x', 1)) e3b = ('/', ('*', 'x', 'y'), ('*', 'x', 1)) assert value_equality(e1, e3) assert not value_equality(e1, e3b) e4 = ('/', 'y', 2) assert not value_equality(e1, e4) assert not value_equality(e3, e4) e5 = ("+", "cat", ("-", "dog", "dog")) assert value_equality(e5, "cat") ## 10 points: hidden tests for value equality. """## Symbolic Expressions The notation we developed enables the representation of _symbolic expressions:_ expressions in which not only numbers appear, but also symbols. Accordingly, we can perform _symbolic_ operations on the expressions: operations that encode what you do with pencil and paper when you work on an expression. Our first symbolic operations will be the implementation of _derivatives._ ### Derivatives Given an expression $f$ and a variable $x$, we can compute symbolically the (partial) derivative $$\frac{\partial f}{\partial x}$$ of $f$ with respect to $x$. For instance: \begin{align*} \frac{\partial}{\partial x}(x^2 + 3y + 4) & = 2x \\[1ex] \frac{\partial}{\partial x}(x^2y + xy^2 + z) & = 2yx + y^2 + z \; . \end{align*} Here, the "partial" in partial derivative simply means: if you need to take the derivative with respect to a specific variable, simply treat all other variables as constants. Computing symbolic derivatives seems complicated, but we can take it in steps. Our expression trees have leaves, consisting of * numerical constants * variables and operators, corresponding to +, -, *, /. Therefore, we break down the task into the computation of the symbolic derivative for numerical constants, variables, and arithmetic operators. The cases for the leaf nodes are: * For a constant $c$, $\partial c / \partial x = 0$. * For a variable $y \neq x$, $\partial y / \partial x = 0$. * $\partial x / \partial x = 1$. For operators, we can use: \begin{align*} \frac{\partial}{\partial x}(f \pm g) & = \frac{\partial f}{\partial x} \pm \frac {\partial g}{\partial x}, \\[1ex] \frac{\partial}{\partial x}(f \cdot g) & = \frac{\partial f}{\partial x} \cdot g + f \cdot \frac{\partial g}{\partial x}, \\[1ex] \frac{\partial}{\partial x}\left(\frac{f}{g}\right) & = \frac{\frac {\partial f}{\partial x} \cdot g - f \cdot \frac{\partial g}{\partial x}}{g^2}. \end{align*} ## Question 4: Derivative of a leaf expression Let's start from a leaf expression. The function derivate_leaf takes as argument an expression that is a leaf, and a variable, and returns the symbolic derivative of the leaf writh respect to the variable. """ ### Derivation of a leaf expression def derivate_leaf(e, x):     """This function takes as input an expression e and a variable x,     and returns the symbolic derivative of e wrt. x, as an expression."""     if isinstance(e, str):         if e == x:             return 1         else:             return 0     else:         return 0 ### You can play here with your code. """Here are some tests. """ # 5 points. assert derivate_leaf("x", "x") == 1 assert derivate_leaf("x", "y") == 0 assert derivate_leaf("y", "z") == 0 assert derivate_leaf(4, "x") == 0 """## Question 5: Derivative of an expression We now let you implement the derivative of a general expression. The function is recursive: for instance, if the expression is (*, $f$, $g$), to compute its derivative with respect to a variable $x$, we use: $$\frac{\partial}{\partial x}(f \cdot g) = \frac{\partial f}{\partial x} \cdot g + f \cdot \frac{\partial g}{\partial x}$$ and so we need to recursively call symbolic derivation on the $f$ and $g$ sub-expressions, to obtain $\partial f / \partial x$ and $\partial g / \partial x$, and then produce and return an expression representing the result. **Important:** The code for checking expression equality is _not_ able to cope with the commutativity of + and *. So, in your solution, please use _exactly_ these forms: \begin{align*} \frac{\partial}{\partial x}(f \pm g) & = \frac{\partial f}{\partial x} \pm \frac {\partial g}{\partial x}, \\[1ex] \frac{\partial}{\partial x}(f \cdot g) & = \frac{\partial f}{\partial x} \cdot g + f \cdot \frac{\partial g}{\partial x}, \\[1ex] \frac{\partial}{\partial x}\left(\frac{f}{g}\right) & = \frac{\frac {\partial f}{\partial x} \cdot g - f \cdot \frac{\partial g}{\partial x}}{g^2}. \end{align*} and not, for instance, $\frac{\partial}{\partial x}(f \cdot g) = g \cdot \frac{\partial f}{\partial x} + f \cdot \frac{\partial g}{\partial x}$, which is mathematically equivalent, but would be considered different by the tests. """ ### Implement derivate def derivate(e, x):     """Returns the derivative of e wrt x.     It can be done in less than 15 lines of code."""     if isinstance(e, tuple):         op, l, r = e         if op == '+' or op == '-':             return (op, derivate(l, x), derivate(r, x))         elif op == '*':             return ('+', ('*', derivate(l, x), r), ('*', l, derivate(r, x)))         else:             return ('/', ('-', ('*', derivate(l, x), r), ('*', l, derivate(r, x))), ('*', r, r))     else:         return derivate_leaf(e, x) ### You can play here with your code. ### 5 points: Tests for derivate for single-operator expressions assert derivate(('+', 'x', 'x'), 'x') == ('+', 1, 1) assert derivate(('-', 4, 'x'), 'x') == ('-', 0, 1) assert derivate(('*', 2, 'x'), 'x') == ('+', ('*', 0, 'x'), ('*', 2, 1)) assert derivate(('/', 2, 'x'), 'x') == ('/', ('-', ('*', 0, 'x'), ('*', 2, 1)), ('*', 'x', 'x')) ### 5 points: Hidden tests for derivate for single-operator expressions ### 10 points: Tests for derivate for composite expressions e1 = ('*', 'x', 'x') e2 = ('*', 3, 'x') num = ('-', e1, e2) e3 = ('*', 'a', 'x') den = ('+', e1, e3) e = ('/', num, den) f = ('/',  ('-',   ('*',    ('-',     ('+', ('*', 1, 'x'), ('*', 'x', 1)),     ('+', ('*', 0, 'x'), ('*', 3, 1))),    ('+', ('*', 'x', 'x'), ('*', 'a', 'x'))),   ('*',    ('-', ('*', 'x', 'x'), ('*', 3, 'x')),    ('+',     ('+', ('*', 1, 'x'), ('*', 'x', 1)),     ('+', ('*', 0, 'x'), ('*', 'a', 1))))),  ('*',   ('+', ('*', 'x', 'x'), ('*', 'a', 'x')),   ('+', ('*', 'x', 'x'), ('*', 'a', 'x')))) assert derivate(e, 'x') == f