

# Programming Language With Numerical Methods
<heads>
Joanna Kozuchowska, Msc
    
## Class 13. Roots of equations.

**Whenever you learn a new feature, you should try it out in interactive mode and make errors on purpose to see what goes wrong and what types of errors you run into.**

## Examples using built-in functions

Each of the methods presented below have some additional configarable parameters like the tolerance for the spacing between the subsequent values of $x$ (`xtol`) or values of $f(x)$ (`rtol`), maximal number of iterations (`maxiter`).


```python
import numpy as np

x = np.linspace(-1.5, 1.5, 200)
y = x**4 - (2*x+1)
```


```python
import matplotlib.pyplot as plt

plt.plot(x, y)
plt.plot(x, np.zeros_like(x))
plt.show()
```

### Bisection


```python
import scipy.optimize as optimize
```


```python
optimize.bisect(lambda x: x**4 - (2*x+1), 1, 2)
```




    1.395336994466561




```python
optimize.bisect(lambda x: x**4 - (2*x+1), 1, 2,  rtol=10**-5)
```




    1.3953323364257812




```python
def fx(x):
    return x**4 - (2*x+1)

optimize.bisect(fx, 1, 2)
```




    1.395336994466561



### Ridder's method


```python
import scipy.optimize as optimize

def fx(x):
    return x**4 - (2*x+1)

optimize.ridder(fx, 1, 2)
```




    1.3953369944679295



### Newton-Raphson method


```python
def fx(x):
    return x**4 - (2*x+1)
    
def fprimex(x): # derivative of fx
    return 4 * x**3 - 2

optimize.newton(fx, 1, fprime=fprimex)
```




    1.395336994467073




```python
# automatic computation of a derivative
import sympy

s_x = sympy.symbols("x") # define variables of the function
s_f = s_x ** 4 - (2 * s_x +1) # define a symbolic function using the variable
f = lambda x: sympy.lambdify(s_x, s_f, 'numpy')(x) # convert function to lambda for quick evaluation
fp = lambda x: sympy.lambdify(s_x, sympy.diff(s_f, s_x), 'numpy')(x) # compute symbolic derivative and convert to lambda
```


```python
optimize.newton(f, 1.5, fprime=fp)
```




    1.395336994467073



### Secant method


```python
import scipy.optimize as optimize

def fx(x):
    return x**4 - (2*x+1)

optimize.newton(lambda x: x**4 - (2*x+1), 1)
```




    1.3953369944670728



## Exercises<a name="exercises"></a>

While finding the solution using an iterative method, couple approaches are available to decide when to stop the calculation:
- limited number of steps
- $|f(x)| \leq \text{tolerance}$
- $|x_i - x_{i+1}| \leq \text{tolerance}$


**For the exercises below, try implementing your own functions, then, check if the obtained solution is similar to the results given by `scipy.optimize` functions.** 

### Exercise 1

A root of $$x^3 - 10x^2 + 5 = 0$$ lies in the interval $(0,\ 1)$. Compute this root to within $10^{-4}$ using any algorithm of your choice.


```python
# using the bisection method from exercise 3:
def bisection(a, b, f, max_iterations=100, tol=0.00001):
    """
    (a, b) -- bracketing range of the root
    f -- function definition
    max_iterations -- maximal number of iterations
    tol -- tolerance of the result, here abs(f(c)) < tol 
    
    returns:
    c -- the root of the function f found with a given tolerance or None if a-b does not bracket the root
    k - number of iterations
    """
    
    fa = f(a)
    fb = f(b)
    # check the root bracketing:
    if fa * fb > 0:
        print(f"{a=} and {b=} do not bracket the root!")
        return None
    # counting iterations
    k = 0

    while k < max_iterations:
        k += 1
        c = (a + b) / 2
        fc = fx(c)
        if abs(fc) <= 0.0001 or fc == 0:
            break
        if fc * fa < 0:
            b = c
            fb = fc
        else:
            a = c
            fa = fc

    return c, k
```


```python
def fx(x):
    return x ** 3 - 10 * x ** 2 + 5

x_0, k = bisection(0, 1, fx, tol=0.0001)
print(x_0, fx(x_0))
```

    0.7346038818359375 -4.889966309207239e-06


### Exercise 2

Sketch the graphs of $y = x$ and $y = 2 \sin x$. Use the bisection method to find an approximation to within $10^{-5}$ to the first positive value of $x$ with $x = 2 \sin x$.


```python
def f(x):
    return x

def g(x):
    return 2 * np.sin(x)

x = np.linspace(0, np.pi)
yx = f(x)
ysin = g(x)
plt.plot(x, yx)
plt.plot(x, ysin)
plt.hlines(0, xmin=0, xmax=np.pi, color="k")
plt.show()
```


```python
# skipping the rest of the solution as I have no idea what I meant
```

### Exercise 3

Use bisection to find the root of
    $$ x^3 - 10x^2 + 5 = 0$$
knowing that it lies in the interval $(0,\ 1)$. Find the solution to 	four-digit accuracy. How many function evaluations are involved in the procedure?


```python
def fx(x):
    return x**3 - 10 * x**2 + 5

a = 0
b = 1
# print(fx(a), fx(b), fx(a)*fx(b))
fa = fx(a)
fb = fx(b)
k = 0

while k < 100:
    c = (a + b) / 2
    fc = fx(c)
    if abs(fc) < 0.0001 or fc == 0:
        break
    if fc * fa < 0:
        b = c
        fb = fc
    else:
        a = c
        fa = fc
    k += 1
print(k)
print(fc)
```

    15
    -4.889966309207239e-06


### Exercise 4

Use the Newton-Raphson method to obtain successive approximations of $\sqrt{2}$. Stop computation when:
- $|f(x)| < 10^{-6}$
- $x_{k+1} -x_{k}<10^{-4}$


```python
# x ** 2 = 2 -> x = sqrt(2)
# f(x) = x**2 - 2 = 0

def f_root(x):
    return x ** 2 - 2

def f_rootp(x):
    return 2 * x

# difference between approximations < 0.0001 
x_0 = optimize.newton(f_root, 1, fprime=f_rootp, tol = 0.0001)
print(x_0, f_root(x_0))

# abs(f(x)) < 10**-6
x_0 = optimize.newton(f_root, 1, fprime=f_rootp, rtol = 0.000001)
print(x_0, f_root(x_0))
```

    1.4142135623746899 4.510614104447086e-12
    1.4142135623730951 4.440892098500626e-16


### Exercise 5

Find the smallest positive zero of
	$$
    f(x) = x^4 - 6.4x^3 + 6.45x^2 + 20.538x - 31.752.
    $$ 
Find brackets of the roots by visualizing the function first. Note that this function has a double root, and both, the incremental search and bisection would fail.


```python
import numpy as np
import matplotlib.pyplot as plt

def fx(x):
    return x**4-6.4*x**3+6.45*x**2+20.538*x-31.752

x = np.linspace(2, 2.5)
y = fx(x)

plt.plot(x, y)
plt.plot(x, np.zeros_like(x), "k")
# limit the x axis
plt.xlim([2.05, 2.2]) 
```


```python
# looks like 2.10 might be a very good guess, but the function is almost flat, 
# it's best to start a little further
```


```python
import scipy.optimize as optimize

def fprime(x):
    return 4 * x**3 - 3 * 6.4 * x**2 + 2 * 6.45 * x + 20.538

x_0 = optimize.newton(fx, 2.3, fprime=fprime)
print(x_0, fx(x_0)) 

x_0 = optimize.newton(fx, 4.3, fprime=fprime)
print(x_0, fx(x_0))

x_0 = optimize.newton(fx, 0, fprime=fprime)
print(x_0, fx(x_0))
```

    2.1000000197215916 0.0
    4.0 -1.7763568394002505e-14
    2.0999999787367614 3.552713678800501e-15


### Exercise 6

Determine the root of 
    $$
    f(x) = x^3 - 10x^2 + 5 = 0
    $$
with Ridder’s method, knowing that it lies in $(0.6, 0.8)$. Compare the number of iterations needed with one of the linear interpolation methods.


```python
def f(x):
    return x**3 - 10*x**2 + 5

# plot
x = np.linspace(-1, 1.5, 100)
plt.plot(x, f(x))
plt.hlines(0, xmin=-1, xmax=1.5, color="k")
```


```python
# using Ridders method
x_0 = optimize.ridder(f, 0.6, 0.8, maxiter=40, rtol=0.0001)
print(x_0, f(x_0))
```

    0.7345667902996535 0.00048000249000956785



```python
# implement own version

x_1 = 0.6
x_2 = 0.8
k = 0
max_iteration = 100
tolerance = 0.0001

while k < max_iteration:
    k += 1
    # compute midpoint
    x_3 = (x_1 + x_2) / 2

    # compute function values
    fx1 = f(x_1)
    fx2 = f(x_2)
    fx3 = f(x_3)
    # next estimate:
    x_new = x_3 + (x_3-x_1) * (np.sign(fx1 - fx2) * fx3) / (np.sqrt(fx3**2) - fx1*fx2)
    fnew = f(x_new)
    print(x_new, f(x_new))
    # check if f(x_new) is small enough
    if abs(fnew) < tolerance:
        break
    # define new bracketing values based on the middle value
    if fx2 * fnew < 0 : # we can also check value of sign functions
        x_2 = x_new
        fx2 = fnew
        x1 = x_3
        fx1 = fx3
    elif fx1 * fnew < 0:
        x_1 = xnew
        fx1 = fnew
    else:
        x_2 = xnew
        fx2 = fnew

print(k)  
    
```

    0.7235888239027736 0.14304934444555428
    0.7446281348835114 -0.13183584189723785
    0.7291735613056736 0.07075644259493874
    0.7388596633147722 -0.055782480509116716
    0.7319726991979618 0.03433896065037967
    0.7365241060276404 -0.025137006528758832
    0.7333489381508981 0.01638889580793723
    0.7354886366225527 -0.011577523255612121
    0.7340107708709983 0.007746195323213634
    0.7350149551747492 -0.0053802303343823965
    0.7343248030142457 0.003642941837775915
    0.7347954911463449 -0.00251011326329742
    0.7344727684687716 0.0017090406004101055
    0.7346932420906711 -0.0011731721491674563
    0.7345422473314126 0.0008008374812993324
    0.7346454834490738 -0.000548767548185225
    0.7345748182425869 0.0003750561360016391
    0.7346231502822851 -0.00025679213182350225
    0.7345900752315953 0.0001756043439620214
    0.7346127010666179 -0.00012018568985272537
    0.7345972193498429 8.220936136638102e-05
    21



```python
# use bisection definition from ex 1
x_0, k = bisection(0.6, 0.8, fx, tol=0.0001)
print(x_0, fx(x_0), k)
```

    0.73460693359375 -4.4786123654994014e-05 14



```python
import math as m
np.sign
```

    Help on ufunc object:
    
    sign = class ufunc(builtins.object)
     |  Functions that operate element by element on whole arrays.
     |  
     |  To see the documentation for a specific ufunc, use `info`.  For
     |  example, ``np.info(np.sin)``.  Because ufuncs are written in C
     |  (for speed) and linked into Python with NumPy's ufunc facility,
     |  Python's help() function finds this page whenever help() is called
     |  on a ufunc.
     |  
     |  A detailed explanation of ufuncs can be found in the docs for :ref:`ufuncs`.
     |  
     |  Calling ufuncs:
     |  ===============
     |  
     |  op(*x[, out], where=True, **kwargs)
     |  Apply `op` to the arguments `*x` elementwise, broadcasting the arguments.
     |  
     |  The broadcasting rules are:
     |  
     |  * Dimensions of length 1 may be prepended to either array.
     |  * Arrays may be repeated along dimensions of length 1.
     |  
     |  Parameters
     |  ----------
     |  *x : array_like
     |      Input arrays.
     |  out : ndarray, None, or tuple of ndarray and None, optional
     |      Alternate array object(s) in which to put the result; if provided, it
     |      must have a shape that the inputs broadcast to. A tuple of arrays
     |      (possible only as a keyword argument) must have length equal to the
     |      number of outputs; use None for uninitialized outputs to be
     |      allocated by the ufunc.
     |  where : array_like, optional
     |      This condition is broadcast over the input. At locations where the
     |      condition is True, the `out` array will be set to the ufunc result.
     |      Elsewhere, the `out` array will retain its original value.
     |      Note that if an uninitialized `out` array is created via the default
     |      ``out=None``, locations within it where the condition is False will
     |      remain uninitialized.
     |  **kwargs
     |      For other keyword-only arguments, see the :ref:`ufunc docs <ufuncs.kwargs>`.
     |  
     |  Returns
     |  -------
     |  r : ndarray or tuple of ndarray
     |      `r` will have the shape that the arrays in `x` broadcast to; if `out` is
     |      provided, it will be returned. If not, `r` will be allocated and
     |      may contain uninitialized values. If the function has more than one
     |      output, then the result will be a tuple of arrays.
     |  
     |  Methods defined here:
     |  
     |  __call__(self, /, *args, **kwargs)
     |      Call self as a function.
     |  
     |  __repr__(self, /)
     |      Return repr(self).
     |  
     |  __str__(self, /)
     |      Return str(self).
     |  
     |  accumulate(...)
     |      accumulate(array, axis=0, dtype=None, out=None)
     |      
     |      Accumulate the result of applying the operator to all elements.
     |      
     |      For a one-dimensional array, accumulate produces results equivalent to::
     |      
     |        r = np.empty(len(A))
     |        t = op.identity        # op = the ufunc being applied to A's  elements
     |        for i in range(len(A)):
     |            t = op(t, A[i])
     |            r[i] = t
     |        return r
     |      
     |      For example, add.accumulate() is equivalent to np.cumsum().
     |      
     |      For a multi-dimensional array, accumulate is applied along only one
     |      axis (axis zero by default; see Examples below) so repeated use is
     |      necessary if one wants to accumulate over multiple axes.
     |      
     |      Parameters
     |      ----------
     |      array : array_like
     |          The array to act on.
     |      axis : int, optional
     |          The axis along which to apply the accumulation; default is zero.
     |      dtype : data-type code, optional
     |          The data-type used to represent the intermediate results. Defaults
     |          to the data-type of the output array if such is provided, or the
     |          the data-type of the input array if no output array is provided.
     |      out : ndarray, None, or tuple of ndarray and None, optional
     |          A location into which the result is stored. If not provided or None,
     |          a freshly-allocated array is returned. For consistency with
     |          ``ufunc.__call__``, if given as a keyword, this may be wrapped in a
     |          1-element tuple.
     |      
     |          .. versionchanged:: 1.13.0
     |             Tuples are allowed for keyword argument.
     |      
     |      Returns
     |      -------
     |      r : ndarray
     |          The accumulated values. If `out` was supplied, `r` is a reference to
     |          `out`.
     |      
     |      Examples
     |      --------
     |      1-D array examples:
     |      
     |      >>> np.add.accumulate([2, 3, 5])
     |      array([ 2,  5, 10])
     |      >>> np.multiply.accumulate([2, 3, 5])
     |      array([ 2,  6, 30])
     |      
     |      2-D array examples:
     |      
     |      >>> I = np.eye(2)
     |      >>> I
     |      array([[1.,  0.],
     |             [0.,  1.]])
     |      
     |      Accumulate along axis 0 (rows), down columns:
     |      
     |      >>> np.add.accumulate(I, 0)
     |      array([[1.,  0.],
     |             [1.,  1.]])
     |      >>> np.add.accumulate(I) # no axis specified = axis zero
     |      array([[1.,  0.],
     |             [1.,  1.]])
     |      
     |      Accumulate along axis 1 (columns), through rows:
     |      
     |      >>> np.add.accumulate(I, 1)
     |      array([[1.,  1.],
     |             [0.,  1.]])
     |  
     |  at(...)
     |      at(a, indices, b=None)
     |      
     |      Performs unbuffered in place operation on operand 'a' for elements
     |      specified by 'indices'. For addition ufunc, this method is equivalent to
     |      ``a[indices] += b``, except that results are accumulated for elements that
     |      are indexed more than once. For example, ``a[[0,0]] += 1`` will only
     |      increment the first element once because of buffering, whereas
     |      ``add.at(a, [0,0], 1)`` will increment the first element twice.
     |      
     |      .. versionadded:: 1.8.0
     |      
     |      Parameters
     |      ----------
     |      a : array_like
     |          The array to perform in place operation on.
     |      indices : array_like or tuple
     |          Array like index object or slice object for indexing into first
     |          operand. If first operand has multiple dimensions, indices can be a
     |          tuple of array like index objects or slice objects.
     |      b : array_like
     |          Second operand for ufuncs requiring two operands. Operand must be
     |          broadcastable over first operand after indexing or slicing.
     |      
     |      Examples
     |      --------
     |      Set items 0 and 1 to their negative values:
     |      
     |      >>> a = np.array([1, 2, 3, 4])
     |      >>> np.negative.at(a, [0, 1])
     |      >>> a
     |      array([-1, -2,  3,  4])
     |      
     |      Increment items 0 and 1, and increment item 2 twice:
     |      
     |      >>> a = np.array([1, 2, 3, 4])
     |      >>> np.add.at(a, [0, 1, 2, 2], 1)
     |      >>> a
     |      array([2, 3, 5, 4])
     |      
     |      Add items 0 and 1 in first array to second array,
     |      and store results in first array:
     |      
     |      >>> a = np.array([1, 2, 3, 4])
     |      >>> b = np.array([1, 2])
     |      >>> np.add.at(a, [0, 1], b)
     |      >>> a
     |      array([2, 4, 3, 4])
     |  
     |  outer(...)
     |      outer(A, B, **kwargs)
     |      
     |      Apply the ufunc `op` to all pairs (a, b) with a in `A` and b in `B`.
     |      
     |      Let ``M = A.ndim``, ``N = B.ndim``. Then the result, `C`, of
     |      ``op.outer(A, B)`` is an array of dimension M + N such that:
     |      
     |      .. math:: C[i_0, ..., i_{M-1}, j_0, ..., j_{N-1}] =
     |         op(A[i_0, ..., i_{M-1}], B[j_0, ..., j_{N-1}])
     |      
     |      For `A` and `B` one-dimensional, this is equivalent to::
     |      
     |        r = empty(len(A),len(B))
     |        for i in range(len(A)):
     |            for j in range(len(B)):
     |                r[i,j] = op(A[i], B[j]) # op = ufunc in question
     |      
     |      Parameters
     |      ----------
     |      A : array_like
     |          First array
     |      B : array_like
     |          Second array
     |      kwargs : any
     |          Arguments to pass on to the ufunc. Typically `dtype` or `out`.
     |      
     |      Returns
     |      -------
     |      r : ndarray
     |          Output array
     |      
     |      See Also
     |      --------
     |      numpy.outer : A less powerful version of ``np.multiply.outer``
     |                    that `ravel`\ s all inputs to 1D. This exists
     |                    primarily for compatibility with old code.
     |      
     |      tensordot : ``np.tensordot(a, b, axes=((), ()))`` and
     |                  ``np.multiply.outer(a, b)`` behave same for all
     |                  dimensions of a and b.
     |      
     |      Examples
     |      --------
     |      >>> np.multiply.outer([1, 2, 3], [4, 5, 6])
     |      array([[ 4,  5,  6],
     |             [ 8, 10, 12],
     |             [12, 15, 18]])
     |      
     |      A multi-dimensional example:
     |      
     |      >>> A = np.array([[1, 2, 3], [4, 5, 6]])
     |      >>> A.shape
     |      (2, 3)
     |      >>> B = np.array([[1, 2, 3, 4]])
     |      >>> B.shape
     |      (1, 4)
     |      >>> C = np.multiply.outer(A, B)
     |      >>> C.shape; C
     |      (2, 3, 1, 4)
     |      array([[[[ 1,  2,  3,  4]],
     |              [[ 2,  4,  6,  8]],
     |              [[ 3,  6,  9, 12]]],
     |             [[[ 4,  8, 12, 16]],
     |              [[ 5, 10, 15, 20]],
     |              [[ 6, 12, 18, 24]]]])
     |  
     |  reduce(...)
     |      reduce(a, axis=0, dtype=None, out=None, keepdims=False, initial=<no value>, where=True)
     |      
     |      Reduces `a`'s dimension by one, by applying ufunc along one axis.
     |      
     |      Let :math:`a.shape = (N_0, ..., N_i, ..., N_{M-1})`.  Then
     |      :math:`ufunc.reduce(a, axis=i)[k_0, ..,k_{i-1}, k_{i+1}, .., k_{M-1}]` =
     |      the result of iterating `j` over :math:`range(N_i)`, cumulatively applying
     |      ufunc to each :math:`a[k_0, ..,k_{i-1}, j, k_{i+1}, .., k_{M-1}]`.
     |      For a one-dimensional array, reduce produces results equivalent to:
     |      ::
     |      
     |       r = op.identity # op = ufunc
     |       for i in range(len(A)):
     |         r = op(r, A[i])
     |       return r
     |      
     |      For example, add.reduce() is equivalent to sum().
     |      
     |      Parameters
     |      ----------
     |      a : array_like
     |          The array to act on.
     |      axis : None or int or tuple of ints, optional
     |          Axis or axes along which a reduction is performed.
     |          The default (`axis` = 0) is perform a reduction over the first
     |          dimension of the input array. `axis` may be negative, in
     |          which case it counts from the last to the first axis.
     |      
     |          .. versionadded:: 1.7.0
     |      
     |          If this is None, a reduction is performed over all the axes.
     |          If this is a tuple of ints, a reduction is performed on multiple
     |          axes, instead of a single axis or all the axes as before.
     |      
     |          For operations which are either not commutative or not associative,
     |          doing a reduction over multiple axes is not well-defined. The
     |          ufuncs do not currently raise an exception in this case, but will
     |          likely do so in the future.
     |      dtype : data-type code, optional
     |          The type used to represent the intermediate results. Defaults
     |          to the data-type of the output array if this is provided, or
     |          the data-type of the input array if no output array is provided.
     |      out : ndarray, None, or tuple of ndarray and None, optional
     |          A location into which the result is stored. If not provided or None,
     |          a freshly-allocated array is returned. For consistency with
     |          ``ufunc.__call__``, if given as a keyword, this may be wrapped in a
     |          1-element tuple.
     |      
     |          .. versionchanged:: 1.13.0
     |             Tuples are allowed for keyword argument.
     |      keepdims : bool, optional
     |          If this is set to True, the axes which are reduced are left
     |          in the result as dimensions with size one. With this option,
     |          the result will broadcast correctly against the original `arr`.
     |      
     |          .. versionadded:: 1.7.0
     |      initial : scalar, optional
     |          The value with which to start the reduction.
     |          If the ufunc has no identity or the dtype is object, this defaults
     |          to None - otherwise it defaults to ufunc.identity.
     |          If ``None`` is given, the first element of the reduction is used,
     |          and an error is thrown if the reduction is empty.
     |      
     |          .. versionadded:: 1.15.0
     |      
     |      where : array_like of bool, optional
     |          A boolean array which is broadcasted to match the dimensions
     |          of `a`, and selects elements to include in the reduction. Note
     |          that for ufuncs like ``minimum`` that do not have an identity
     |          defined, one has to pass in also ``initial``.
     |      
     |          .. versionadded:: 1.17.0
     |      
     |      Returns
     |      -------
     |      r : ndarray
     |          The reduced array. If `out` was supplied, `r` is a reference to it.
     |      
     |      Examples
     |      --------
     |      >>> np.multiply.reduce([2,3,5])
     |      30
     |      
     |      A multi-dimensional array example:
     |      
     |      >>> X = np.arange(8).reshape((2,2,2))
     |      >>> X
     |      array([[[0, 1],
     |              [2, 3]],
     |             [[4, 5],
     |              [6, 7]]])
     |      >>> np.add.reduce(X, 0)
     |      array([[ 4,  6],
     |             [ 8, 10]])
     |      >>> np.add.reduce(X) # confirm: default axis value is 0
     |      array([[ 4,  6],
     |             [ 8, 10]])
     |      >>> np.add.reduce(X, 1)
     |      array([[ 2,  4],
     |             [10, 12]])
     |      >>> np.add.reduce(X, 2)
     |      array([[ 1,  5],
     |             [ 9, 13]])
     |      
     |      You can use the ``initial`` keyword argument to initialize the reduction
     |      with a different value, and ``where`` to select specific elements to include:
     |      
     |      >>> np.add.reduce([10], initial=5)
     |      15
     |      >>> np.add.reduce(np.ones((2, 2, 2)), axis=(0, 2), initial=10)
     |      array([14., 14.])
     |      >>> a = np.array([10., np.nan, 10])
     |      >>> np.add.reduce(a, where=~np.isnan(a))
     |      20.0
     |      
     |      Allows reductions of empty arrays where they would normally fail, i.e.
     |      for ufuncs without an identity.
     |      
     |      >>> np.minimum.reduce([], initial=np.inf)
     |      inf
     |      >>> np.minimum.reduce([[1., 2.], [3., 4.]], initial=10., where=[True, False])
     |      array([ 1., 10.])
     |      >>> np.minimum.reduce([])
     |      Traceback (most recent call last):
     |          ...
     |      ValueError: zero-size array to reduction operation minimum which has no identity
     |  
     |  reduceat(...)
     |      reduceat(a, indices, axis=0, dtype=None, out=None)
     |      
     |      Performs a (local) reduce with specified slices over a single axis.
     |      
     |      For i in ``range(len(indices))``, `reduceat` computes
     |      ``ufunc.reduce(a[indices[i]:indices[i+1]])``, which becomes the i-th
     |      generalized "row" parallel to `axis` in the final result (i.e., in a
     |      2-D array, for example, if `axis = 0`, it becomes the i-th row, but if
     |      `axis = 1`, it becomes the i-th column).  There are three exceptions to this:
     |      
     |      * when ``i = len(indices) - 1`` (so for the last index),
     |        ``indices[i+1] = a.shape[axis]``.
     |      * if ``indices[i] >= indices[i + 1]``, the i-th generalized "row" is
     |        simply ``a[indices[i]]``.
     |      * if ``indices[i] >= len(a)`` or ``indices[i] < 0``, an error is raised.
     |      
     |      The shape of the output depends on the size of `indices`, and may be
     |      larger than `a` (this happens if ``len(indices) > a.shape[axis]``).
     |      
     |      Parameters
     |      ----------
     |      a : array_like
     |          The array to act on.
     |      indices : array_like
     |          Paired indices, comma separated (not colon), specifying slices to
     |          reduce.
     |      axis : int, optional
     |          The axis along which to apply the reduceat.
     |      dtype : data-type code, optional
     |          The type used to represent the intermediate results. Defaults
     |          to the data type of the output array if this is provided, or
     |          the data type of the input array if no output array is provided.
     |      out : ndarray, None, or tuple of ndarray and None, optional
     |          A location into which the result is stored. If not provided or None,
     |          a freshly-allocated array is returned. For consistency with
     |          ``ufunc.__call__``, if given as a keyword, this may be wrapped in a
     |          1-element tuple.
     |      
     |          .. versionchanged:: 1.13.0
     |             Tuples are allowed for keyword argument.
     |      
     |      Returns
     |      -------
     |      r : ndarray
     |          The reduced values. If `out` was supplied, `r` is a reference to
     |          `out`.
     |      
     |      Notes
     |      -----
     |      A descriptive example:
     |      
     |      If `a` is 1-D, the function `ufunc.accumulate(a)` is the same as
     |      ``ufunc.reduceat(a, indices)[::2]`` where `indices` is
     |      ``range(len(array) - 1)`` with a zero placed
     |      in every other element:
     |      ``indices = zeros(2 * len(a) - 1)``, ``indices[1::2] = range(1, len(a))``.
     |      
     |      Don't be fooled by this attribute's name: `reduceat(a)` is not
     |      necessarily smaller than `a`.
     |      
     |      Examples
     |      --------
     |      To take the running sum of four successive values:
     |      
     |      >>> np.add.reduceat(np.arange(8),[0,4, 1,5, 2,6, 3,7])[::2]
     |      array([ 6, 10, 14, 18])
     |      
     |      A 2-D example:
     |      
     |      >>> x = np.linspace(0, 15, 16).reshape(4,4)
     |      >>> x
     |      array([[ 0.,   1.,   2.,   3.],
     |             [ 4.,   5.,   6.,   7.],
     |             [ 8.,   9.,  10.,  11.],
     |             [12.,  13.,  14.,  15.]])
     |      
     |      ::
     |      
     |       # reduce such that the result has the following five rows:
     |       # [row1 + row2 + row3]
     |       # [row4]
     |       # [row2]
     |       # [row3]
     |       # [row1 + row2 + row3 + row4]
     |      
     |      >>> np.add.reduceat(x, [0, 3, 1, 2, 0])
     |      array([[12.,  15.,  18.,  21.],
     |             [12.,  13.,  14.,  15.],
     |             [ 4.,   5.,   6.,   7.],
     |             [ 8.,   9.,  10.,  11.],
     |             [24.,  28.,  32.,  36.]])
     |      
     |      ::
     |      
     |       # reduce such that result has the following two columns:
     |       # [col1 * col2 * col3, col4]
     |      
     |      >>> np.multiply.reduceat(x, [0, 3], 1)
     |      array([[   0.,     3.],
     |             [ 120.,     7.],
     |             [ 720.,    11.],
     |             [2184.,    15.]])
     |  
     |  ----------------------------------------------------------------------
     |  Data descriptors defined here:
     |  
     |  identity
     |      The identity value.
     |      
     |      Data attribute containing the identity element for the ufunc, if it has one.
     |      If it does not, the attribute value is None.
     |      
     |      Examples
     |      --------
     |      >>> np.add.identity
     |      0
     |      >>> np.multiply.identity
     |      1
     |      >>> np.power.identity
     |      1
     |      >>> print(np.exp.identity)
     |      None
     |  
     |  nargs
     |      The number of arguments.
     |      
     |      Data attribute containing the number of arguments the ufunc takes, including
     |      optional ones.
     |      
     |      Notes
     |      -----
     |      Typically this value will be one more than what you might expect because all
     |      ufuncs take  the optional "out" argument.
     |      
     |      Examples
     |      --------
     |      >>> np.add.nargs
     |      3
     |      >>> np.multiply.nargs
     |      3
     |      >>> np.power.nargs
     |      3
     |      >>> np.exp.nargs
     |      2
     |  
     |  nin
     |      The number of inputs.
     |      
     |      Data attribute containing the number of arguments the ufunc treats as input.
     |      
     |      Examples
     |      --------
     |      >>> np.add.nin
     |      2
     |      >>> np.multiply.nin
     |      2
     |      >>> np.power.nin
     |      2
     |      >>> np.exp.nin
     |      1
     |  
     |  nout
     |      The number of outputs.
     |      
     |      Data attribute containing the number of arguments the ufunc treats as output.
     |      
     |      Notes
     |      -----
     |      Since all ufuncs can take output arguments, this will always be (at least) 1.
     |      
     |      Examples
     |      --------
     |      >>> np.add.nout
     |      1
     |      >>> np.multiply.nout
     |      1
     |      >>> np.power.nout
     |      1
     |      >>> np.exp.nout
     |      1
     |  
     |  ntypes
     |      The number of types.
     |      
     |      The number of numerical NumPy types - of which there are 18 total - on which
     |      the ufunc can operate.
     |      
     |      See Also
     |      --------
     |      numpy.ufunc.types
     |      
     |      Examples
     |      --------
     |      >>> np.add.ntypes
     |      18
     |      >>> np.multiply.ntypes
     |      18
     |      >>> np.power.ntypes
     |      17
     |      >>> np.exp.ntypes
     |      7
     |      >>> np.remainder.ntypes
     |      14
     |  
     |  signature
     |      Definition of the core elements a generalized ufunc operates on.
     |      
     |      The signature determines how the dimensions of each input/output array
     |      are split into core and loop dimensions:
     |      
     |      1. Each dimension in the signature is matched to a dimension of the
     |         corresponding passed-in array, starting from the end of the shape tuple.
     |      2. Core dimensions assigned to the same label in the signature must have
     |         exactly matching sizes, no broadcasting is performed.
     |      3. The core dimensions are removed from all inputs and the remaining
     |         dimensions are broadcast together, defining the loop dimensions.
     |      
     |      Notes
     |      -----
     |      Generalized ufuncs are used internally in many linalg functions, and in
     |      the testing suite; the examples below are taken from these.
     |      For ufuncs that operate on scalars, the signature is None, which is
     |      equivalent to '()' for every argument.
     |      
     |      Examples
     |      --------
     |      >>> np.core.umath_tests.matrix_multiply.signature
     |      '(m,n),(n,p)->(m,p)'
     |      >>> np.linalg._umath_linalg.det.signature
     |      '(m,m)->()'
     |      >>> np.add.signature is None
     |      True  # equivalent to '(),()->()'
     |  
     |  types
     |      Returns a list with types grouped input->output.
     |      
     |      Data attribute listing the data-type "Domain-Range" groupings the ufunc can
     |      deliver. The data-types are given using the character codes.
     |      
     |      See Also
     |      --------
     |      numpy.ufunc.ntypes
     |      
     |      Examples
     |      --------
     |      >>> np.add.types
     |      ['??->?', 'bb->b', 'BB->B', 'hh->h', 'HH->H', 'ii->i', 'II->I', 'll->l',
     |      'LL->L', 'qq->q', 'QQ->Q', 'ff->f', 'dd->d', 'gg->g', 'FF->F', 'DD->D',
     |      'GG->G', 'OO->O']
     |      
     |      >>> np.multiply.types
     |      ['??->?', 'bb->b', 'BB->B', 'hh->h', 'HH->H', 'ii->i', 'II->I', 'll->l',
     |      'LL->L', 'qq->q', 'QQ->Q', 'ff->f', 'dd->d', 'gg->g', 'FF->F', 'DD->D',
     |      'GG->G', 'OO->O']
     |      
     |      >>> np.power.types
     |      ['bb->b', 'BB->B', 'hh->h', 'HH->H', 'ii->i', 'II->I', 'll->l', 'LL->L',
     |      'qq->q', 'QQ->Q', 'ff->f', 'dd->d', 'gg->g', 'FF->F', 'DD->D', 'GG->G',
     |      'OO->O']
     |      
     |      >>> np.exp.types
     |      ['f->f', 'd->d', 'g->g', 'F->F', 'D->D', 'G->G', 'O->O']
     |      
     |      >>> np.remainder.types
     |      ['bb->b', 'BB->B', 'hh->h', 'HH->H', 'ii->i', 'II->I', 'll->l', 'LL->L',
     |      'qq->q', 'QQ->Q', 'ff->f', 'dd->d', 'gg->g', 'OO->O']
    


a271f5a0e8780593bd3748d2dad1ffb61ec06912
ipynb
Jupyter Notebook
_exercise_answers/LNM_class_13_roots_solved.ipynb
Fikrahelhifzi/Python_Numerical_Method
538713303d4493f32e447137ac2a707cbc2a146f
MIT
_exercise_answers/LNM_class_13_roots_solved.ipynb
Fikrahelhifzi/Python_Numerical_Method
538713303d4493f32e447137ac2a707cbc2a146f
MIT
_exercise_answers/LNM_class_13_roots_solved.ipynb
Fikrahelhifzi/Python_Numerical_Method
538713303d4493f32e447137ac2a707cbc2a146f
MIT
Qwen/Qwen-72B
1. YES
2. YES