# Nd:YVO$_4$ Laser

Here, I discuss a (real) 100-MHz Nd:YVO4 laser mode-locked with a SESAM (SEmiconductor Saturable Absorber Mirror) regarding its stability.

```
import sympy as sym
import numpy as np
sym.init_printing(use_latex='mathjax')

wlP, wlL, wL, wA = sym.symbols('lambda_P lambda_L w_L w_A', real=True)
TR, tauP, tauL = sym.symbols('T_R tau_P tau_L', real=True)
Toc, etaP = sym.symbols('T_{oc} eta_P', real=True)
DR = sym.Symbol('\Delta R', real=True)
DRns = sym.Symbol('\Delta R_{ns}', real=True)
FsatA, FsatL = sym.symbols('F_{satA} F_{satL}', real=True)
Pstmax, gstmax = sym.symbols('P_{stmax} g_{stmax}', real=True)

# Laser parameters:
v = {wlP: 808e-9,
     Toc: 8.7e-2,
     wlL: 1064e-9,
     wL: 80e-6,
     wA: 140e-6,
     TR: 1 / 100.25e6,
     tauP: 10.6e-12,
     tauL: 100e-6,
     FsatL: 82e-3 / 1e-4,
     FsatA: 60e-6 / 1e-4,
     DR: 1.7e-2,
     DRns: 1.3e-2,
     Pstmax: 4.6}
```

# Output Power

We have seen in a previous notebook that the calculation of the steady-state output power is not a simple task (sympy was unable to find an analytical solution). We will therefore address this problem numerically.

```
import numpy as np
from scipy.optimize import brentq
sym.init_printing(use_latex='mathjax')

def qP(EP, DR, EsatA):
    """Loss at SESAM"""
    S = EP / EsatA
    if type(EP) is float:
        exp = np.exp
    else:
        exp = sym.exp
    return(DR / S * (1 - exp(-S)))

EsatA, EsatL = sym.symbols('E_{satA} E_{satL}', real=True)
Pst, gst, l, EP = sym.symbols('P_{st} g_{st} l E_P', real=True)
PP = sym.symbols('P_P')

# Derived:
v[EsatL] = np.pi * v[wL]**2 * v[FsatL]
v[EsatA] = np.pi * v[wA]**2 * v[FsatA]
v[etaP] = v[wlP] / v[wlL]
v[l] = v[Toc] + v[DRns]
v[gstmax] = v[l] + qP(v[Pstmax] * v[TR], v[DR], v[EsatA])

# Steady-state equations:
gst_ = sym.Eq(gst, l + qP(Pst * TR, DR, EsatA))
Pst_ = sym.Eq(Pst, -EsatL / tauL + etaP * PP / gst)

Pst_.subs(gst_.lhs, gst_.rhs)
```

$$P_{{st}} = - \frac{E_{{satL}}}{\tau_{L}} + \frac{P_{P} \eta_{P}}{\frac{E_{{satA}} \Delta R}{P_{{st}} T_{R}} \left(1 - e^{- \frac{P_{{st}} T_{R}}{E_{{satA}}}}\right) + l}$$

```
# The root of this function is the steady-state intracavity power:
rootfunc = Pst_.subs(gst_.lhs, gst_.rhs).rhs - Pst
rootfunc = rootfunc.subs(v)

# The pump-threshold is:
PPthreshold = (EsatL / tauL * (l + DR) / etaP).subs(v)

N = 100
PPrange = np.linspace(PPthreshold, (Pstmax * Toc / etaP).subs(v), N)
Pstrange = np.zeros(N)

for i in range(N):
    PPi = PPrange[i]
    offs = (EsatL / tauL).subs(v)
    # assume non-linear losses (qP(EP)) = 0:
    upperBound = (-offs + PPi * etaP / l).subs(v)
    # assume max. non-linear losses (qP(EP) = DR):
    lowerBound = (-offs + PPi * etaP / (l + DR)).subs(v)

    f = sym.lambdify(Pst, rootfunc.subs(PP, PPi))
    Pstrange[i] = brentq(f, lowerBound, upperBound)
```

```
%matplotlib inline
import matplotlib.pyplot as pl

# Initial slope efficiency (@ Pst = 0):
rho0 = (etaP * Toc / (l + DR)).subs(v)

fig, ax1 = pl.subplots()
pl.title('Output Power vs Pump Power')
pl.plot(PPrange, Toc.subs(v) * Pstrange, r'b-');
pl.plot([PPthreshold, PPrange[-1]], [0, rho0 * (PPrange[-1] - PPthreshold)],
        r'k--')
pl.xlabel('Pump Power (W)');
pl.ylabel('Output Power (W)', color='b');
ax2 = ax1.twinx()
ax2.plot(PPrange, Pstrange * (TR / EsatA).subs(v), r'r-')
ax2.set_ylabel('S-Parameter (1)', va='bottom', rotation=270, color='r');
```

The plot shows that the laser operates far above the lasing threshold. A comparison to a straight line (dashed) shows that the slope efficiency increases at higher power due to stronger bleaching of the absorber (i.e. lower losses).

Apparently, the saturable absorber is not saturated very strongly as $S$ remains $<1$.


```python
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
```

**Exercise 1 (20 points)**

Implement a function that returns $n$ samples from a multivariate Gaussian distribution in C++ and wrap it for use in Python using `pybind11`. Use only standard C++ and the `Eigen` library. The function signature in Python  is

```python
def mvnorm(mu, Sigma, n):
    """Returns n random samples from a multivariate Gaussian distribution.

    mu is a mean vector
    Sigma is a covariance matrix

    Returns an n by p matrix, where p is the dimension of the distribution.
    """
```

```python
%%file rng.cpp
<%
cfg['compiler_args'] = ['-std=c++11']
cfg['include_dirs'] = ['eigen']
setup_pybind11(cfg)
%>

#include <pybind11/pybind11.h>
#include <pybind11/eigen.h>

#include <Eigen/Cholesky>
#include <random>

namespace py = pybind11;

Eigen::MatrixXd mvn(Eigen::VectorXd mu, Eigen::MatrixXd sigma, int n) {
    std::default_random_engine generator;
    std::normal_distribution<double> distribution(0, 1);

    Eigen::MatrixXd A(sigma.llt().matrixL());
    int p = mu.size();

    Eigen::MatrixXd Z(n, p);
    for (int i=0; i<n; i++) {
        Eigen::VectorXd v(p);
        for (int j=0; j<p; j++) {
            v[j] = distribution(generator);
        }
        Z.row(i) = mu + A * v;
    }
    return Z;
}

PYBIND11_PLUGIN(rng) {
    pybind11::module m("rng", "auto-compiled c++ extension");
    m.def("mvn", &mvn);
    return m.ptr();
}
```

    Overwriting rng.cpp

```python
import cppimport
import numpy as np

rng = cppimport.imp("rng")

mu = 4.0*np.ones(2)
sigma = np.array([[1,0.6], [0.6, 1]])
n = 1000
x, y = rng.mvn(mu, sigma, n).T
sns.jointplot(x, y, kind='scatter')
pass
```

**Exercise 2 (20 points)**

- Consider a sequence of $n$ Bernoulli trials with success probability $p$ per trial. A string of consecutive successes is known as a success *run*. Write a function that returns the counts for runs of length $k$ for each $k$ observed in a dictionary.

For example: if the trials were [0, 1, 0, 1, 1, 0, 0, 0, 0, 1], the function should return
```
{1: 2, 2: 1})
```

- What is the probability of observing at least one run of length 5 or more when $n=100$ and $p=0.5$?. Estimate this from 100,000 simulated experiments. Is this more, less or equally likely than finding runs of length 7 or more when $p=0.7$?

```python
from collections import Counter

def count_runs(xs):
    """Count number of success runs of length k."""
    ys = []
    count = 0
    for x in xs:
        if x == 1:
            count += 1
        else:
            if count:
                ys.append(count)
                count = 0
    if count:
        ys.append(count)
    return Counter(ys)
```

```python
count_runs([0, 1, 0, 1, 1, 0, 0, 0, 0, 1])
```

    Counter({1: 2, 2: 1})

```python
def count_runs_alt(x):
    """Returns the counts for runs of length  k for each observed in x.

    This works but is slower.
    """
    return Counter(len(s) for s in ''.join(map(str, x)).split('0') if s)
```

```python
count_runs_alt([0, 1, 0, 1, 1, 0, 0, 0, 0, 1])
```

    Counter({1: 2, 2: 1})

```python
%timeit count_runs([0, 1, 0, 1, 1, 0, 0, 0, 0, 1])
```

    The slowest run took 4.84 times longer than the fastest. This could mean that an intermediate result is being cached.
    100000 loops, best of 3: 6.08 µs per loop

```python
%timeit count_runs_alt([0, 1, 0, 1, 1, 0, 0, 0, 0, 1])
```

    100000 loops, best of 3: 8.97 µs per loop

```python
def run_prob(expts, n, k, p):
    xxs = np.random.choice([0,1], (expts, n), p=(1-p, p))
    return sum(max(d.keys()) >= k for d in map(count_runs, xxs))/expts
```

```python
run_prob(expts=100000, n=100, k=5, p=0.5)
```

    0.8103

```python
run_prob(expts=100000, n=100, k=7, p=0.7)
```

    0.94929

**Exercise 3 (20 points)**.

- Consider an unbiased random walk of length $n$ as simulated with a sequence of -1 or +1 values. If we start from 0, plot the distribution of *last* return times for 100,000 simulations with $n = 100$. The last return time is the last time the cumulative sum of the random walk is zero - this may be the starting point if the walk never returns to zero in 100 steps.

- Do a maximum likeliood fit of a beta distribution to the set of last return times using the `beta.fit()` function from `scipy.stats`. Set the lower bound (loc) = 0 and the upper bound (scale) = 100 for plotting. Superimpose the fitted beta PDF on the normalized histogram of last reutrn times.

```python
n = 100
k = 100000

returns = np.zeros(k).astype('int')
for i in range(k):
    x = np.random.choice([-1,1], n)
    y = np.r_[0, np.cumsum(x)]
    returns[i] = np.nonzero(y == 0)[0][-1]
plt.hist(returns, normed=True)
pass
```

```python
from scipy.stats import beta
```

```python
a, b, loc, scale = beta.fit(returns)
x = np.linspace(0, 100, 100)
plt.plot(x, beta(a=a, b=b, loc=0, scale=100).pdf(x), linestyle='dashed', color='blue')
plt.hist(returns, histtype='step', normed=True, linewidth=1)
pass
```

**Exercise 4 (20 points)**

The Cauchy distribution is given by
$$
f(x) = \frac{1}{\pi (1 + x^2)}, \ \ -\infty \lt x \lt \infty
$$

- Integrate the tail probability $P(X > 2)$ using Monte Carlo
    1. Sampling from the Cauchy distribution directly
    2. Sampling from the uniform distribution using an appropriate change of variables
- Plot the 95% CI for the Monte Carlo estimates for n = 1 to 1000
    1. For sampling from the Cauchy distribution using mulitple Monte Carlo sequences
    2. For sampling from the uniform distribution using bootstrap samples of a single Monte Carlo sequence

```python
from scipy import stats
```

```python
# Direct
n = 1000
sum(stats.cauchy().rvs(n) > 2)/n
```

    0.156

```python
# After change of variables
x = stats.uniform().rvs(n)
np.mean(2/(np.pi*(x**2+4)))
```

    0.14766263754289377

```python
# Check (not required)
1 -  stats.cauchy.cdf(2)
```

    0.14758361765043326

#### Sampling from the Cauchy distribution using mulitple Monte Carlo sequences

```python
n=1000
reps = 1000

samples = stats.cauchy().rvs((n, reps))

# repeat multiple Monte Carlo sequences
ys = np.zeros((n, reps))
for k in range(1, n+1):
    ys[k-1] = np.sum(samples[:k, :] > 2, axis=0)/k
upper, lower = np.percentile(ys, [2.5, 97.5], axis=1)

plt.plot(np.arange(1, n+1)[:, None], ys, c='grey', alpha=0.02)
plt.plot(np.arange(1, n+1), ys[:, 0], c='red', linewidth=1) # one path
plt.plot(np.arange(1, n+1), upper, 'b', np.arange(1, n+1), lower, 'b')
pass
```

#### Sampling from the uniform distribution using bootstrap samples of a single Monte Carlo sequence

```python
n=1000
reps = 1000

samples = stats.uniform().rvs(n)
samples = 2/(np.pi*(samples**2+4))

# generate bootsrap samples
xb = np.random.choice(samples, (n, reps), replace=True)
yb = 1/np.arange(1, n+1)[:, None] * np.cumsum(xb, axis=0)
upper, lower = np.percentile(yb, [2.5, 97.5], axis=1)

plt.plot(np.arange(1, n+1)[:, None], yb, c='grey', alpha=0.02)
plt.plot(np.arange(1, n+1), yb[:, 0], c='red', linewidth=1)
plt.plot(np.arange(1, n+1), upper, 'b', np.arange(1, n+1), lower, 'b')
pass
```

**Exercise 5 (20 points)**.

Estimate the following integral using Monte Carlo integration

$$
\int_{-\infty}^{\infty} x^2 \frac{1}{2}e^{-|x|} dx
$$

Hint: See notes on importance sampling and figure.

```python
# Use importance sampling a normal distribuion

def p(x):
    """Double exponential density."""
    return 0.5*np.exp(-np.abs(x))

n = 1000000
x = stats.norm(0, 2).rvs(n)
np.mean(x**2 * p(x)/stats.norm(0, 2).pdf(x))
```

    1.9965001698406084

```python
# Check (not required)
from sympy import symbols, integrate, exp, oo

x = symbols('x')
integrate(x**2 * exp(-x), (x, 0, oo))
```

    2

```python

```


# Introduction to seismic modelling - 2D acoustic propagator

This notebook is the first in a series of tutorials highlighting various aspects of seismic inversion based on Devito operators. In this first example we aim to highlight the core ideas behind seismic modelling, where we create a numerical model that captures the processes involved in a seismic survey. This forward model will then form the basis for further tutorials on the implementation of inversion processes using Devito operators.

## Modelling workflow

The core process we are aiming to model is a seismic survey, which consists of two main components:

- **Source** - A source is positioned at a single or a few physical locations where artificial pressure is injected into the domain we want to model. In the case of land survey, it is usually dynamite blowing up at a given location, or a vibroseis (a vibrating engine generating continuous sound waves). For a marine survey, the source is an air gun sending a bubble of compressed air into the water that will expand and generate a seismic wave.
- **Receiver** - A set of microphones or hydrophones are used to measure the resulting wave and create a set of measurements called a *Shot Record*. These measurements are recorded at multiple locations, and usually at the surface of the domain or at the bottom of the ocean in some marine cases.

In order to create a numerical model of a seismic survey, we need to solve the wave equation and implement source and receiver interpolation to inject the source and record the seismic wave at sparse point locations in the grid.

## The acoustic seismic wave equation
The acoustic wave equation for the square slowness $m$, defined as $m=\frac{1}{c^2}$, where $c$ is the speed of sound in the given physical media, and a source $q$ is given by:

\begin{cases}
 &m \frac{\partial^2 u(\mathbf{x},t)}{\partial t^2} - \nabla^2 u(\mathbf{x},t) = q \ \text{in } \Omega \\
 &u(.,t=0) = 0 \\
 &\frac{\partial u(\mathbf{x},t)}{\partial t}|_{t=0} = 0
\end{cases}

with the zero initial conditions to guarantee unicity of the solution.
The boundary conditions are Dirichlet conditions:
\begin{equation}
 u(\mathbf{x},t)|_{\delta\Omega} = 0
\end{equation}

where $\delta\Omega$ is the surface of the boundary of the model $\Omega$.

# Finite domains

The last piece of the puzzle is the computational limitation. In the field, the seismic wave propagates in every direction to an "infinite" distance. However, solving the wave equation in a mathematically/discrete infinite domain is not feasible. In order to compensate, Absorbing Boundary Conditions (ABC) or Perfectly Matched Layers (PML) are required to mimic an infinite domain. These two methods allow us to approximate an unbounded media by damping and absorbing the waves at the limit of the domain to avoid reflections.

The simplest of these methods is the absorbing damping mask. The core idea is to extend the physical domain and to add a *sponge mask* in this extension that will absorb the incident waves. The acoustic wave equation with this damping mask can be rewritten as:

\begin{cases}
 &m \frac{\partial^2 u(\mathbf{x},t)}{\partial t^2} - \nabla^2 u(\mathbf{x},t) + \eta \frac{\partial u(\mathbf{x},t)}{\partial t}=q  \ \text{in } \Omega \\
 &u(.,0) = 0 \\
 &\frac{\partial u(\mathbf{x},t)}{\partial t}|_{t=0} = 0
\end{cases}

where $\eta$ is the damping mask which is equal to $0$ inside the physical domain and increasing inside the sponge layer. Multiple choice of profile can be chosen for $\eta$ from, e.g., linear to exponential.

# Seismic modelling with devito

We describe here a step by step setup of seismic modelling with Devito in a simple 2D case. We will create a physical model of our domain and define a single source along with a set of receivers. But first, we initialize some basic utilities.

```python
import numpy as np
%matplotlib inline
```

## Define the physical problem

The first step is to define the physical model:

- What are the physical dimensions of interest
- What is the velocity profile of this physical domain

We will create a simple velocity model here by hand for demonstration purposes. This model essentially consists of two layers, each with a different velocity: $1.5km/s$ in the top layer and $2.5km/s$ in the bottom layer. We will use this simple model a lot in the following tutorials, so we will rely on a utility function to create it again later.

```python
#NBVAL_IGNORE_OUTPUT
from examples.seismic import Model, plot_velocity

# Define a physical size
shape = (101, 101)  # Number of grid point (nx, nz)
spacing = (10., 10.)  # Grid spacing in m. The domain size is now 1km by 1km
origin = (0., 0.)  # What is the location of the top left corner. This is necessary to define
# the absolute location of the source and receivers

# Define a velocity profile. The velocity is in km/s
v = np.empty(shape, dtype=np.float32)
v[:, :51] = 1.5
v[:, 51:] = 2.5

# With the velocity and model size defined, we can create the seismic model that
# encapsulates this properties. We also define the size of the absorbing layer as 10 grid points
model = Model(vp=v, origin=origin, shape=shape, spacing=spacing,
              space_order=2, nbl=10)

plot_velocity(model)
```

# Acquisition geometry

To fully define our problem setup we also need to define the source that injects the wave to the model and the set of receiver locations at which to the wavefield is sampled. The source time signature will be modelled using a 'Ricker' wavelet whose form is defined as

\begin{equation}
  q(t) = (1-2\pi^2 f_0^2 (t - \frac{1}{f_0})^2 )e^{- \pi^2 f_0^2 (t - \frac{1}{f_0})}.
\end{equation}

To include such a source signature in our model we first need to define the time duration of our model and the timestep size, which is dictated by the CFL condition and our grid spacing. Luckily, our `Model` utility provides us with the critical timestep size, so we can fully discretize our model time axis as an array:

```python
from examples.seismic import TimeAxis

t0 = 0.  # Simulation starts a t=0
tn = 1000.  # Simulation last 1 second (1000 ms)
dt = model.critical_dt  # Time step from model grid spacing

time_range = TimeAxis(start=t0, stop=tn, step=dt)
```

Recall that we can view the documentation for the `TimeAxis` object (or any other object via):

```python
print(time_range.__doc__)
```

        Data object to store the TimeAxis. Exactly three of the four key arguments
        must be prescribed. Because of remainder values it is not possible to create
        a TimeAxis that exactly adhears to the inputs therefore start, stop, step
        and num values should be taken from the TimeAxis object rather than relying
        upon the input values.

        The four possible cases are:
        start is None: start = step*(1 - num) + stop
        step is None: step = (stop - start)/(num - 1)
        num is None: num = ceil((stop - start + step)/step);
                     because of remainder stop = step*(num - 1) + start
        stop is None: stop = step*(num - 1) + start

        Parameters
        ----------
        start : float, optional
            Start of time axis.
        step : float, optional
            Time interval.
        num : int, optional
            Number of values (Note: this is the number of intervals + 1).
            Stop value is reset to correct for remainder.
        stop : float, optional
            End time.

(Note: Values of the time steps are stored in `time_range.time_values`.)

We position the source at a depth of $20m$ and in the center of the $x$ axis ($\mathbf{x}_{src}=(500m, 20m)$), with a peak wavelet frequency of $10Hz$.

```python
#NBVAL_IGNORE_OUTPUT
from examples.seismic import RickerSource

f0 = 0.010  # Source peak frequency is 10Hz (0.010 kHz)
src = RickerSource(name='src', grid=model.grid, f0=f0,
                   npoint=1, time_range=time_range)

# Set source coordinates
src.coordinates.data[0, 0] = np.array(model.domain_size[0]) * .5
src.coordinates.data[0, 1] = 20.  # Depth is 20m

# We can plot the time signature to see the wavelet via:
src.show()
```

Similarly to our source object, we can now define our receiver geometry as a symbol of type `Receiver`. It is worth noting here that both utility classes, `RickerSource` and `Receiver` are thin wrappers around Devito's `SparseTimeFunction` type, which encapsulates sparse point data and allows us to inject and interpolate values into and out of the computational grid. As we have already seen, both types provide a `.coordinates` property to define the position within the domain of all points encapsulated by that symbol.

In this example we will position receivers at the same depth as the source, every $10m$ along the x axis. The `rec.data` property will be initialized, but left empty, as we will compute the receiver readings during the simulation.

```python
#NBVAL_IGNORE_OUTPUT
from examples.seismic import Receiver

# Create symbol for 101 receivers
rec = Receiver(name='rec', grid=model.grid, npoint=101, time_range=time_range)

# Prescribe even spacing for receivers along the x-axis
rec.coordinates.data[:, 0] = np.linspace(0, model.domain_size[0], num=101)
rec.coordinates.data[:, 1] = 20.  # Depth is 20m

# We can now show the source and receivers within our domain:
# Red dot: Source location
# Green dots: Receiver locations (every 4th point)
plot_velocity(model, source=src.coordinates.data,
              receiver=rec.coordinates.data[::4, :])
```

# Finite-difference discretization

The finite-difference approximation is derived from Taylor expansions of the continuous field after removing the error term.

## Time discretization

We only consider the second order time discretization for now. From the Taylor expansion, the second order discrete approximation of the second order time derivative is:
\begin{equation}
\begin{aligned}
 \frac{\partial^2 u(\mathbf{x},t)}{\partial t^2} = \frac{\mathbf{u}(\mathbf{x},\mathbf{t+\Delta t}) - 2 \mathbf{u}(\mathbf{x},\mathbf{t}) + \mathbf{u}(\mathbf{x},\mathbf{t-\Delta t})}{\mathbf{\Delta t}^2} + O(\mathbf{\Delta t}^2).
\end{aligned}
\end{equation}

where $\mathbf{u}$ is the discrete wavefield, $\mathbf{\Delta t}$ is the discrete
time-step (distance between two consecutive discrete time points) and $O(\mathbf{\Delta
  t}^2)$ is the temporal discretization error term. The discretized approximation of the
second order time derivative is then given by dropping the error term. This derivative is represented in Devito by `u.dt2` where u is a `TimeFunction` object.

## Spatial discretization

We define the discrete Laplacian as the sum of the second order spatial
derivatives in the three dimensions:
\begin{equation}
\begin{aligned}
\Delta \mathbf{u}(\mathbf{x},\mathbf{y},\mathbf{z},\mathbf{t})= \sum_{j=1}^{j=\frac{k}{2}} \Bigg[\alpha_j \Bigg(&
\mathbf{u}(\mathbf{x+jdx},\mathbf{y},\mathbf{z},\mathbf{t})+\mathbf{u}(\mathbf{x-jdx},\mathbf{y},\mathbf{z},\mathbf{t}) + \\
&\mathbf{u}(\mathbf{x},\mathbf{y+jdy},\mathbf{z},\mathbf{t})+\mathbf{u}(\mathbf{x},\mathbf{y-jdy},\mathbf{z}\mathbf{t}) + \\
&\mathbf{u}(\mathbf{x},\mathbf{y},\mathbf{z+jdz},\mathbf{t})+\mathbf{u}(\mathbf{x},\mathbf{y},\mathbf{z-jdz},\mathbf{t})\Bigg) \Bigg] + \\
&3\alpha_0 \mathbf{u}(\mathbf{x},\mathbf{y},\mathbf{z},\mathbf{t}).
\end{aligned}
\end{equation}

where $\alpha_j$ are the appropriate finite difference weights for the approximation. This derivative is represented in Devito by `u.laplace`.

## Wave equation

With the space and time discretization defined, we can fully discretize the wave-equation with the combination of time and space discretizations and obtain the following second order in time and $k^{th}$ order in space discrete stencil to update one grid point at position $\mathbf{x}, \mathbf{y},\mathbf{z}$ at time $\mathbf{t}$, i.e.
\begin{equation}
\begin{aligned}
\mathbf{u}(\mathbf{x},\mathbf{y},\mathbf{z},\mathbf{t+\Delta t}) = &2\mathbf{u}(\mathbf{x},\mathbf{y},\mathbf{z},\mathbf{t}) - \mathbf{u}(\mathbf{x},\mathbf{y}, \mathbf{z},\mathbf{t-\Delta t}) +\\
& \frac{\mathbf{\Delta t}^2}{\mathbf{m(\mathbf{x},\mathbf{y},\mathbf{z})}} \Big(\Delta \mathbf{u}(\mathbf{x},\mathbf{y},\mathbf{z},\mathbf{t}) + \mathbf{q}(\mathbf{x},\mathbf{y},\mathbf{z},\mathbf{t}) \Big).
\end{aligned}
\end{equation}

```python
# In order to represent the wavefield u and the square slowness we need symbolic objects
# corresponding to time-space-varying field (u, TimeFunction) and
# space-varying field (m, Function)
from devito import TimeFunction

# Define the wavefield with the size of the model and the time dimension
u = TimeFunction(name="u", grid=model.grid, time_order=2, space_order=2)

# We can now write the PDE
pde = model.m * u.dt2 - u.laplace + model.damp * u.dt

# The PDE representation is as on paper
pde
```

$\displaystyle \operatorname{damp}{\left(x,y \right)} \frac{\partial}{\partial t} u{\left(t,x,y \right)} - \frac{\partial^{2}}{\partial x^{2}} u{\left(t,x,y \right)} - \frac{\partial^{2}}{\partial y^{2}} u{\left(t,x,y \right)} + \frac{\frac{\partial^{2}}{\partial t^{2}} u{\left(t,x,y \right)}}{\operatorname{vp}^{2}{\left(x,y \right)}}$

```python
# This discrete PDE can be solved in a time-marching way updating u(t+dt) from the previous time step
# Devito as a shortcut for u(t+dt) which is u.forward. We can then rewrite the PDE as
# a time marching updating equation known as a stencil using customized SymPy functions
from devito import Eq, solve

stencil = Eq(u.forward, solve(pde, u.forward))
```

# Source injection and receiver interpolation

With a numerical scheme to solve the homogenous wave equation, we need to add the source to introduce seismic waves and to implement the measurement operator, and interpolation operator. This operation is linked to the discrete scheme and needs to be done at the proper time step. The semi-discretized in time wave equation with a source reads:

\begin{equation}
\begin{aligned}
\mathbf{u}(\mathbf{x},\mathbf{y},\mathbf{z},\mathbf{t+\Delta t}) = &2\mathbf{u}(\mathbf{x},\mathbf{y},\mathbf{z},\mathbf{t}) - \mathbf{u}(\mathbf{x},\mathbf{y}, \mathbf{z},\mathbf{t-\Delta t}) +\\
& \frac{\mathbf{\Delta t}^2}{\mathbf{m(\mathbf{x},\mathbf{y},\mathbf{z})}} \Big(\Delta \mathbf{u}(\mathbf{x},\mathbf{y},\mathbf{z},\mathbf{t}) + \mathbf{q}(\mathbf{x},\mathbf{y},\mathbf{z},\mathbf{t}) \Big).
\end{aligned}
\end{equation}

It shows that in order to update $\mathbf{u}$ to the time $\mathbf{t+\Delta t}$ we must inject the value of the source term $\mathbf{q}$ at time $\mathbf{t}$. In Devito, this corresponds to the update of $u$ at index $t+1$ (t = time implicitly) with the source of time $t$.

```python
# Finally we define the source injection and receiver read function to generate the corresponding code
src_term = src.inject(field=u.forward, expr=src * dt**2 / model.m)

# Create interpolation expression for receivers
rec_term = rec.interpolate(expr=u.forward)
```

# Devito operator and solve
After constructing all the necessary expressions for updating the wavefield, injecting the source term and interpolating onto the receiver points, we can now create the Devito operator that will generate the C code at runtime. When creating the operator, Devito's two optimization engines will log which performance optimizations have been performed:
* **DSE:** The Devito Symbolics Engine will attempt to reduce the number of operations required by the kernel.
* **DLE:** The Devito Loop Engine will perform various loop-level optimizations to improve runtime performance.

**Note**: The argument `subs=model.spacing_map` causes the operator to substitute values for our current grid spacing into the expressions before code generation. This reduces the number of floating point operations executed by the kernel by pre-evaluating certain coefficients.

```python
#NBVAL_IGNORE_OUTPUT
from devito import Operator

op = Operator([stencil] + src_term + rec_term, subs=model.spacing_map)
```

Now we can execute the create operator for a number of timesteps. We specify the number of timesteps to compute with the keyword `time` and the timestep size with `dt`.

```python
#NBVAL_IGNORE_OUTPUT
op(time=time_range.num-1, dt=model.critical_dt)
```

    Operator `Kernel` run in 0.02 s

After running our operator kernel, the data associated with the receiver symbol `rec.data` has now been populated due to the interpolation expression we inserted into the operator. This allows us the visualize the shot record:

```python
#NBVAL_IGNORE_OUTPUT
from examples.seismic import plot_shotrecord

plot_shotrecord(rec.data, model, t0, tn)
```

# Running Devito programs with MPI

MPI is immediatley supported for any Devito program. Simply type:

```
mpirun <mpi args> python my_program.py
```

## Exercise: Run script with MPI

In the folder `~/OGHPC2020/morning_session_notebooks` there is a python script named `acoustic_script.py` which essentially the python code introduced above but with the plotting instructions removed.

As a quick exercise, let us run this script with `mpirun` and satisfy ourselves that it returns the same results that were produced above:
- In a new cell below, import the `norm` builtin (`from devito import norm`) and print the result from taking the norm of the receiver data (`norm(rec)`).
- Then run the script `acoustic_script.py` over two mpi processes (`mpirun -np 2 python acoustic_script.py`) and check the result is the same. (Note: If executing via the jupyterhub terminal please do not run over 'too' many mpi processes as this may cause issues for other users.)


(sec:fourierove_vrste)=
# Fourierove vrste

Cilj Fourierovih vrst je, da bi poljubne (periodične) podatke opisali z vsoto harmonskih funkcij različnih osnovnih period. Ta cilj je v tem trenutku definiran relativno površno; vse detajle bomo pogledali v tem poglavju. Pot nas bo vodila preko sledečih korakov:

1. zapis periodične funkcije, kot vsote harmonskih funkcij
1. identifikacija harmonskih funkcij iz točke 1.
1. posplošitev

To poglavje je napisano na podlagi večjega števila virov in lastnega raziskovanja. Med knjižnjimi viri bralcu priporočam {cite:t}`shin2008`, med video vsebinami pa [Grant Sanderson: But what is a Fourier series? From heat flow to drawing with circles]( https://www.youtube.com/watch?v=r6sGWTCMz2k).

## Zapis periodične funkcije, kot vsote harmonskih funkcij

Preden gremo v podrobnosti Fourierovih vrst, si pogljemo definicijo splošne harmonske funkcije:

$$
x(t) = A\,\sin(\omega\,t+\varphi),
$$

kjer je $A$ amplituda, $\omega$ krožna frekvenca (enota: rad/s) in $\varphi$ fazni kot (enota rad) glede na izhodišče časa. Pri tem je $\omega=2\,\pi\,f$ in je $f$ (sekundna) frekvenca (enota: 1/s=Hz). Pozitivna faza pomakne harmonsko funkcijo proti levi, negativna faza pa proti desni. Perioda $T$, ko se harmonska funkcija ponovi je definirana z krožno frekvenco: $T=2\,\pi/\omega$.

Primer harmonske funkcije z amplitudo $A=1$, krožno frekvenco $\omega=1\,$rad/s in pozitivno fazo $\varphi=1\,$rad je prikazan spodaj.

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

T = 6
A = 1
t, dt = np.linspace(-0.5*T,1.5*T,201, retstep=True)
ω = 1
ϕ = 1

fig, ax1 = plt.subplots()
plt.title(f'Harmonska funkcija: $x(t) = A\,\sin(\\omega\,t+\\varphi); \\omega={ω}, \\varphi={ϕ}$ ')

ax1.set_xlabel('$t\\quad $[s]')
ax1.set_ylabel('$x(t)\quad$[]', color='C0')
ax1.tick_params(axis='y', labelcolor='C0')
ax1.vlines(0, -1.3, 1.3, 'k', lw=0.5)
ax1.hlines(0, np.min(t), np.max(t), 'k', lw=0.5)
ax1.plot(t, A*np.sin(ω*t+ϕ), label='Harmonski podatki', c='C1', lw=2, alpha=0.8)
ax1.vlines(-ϕ+2*np.pi*np.arange(T/(ω*2*np.pi)+1), -1.3, 1.3, ls='-.', lw=1)
ax1.annotate('$\\varphi$',
            xy=(-ϕ, 1.1), xycoords='data',
            va='center', size='large',
            xytext=(0.1, 1.1), textcoords='data',
            arrowprops=dict(arrowstyle='->',
                            connectionstyle='arc3,rad=0',
                            color="k", shrinkA=0, shrinkB=0),
            )
ax1.hlines(A, 1., 3, ls='-.', lw=1)
ax1.annotate('$A$',
            xy=(2.5, 0), xycoords='data',
            ha='center', size='large',
            xytext=(2.5, A+.05), textcoords='data',
            arrowprops=dict(arrowstyle='<-',
                            connectionstyle='arc3,rad=0',
                            color="k", shrinkA=0, shrinkB=0),
            )
ax1.annotate('$T=\\frac{2\,\pi}{\\omega}$',
            xy=(-ϕ, -1.1), xycoords='data',
            va='center', size='large',
            xytext=(2*np.pi-ϕ+0.1, -1.1), textcoords='data',
            arrowprops=dict(arrowstyle='<->',
                            connectionstyle='arc3,rad=0',
                            color="k", shrinkA=0, shrinkB=0),
            )
plt.show()
```

Potem, ko smo podrobneje spoznali eno harmonsko funkcijo, nadaljujemo s vsoto harmonskih funkcij, pri tem se bomo omejili na kompleksno periodične podatke (glejte {ref}`sec:klasifikacija_pod`), kjer je razmerje med frekvencami posameznih harmonikov racionalno število. Frekvence harmonskih funkcij bomo dodatno omejili: če je osnovna krožna frekvenca $\omega_p=2\,\pi/T_p$ definirana z osnovno periodo $T_p$, potem bomo za vse ostale harmonike zahtevali, da imajo krožno frekvenco, ki je večkratnik osnovne krožne frekvence.

Opisu ustreza vsota harmonskih funkcij:

$$
x(t)=\frac{a_0}{2}+\sum_{n=1}^{\infty}
\left[
a_n\cos\left(\frac{2\,\pi\,n\,t}{T_p} \right)
+
b_n\sin\left(\frac{2\,\pi\,n\,t}{T_p} \right)
\right],
$$

kjer je $a_0$ konstanta, $a_n$ amplituda $n$-te cosinusne komponente in $b_n$ amplituda $n$-te sinusne komponente.

Spodaj si poglejmo primer harmonske funkcije, ki ustreza zgornjim omejitvam.

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

Tp = 2
t, dt = np.linspace(-0.5*Tp,2.5*Tp,301, retstep=True)
a0 = 1.
N = 10
seed = 0
rg = np.random.default_rng(seed)
a = rg.normal(size=N)*1/np.arange(1,N+1)**2 #skaliranje na koncu, da imajo višje komponente imajo manjšo amplitudo
b = rg.normal(size=N)*1/np.arange(1,N+1)**2
x = a0/2
for n in range(N):
    x += a[n]*np.cos(2*np.pi*(n+1)*t/Tp) + b[n]*np.sin(2*np.pi*(n+1)*t/Tp)

fig, ax1 = plt.subplots()
plt.title(f'Kompleksno periodična funkcija')

ax1.set_xlabel('$t\\quad $[s]')
ax1.set_ylabel('$x(t)\quad$[-]', color='C1')
ax1.tick_params(axis='y', labelcolor='C1')
ax1.vlines(0, -2, 2, 'k', lw=0.5)
ax1.hlines(0, np.min(t), np.max(t), 'k', lw=0.5)
ax1.plot(t, x, label='Periodični podatki', c='C1', lw=2, alpha=0.8)
ax1.vlines([Tp], -2, 2, ls='-.', lw=1)
ax1.hlines(a0/2, np.min(t), np.max(t), ls='-.', lw=1)
ax1.annotate('$\\frac{a_0}{2}$',
            xy=(2.5, 0), xycoords='data',
            ha='center', size='large',
            xytext=(2.5, a0/2+.15), textcoords='data',
            arrowprops=dict(arrowstyle='<-',
                            connectionstyle='arc3,rad=0',
                            color="k", shrinkA=0, shrinkB=0),
            )
ax1.annotate('$T_{p}$',
            xy=(0, -0.5), xycoords='data',
            va='center', size='large',
            xytext=(Tp+0.05, -0.5), textcoords='data',
            arrowprops=dict(arrowstyle='<->',
                            connectionstyle='arc3,rad=0',
                            color="k", shrinkA=0, shrinkB=0),
            )
plt.ylim(-1,2)
plt.show()
```

Periodična funkcija kakor smo si jo pogledali zgoraj je ključnega pomena za razumevanje Fourierovih vrst in pozneje Fourierove transformacije (glejte {ref}`sec:fourierova_int_transf`). V nadaljevanju bomo periodično funkcijo najprej preoblikovali v kompleksno obliko, saj nam bo to zelo poenostavilo matematični zapis in fizikalno jasnost.

Zapis v kompleksno obliko začnemo s pomočjo Eulerjeve formule:

$$
e^{\mathrm{i}\,\alpha} = \cos\alpha + \mathrm{i}\,\sin\alpha,
$$

kjer je $\mathrm{i}=\sqrt{-1}$.

Če uporabimo $\alpha=2\,\pi\,n\,t/T_p$, lahko zapišemo:

$$
\cos\alpha=\frac{1}{2}e^{\mathrm{i}\,\alpha}+\frac{1}{2}e^{-\mathrm{i}\,\alpha}
\qquad
\sin\alpha=-\frac{1}{2}\,\mathrm{i}\,e^{\mathrm{i}\,\alpha}+\frac{1}{2}\,\mathrm{i}\,e^{-\mathrm{i}\,\alpha}
$$

ter funkcijo:

$$
x(t)=\frac{a_0}{2}+\sum_{n=1}^{\infty}
\left[
a_n\cos\big(\frac{2\,\pi\,n\,t}{T_p} \big)
+
b_n\sin\big(\frac{2\,\pi\,n\,t}{T_p} \big)
\right],
$$

preoblikujemo v:

$$
x(t)=\frac{a_0}{2}+\sum_{n=1}^{\infty}
\left[
a_n\big(
\frac{1}{2}e^{\mathrm{i}\,\alpha}+\frac{1}{2}e^{-\mathrm{i}\,\alpha}
\big)+
b_n\big(
-\frac{1}{2}\,\mathrm{i}\,e^{\mathrm{i}\,\alpha}+\frac{1}{2}\,\mathrm{i}\,e^{-\mathrm{i}\,\alpha}
\big)
\right].
$$

Urejamo naprej:

$$
x(t)=\frac{a_0}{2}+\sum_{n=1}^{\infty}
\left[
\big(
\frac{1}{2}a_n-\frac{1}{2}\mathrm{i}b_n
\big)\,e^{\mathrm{i}\,2\,\pi\,n\,t/T_p}
\right]
+\sum_{n=1}^{\infty}
\left[
\big(
\frac{1}{2}a_n+\frac{1}{2}\mathrm{i}b_n
\big)\,e^{-\mathrm{i}\,2\,\pi\,n\,t/T_p}
\right].
$$

Zgornji izraz poenostavimo v:

$$
x(t)=\sum_{n=-\infty}^{\infty}
c_n\,e^{\mathrm{i}\,2\pi\,n\,t/T_p},
$$

kjer velja:

$$
c_n =
\begin{cases}
    \frac{1}{2}a_n-\frac{1}{2}\textrm{i}b_n,& \text{če } n\geq +1\\
    \frac{1}{2}a_n,              & \text{če } n = 0\\
    \frac{1}{2}a_{|n|}+\frac{1}{2}\textrm{i}b_{|n|},& \text{če } n\leq -1\\
\end{cases}
$$

Ugotovimo, da velja $c_n=c_{-n}^{*}$, kjer smo z ${}^*$ označili kompleksno konjugirano vrednost.

:::{note}
Zapis kompleksno periodične funkcije z realnimi koeficienti:

$$
x(t)=\frac{a_0}{2}+\sum_{n=1}^{\infty}
\left[
a_n\cos\left(\frac{2\,\pi\,n\,t}{T_p} \right)
+
b_n\sin\left(\frac{2\,\pi\,n\,t}{T_p} \right)
\right]
$$

:::

:::{note}
Zapis kompleksno periodične funkcije s kompleksnimi koeficienti:

$$
x(t)=\sum_{n=-\infty}^{\infty}
c_n\,e^{\mathrm{i}\,2\pi\,n\,t/T_p},
$$

:::

Oba zapisa zgoraj seveda vodita v isti rezultat (glejte primer spodaj).

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

Tp = 2
t, dt = np.linspace(-0.5*Tp,2.5*Tp,301, retstep=True)
a0 = 1.
N = 10
seed = 0
rg = np.random.default_rng(seed)
a = rg.normal(size=N)*1/np.arange(1,N+1)**2 #skaliranje na koncu, da imajo višje komponente imajo manjšo amplitudo
b = rg.normal(size=N)*1/np.arange(1,N+1)**2

c = np.zeros(2*N+1, dtype='complex')
c[N+1:] = 0.5*a-0.5j*b
c[N] = a0/2
c[:N] = np.conj(c[N+1:])[::-1]

x1 = a0/2
for n in range(N):
    x1 += a[n]*np.cos(2*np.pi*(n+1)*t/Tp) + b[n]*np.sin(2*np.pi*(n+1)*t/Tp)

x = np.zeros(len(t), 'complex')
for n in range(-N,N+1):
    x += c[N+n]*np.exp(2j*np.pi*n*t/Tp)
x = np.real(x) # teoretično pričakujemo samo realno rezultat (imaginarni del mora biti na nivoju numerične napake)

fig, ax1 = plt.subplots()
plt.title(f'Kompleksno periodična funkcija')

ax1.set_xlabel('$t\\quad $[s]')
ax1.set_ylabel('$x(t)\quad$[-]', color='C1')
ax1.tick_params(axis='y', labelcolor='C1')
ax1.vlines(0, -2, 2, 'k', lw=0.5)
ax1.hlines(0, np.min(t), np.max(t), 'k', lw=0.5)
ax1.plot(t, x1, label='Klasična definicija', c='C1', lw=2, alpha=0.8)
ax1.plot(t, x, label='Definicija v kompleksni obliki', c='C0', ls=':', lw=2, alpha=1)
ax1.vlines([Tp], -2, 2, ls='-.', lw=1)
ax1.hlines(a0/2, np.min(t), np.max(t), ls='-.', lw=1)
ax1.annotate('$\\frac{a_0}{2}$',
            xy=(2.5, 0), xycoords='data',
            ha='center', size='large',
            xytext=(2.5, a0/2+.13), textcoords='data',
            arrowprops=dict(arrowstyle='<-',
                            connectionstyle='arc3,rad=0',
                            color="k", shrinkA=0, shrinkB=0),
            )
ax1.annotate('$T_{p}$',
            xy=(0, -0.5), xycoords='data',
            va='center', size='large',
            xytext=(Tp+0.05, -0.5), textcoords='data',
            arrowprops=dict(arrowstyle='<->',
                            connectionstyle='arc3,rad=0',
                            color="k", shrinkA=0, shrinkB=0),
            )
ax1.legend(loc=1)
plt.ylim(-1,2)
plt.show()
```

## Identifikacija harmonskih funkcij, ki sestavljajo periodično funkcijo

Sedaj se lahko vrnemo na izvorno nalogo Fourierovih vrst: *popis poljubne periodične funkcije s superpozicijo različnih harmonskih funkcij*. Vprašanje torej je: ali v periodični funkciji:

$$
x(t)=\sum_{n=-\infty}^{\infty}
c_n\,e^{\mathrm{i}\,2\pi\,n\,t/T_p},\qquad c_n=c_{-n}^*
$$

lahko identificiramo harmonsko funkcijo s frekvenco $m$ kratnika osnovne frekvence, torej  $c_m\,e^{\mathrm{i}\,2\pi\,m\,t/T_p}$? Izkaže se, da je to relativno enostavno, če poskušamo izračunati integral periodične funkcije $x(t)$ pomnožen z enotsko harmonsko funkcijo  $e^{-\mathrm{i}\,2\pi\,m\,t/T_p}$, ki ima negativno frekvenco $-m/T_p$ (namesto $\textrm{i}$, imamo v eksponentu $\textrm{-i}$):

$$
\underbrace{\int_0^{T_p} x(t)\,e^{-\mathrm{i}\,2\pi\,m\,t/T_p}\,\mathrm{d}t}_{A_m}=
\underbrace{\int_0^{T_p} \sum_{n=-\infty}^{\infty}
c_n\,e^{\mathrm{i}\,2\pi\,n\,t/T_p} \,\,e^{-\mathrm{i}\,2\pi\,m\,t/T_p}\,\mathrm{d}t}_{B_m}
$$

Ker $x(t)$ predstavlja znano periodično funkcijo (oz. podatke), lahko integral $A_m$ izračunamo. $B_m$ pa lahko poenostavimo v:

$$
B_m=\int_0^{T_p} \sum_{n=-\infty}^{\infty}
c_n\,e^{\mathrm{i}\,2\pi\,(n-m)\,t/T_p} \,\mathrm{d}t
$$

Zamenjajmo sedaj vrstni red seštevanja in integriranja, ter izpostavimo konstanto $c_n$:

$$
B_m= \sum_{n=-\infty}^{\infty}
c_n\,\int_0^{T_p}e^{\mathrm{i}\,2\pi\,(n-m)\,t/T_p} \,\mathrm{d}t
$$

Ker integriramo harmonsko funkcijo s frekvenco, ki je celi večkratnik osnovne frekvence $1/T_p$, je rezultat vedno 0, razen takrat, ko je $n=m$:

$$
\int_0^{T_p}e^{\mathrm{i}\,2\pi\,(n-m)\,t/T_p} \,\mathrm{d}t=
\begin{cases}
    0,& \text{če } n\ne m\\
    T_p,& \text{če } n= m\\
\end{cases}
$$

Sledi torej:

$$
B_m = c_m\,T_p
$$

Ker velja $A_m=B_m$ sledi, da je neznan Fourierov koeficient $c_m$ definirana kot (gre za kompleksno število z amplitudo in fazo):

$$
c_m = \frac{A_m}{T_p} = \frac{1}{T_p}\,\int_0^{T_p} x(t)\,e^{-\mathrm{i}\,2\pi\,m\,t/T_p}\,\mathrm{d}t
$$

Izpeljali smo torej, da je posamezno harmonsko komponento relativno enostavno določiti; težava v praksi je, da dani podatki $x(t)$ nujno ne izpolnjujejo vseh predpostavk, ki smo jih zgoraj naredili (npr. da je harmonik celi večkratnik osnovne frekvence). Kljub temu, pa Fourierove vrste uporabljamo in nam dajejo odlične rezultate.

## Posplošitev

Fourierove vrste najpogosteje najdemo definirane v treh oblikah ($T_p$ predstavlja osnovno period in $N$ število harmonskih komponent, ki jih upoštevamo/identificiramo):

(sec:FV_exp)=

### Fourierove vrste v eksponentni (kompleksni) obliki

:::{note}
Fourierovi koeficienti:

$$
c_n = \frac{1}{T_p}\,\int_0^{T_p} x(t)\,e^{-\mathrm{i}\,2\pi\,n\,t/T_p}\,\mathrm{d}t
$$

$$
x(t)=\sum_{n=-N}^{N}
c_n\,e^{\mathrm{i}\,2\pi\,n\,t/T_p},\qquad c_n=c_{-n}^*
$$

:::

(sec:FV_sin_cos)=

### Fourierove vrste v sinus-cosinusni obliki

:::{note}

Fourierovi koeficienti:

$$
a_n = \frac{1}{T_p}\,\int_0^{T_p} x(t)\,\cos(2\pi\,n\,t/T_p)\,\mathrm{d}t
$$

$$
b_n = \frac{1}{T_p}\,\int_0^{T_p} x(t)\,\sin(2\pi\,n\,t/T_p)\,\mathrm{d}t
$$

Rekonstruirana zvezna funkcija:

$$
x(t)=\frac{a_0}{2}+\sum_{n=1}^{N}
a_n\,\cos(2\pi\,n\,t/T_p)+
b_n\,\sin(2\pi\,n\,t/T_p)
$$

:::

### Fourierove vrste v amplitudno-fazni obliki

:::{note}

Rekonstruirana zvezna funkcija:

$$
x(t)=\frac{A_0}{2}+\sum_{n=1}^{N}
A_n\,\cos(2\pi\,n\,t/T_p-\varphi_n)
$$

:::

Kjer moramo amplitudo $A_n$ in fazo $\varphi_n$ določit s pomočjo eksponentne ali sinusno-cosinusne oblike in ob pomoči prehodov med posameznimi oblikami zapisov:

$$
\begin{split}
c_0&=A_0&=a_0\\
c_n&=\frac{A_n}{2}\,e^{-\mathrm{i}\varphi_n}&=\frac{1}{2}(a_n-\mathrm{i}b_n)\\
c_n&=c_{-n}^*&\\
\end{split}
$$

V tej knjigi bomo skoraj izključno uporabljali eksponentno obliko, saj je najbolj kompaktna, enostavna za uporabo in matematične izpeljave.

## Nekatere lastnosti Fourierovih vrst

#### Odvodi in integrali Fourierovih vrst

:::{note}
Za periodično funkcijo, ki jo na področju zveznosti lahko popišemo s Fourierovimi vrstami govorimo, da izpolnjujejo **Dirichletovim pogoj**.
:::

Če Dirichletov pogoj izpolnjuje tudi odvod $\dot x(t)$, potem je mogoče odvod izračunati neposredno iz Fourierovih vrst za $x(t)$ pri čemer je treba odvajati posamezne člene.

$$
\dot{x}(t)=\sum_{n=-N}^{N}
\frac{\mathrm{i}\,2\pi\,n}{T_p}\,c_n\,e^{\mathrm{i}\,2\pi\,n\,t/T_p},\qquad c_n=c_{-n}^*
$$

Analogno velja za integracijo:

$$
x(t)=\sum_{n=-N}^{N}
\frac{T_p}{\mathrm{i}\,2\pi\,n}\,\dot{c}_n\,e^{\mathrm{i}\,2\pi\,n\,t/T_p},\qquad \dot{c}_n=\dot{c}_{-n}^*,
$$

kjer so $\dot{c}_n$ konstante Fourierove vrste za $\dot{x}(t)$. Za primer, glejte zglede v nadaljevanju.

#### Fourierove vrste lihih in sodih funkcij

Funkcija $x(t)$ je **soda**, če velja:

$$
x(-t)=x(t),
$$

takrat velja tudi:

$$
\int_a^a x(t)\,\mathrm{d}t=2\,\int_0^a x(t)\,\mathrm{d}t.
$$

Funkcija je **liha**, če velja:

$$
x(-t)=-x(t),
$$

takrat velja tudi

$$
\int_a^a x(t)\,\mathrm{d}t=0.
$$

Izpostavimo lahko, da je funkcija $\cos()$ soda, funkcija $\sin()$ pa liha.

Za poznejšo obravnavo bo pomemben produkt dveh funkcij (ko npr. pri izračunu koeficientov Fourierove vrste); ali je produkt  funkcij $x(t)\,y(t)$ sodi ali lihi, je odvisno od sodosti in lihosti funkcij $x(t)$ in $y(t)$. Odnosi so definirani v tabeli spodaj.

| če $x(t)$ | če $y(t)$ | je rezultat $x(t)\,y(t)$|
|:-|-:|:-:|
| liha  | liha  | *soda*  |
| liha  | soda  | *liha*  |
| soda  | liha  | *liha*  |
| soda  | soda  | *soda*  |

Glede na zgornjo tabelo lahko sklenemo (glejte tudi {ref}`sec:FV_sin_cos`):

:::{note}

* če iščemo Fourierovo vrsto *sode* periodične funkcije $x(t)$, potem bodo sinusni členi $b_n$ enaki nič (ker je produkt sode in lihe funkcije liha funkcija; integral lihe funkcije pa je 0).
* če iščemo Fourierovo vrsto *lihe* peridične funkcije $x(t)$, potem bodo cosinusni členi $a_n$ enaki nič (ker je produkt lihe in sode funkcije liha funkcija; integral lihe funkcije pa je 0).

:::

## Nekateri zgledi

### Kompleksno periodična funkcija

Najprej si bomo pogledali kompleksno periodično funkcijo, ki smo jo spoznali in definirali že zgoraj. Funkcija je definirana z $N=10$ harmonskimi komponentami $c_n$, ki so v spodnjem primeru znane; tukaj bomo pokazali, da nas uporaba zgoraj definiranih enačb vodi v točno identifikacijo le-teh.

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

Tp = 2
t, dt = np.linspace(-0.5*Tp,2.5*Tp,301, retstep=True)
a0 = 1.
N = 10
seed = 0
rg = np.random.default_rng(seed)
a = rg.normal(size=N)*1/np.arange(1,N+1)**2 #skaliranje na koncu, da imajo višje komponente imajo manjšo amplitudo
b = rg.normal(size=N)*1/np.arange(1,N+1)**2

c = np.zeros(2*N+1, dtype='complex')
c[N+1:] = 0.5*a-0.5j*b
c[N] = a0/2
c[:N] = np.conj(c[N+1:])[::-1]

x = np.zeros(len(t), 'complex')
for n in range(-N,N+1):
    x += c[N+n]*np.exp(2j*np.pi*n*t/Tp)
x = np.real(x) # teoretično pričakujemo samo realno rezultat (imaginarni del mora biti na nivoju numerične napake)

fig, ax1 = plt.subplots()
plt.title(f'Kompleksno periodična funkcija')

ax1.set_xlabel('$t\\quad $[s]')
ax1.set_ylabel('$x(t)\quad$[-]')
ax1.tick_params(axis='y')
ax1.vlines(0, -2, 2, 'k', lw=0.5)
ax1.hlines(0, np.min(t), np.max(t), 'k', lw=0.5)
ax1.plot(t, x, label='Definicija v kompleksni obliki', c='C0', lw=2, alpha=1)
ax1.vlines([0, Tp], -2, 2, ls='-.', lw=1)
ax1.hlines(a0/2, np.min(t), np.max(t), ls='-.', lw=1)
ax1.legend(loc=1)
plt.ylim(-1,2)
plt.show()
```

V primeru zgoraj je peridočna funkcija generirana na podlagi vektorja Fourierovih koeficientov:

```python
c
```

    array([-0.00632711+0.00521257j, -0.00434404+0.00254093j,
            0.00739907-0.00247109j,  0.01330612-0.00555366j,
            0.00502215-0.01017038j, -0.01071339-0.02491822j,
            0.00327813-0.00683724j,  0.03557904-0.12916838j,
           -0.01651311+0.00516575j,  0.06286511-0.31163723j,
            0.5       +0.j        ,  0.06286511+0.31163723j,
           -0.01651311-0.00516575j,  0.03557904+0.12916838j,
            0.00327813+0.00683724j, -0.01071339+0.02491822j,
            0.00502215+0.01017038j,  0.01330612+0.00555366j,
            0.00739907+0.00247109j, -0.00434404-0.00254093j,
           -0.00632711-0.00521257j])

Opazimo, da je vektor kompleksno konjugiran okoli središčnega elementa. Poljubni element lahko zgolj na podlagi časovne vrste $x(t)$ določimo tako, da sledimo definiciji:

$$
c_n = \frac{1}{T_p}\,\int_0^{T_p} x(t)\,e^{-\mathrm{i}\,2\pi\,n\,t/T_p}\,\mathrm{d}t
$$

kar je v numerični implementaciji (za `n=1`):

```python
n=1 # poskusite še druge vrednosti, tudi n>N!
sel = np.logical_and(t>=0, t<=Tp)
np.trapz(x[sel]*np.exp(-2j*np.pi*n*t[sel]/Tp), dx=dt)/Tp
```

    (0.06286511054669663+0.3116372312686761j)

Pri tem smo pazili, da smo izbrali (`sel`) samo eno periodo v časovni vrsti. Tukaj velja poudariti, da moramo integrirati po celotni periodi. Rezultat je **točen** do nivoja natančnosti zapisa v računalnik.

Vredno je opozoriti, da moramo biti zelo natančni pri implementaciji izpeljanih izrazov. Hitro lahko dobimo *zelo dober* rezultat; vendar dokler ni *točen* do nivoja natančnosti zapisa v računalnik, do takrat je nekje v implementaciji napaka.

Napako bi lahko naredili, če bi naša diskretizacija bila takšna, da ne bi ključevala točke pri 0s in točke pri $T_p$. Tako napako lahko preiskusite tako, greste par vrstic navzgor v definicijo slike in vrstico:
```python
t, dt = np.linspace(-0.5*Tp,2.5*Tp,301, retstep=True)
```
spremenite v (301 v 300):
```python
t, dt = np.linspace(-0.5*Tp,2.5*Tp,300, retstep=True)
```

### Odvod / Integral kompleksno periodične funkcije

Nadaljujemo z zgledom zgoraj in si najprej poglejmo, da je odvod po času res enak Fourierovi vrsti, kjer smo vsakega od členov preprosto odvajali.

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

Tp = 2
t, dt = np.linspace(-0.5*Tp,2.5*Tp,301, retstep=True)
a0 = 1.
N = 10
seed = 0
rg = np.random.default_rng(seed)
a = rg.normal(size=N)*1/np.arange(1,N+1)**2 #skaliranje na koncu, da imajo višje komponente imajo manjšo amplitudo
b = rg.normal(size=N)*1/np.arange(1,N+1)**2

c = np.zeros(2*N+1, dtype='complex')
c[N+1:] = 0.5*a-0.5j*b
c[N] = a0/2
c[:N] = np.conj(c[N+1:])[::-1]

x = np.zeros(len(t), 'complex')
for n in range(-N,N+1):
    x += c[N+n]*np.exp(2j*np.pi*n*t/Tp)
x = np.real(x) # teoretično pričakujemo samo realno rezultat (imaginarni del mora biti na nivoju numerične napake)
x_d_t = np.gradient(x,dt)

x_d_v = np.zeros(len(t), 'complex') # ODVOD Z VRSTAMI
for n in range(-N,N+1):
    x_d_v += c[N+n]*np.exp(2j*np.pi*n*t/Tp)*2j*np.pi*n/Tp
x_d_v = np.real(x_d_v) # teoretično pričakujemo samo realno rezultat (imaginarni del mora biti na nivoju numerične napake)

fig, ax1 = plt.subplots()
ax1.set_xlabel('$t\\quad $[s]')
ax1.vlines([0, Tp], -4, 4, 'k', lw=0.5)
ax1.plot(t, x_d_t, label='$\dot{x}(t)$ - izračunano v času', c='C0', lw=2, alpha=1)
ax1.plot(t, x_d_v, label='$\dot{x}(t)$ - vrste', ls=':', c='C1', lw=2, alpha=1)
ax1.legend()
plt.show()
```

Zgornji primer lahko nadaljujemo in izvorni pomik $x(t)$ identificiramo iz Fourierovih vrst za hitrost $\dot x(t)$, glejte kodo in sliko spodaj. Pri integriranju imamo težavo v primeru Fourierovega koeficienta s frekvenco 0 (statična komponenta); v kodi spodaj to frekvenco enostavno preskočimo, saj je ne moremo določiti. Podobno težavo bi imeli v času; če integriramo nedoločeni integral, potem pri integriranju rabimo začetno vrednost. Na sliki spodaj se zato med obema pristopoma pokaže odstopanje v velikosti statične komponente.

```python
#x_d_t je hitrost, a_0 pa statična komponenta prenešena iz primera zgoraj

N = 10 # identifikacija Fourierovih koeficientov hitrosti
c_d = np.zeros(2*N+1, dtype='complex')
n = np.arange(-N,N+1)
sel = np.logical_and(t>=0, t<=Tp)
for i in n:
    c_d[i+N] = np.trapz(x_d_t[sel]*np.exp(-2j*np.pi*i*t[sel]/Tp), dx=dt)/Tp

x_v = np.zeros(len(t), 'complex') # INTEGRAL Z VRSTAMI
for n in range(-N,N+1):
    if n!=0:
        x_v += c_d[N+n]*np.exp(2j*np.pi*n*t/Tp)/(2j*np.pi*n/Tp)
x_v = np.real(x_v) # teoretično pričakujemo samo realno rezultat (imaginarni del mora biti na nivoju numerične napake)

fig, ax1 = plt.subplots()

ax1.set_xlabel('$t\\quad $[s]')
ax1.plot(t, x, label='$x(t)$ - definicija', c='C0', lw=2, alpha=1)
ax1.plot(t, x_v, label='$x(t)$ - vrste iz koeficientov FV za hitrost $\dot{c}$', ls=':', c='C1', lw=2, alpha=1)
ax1.hlines(a0/2, np.min(t), np.max(t), ls='-.', lw=1)
ax1.hlines(0, np.min(t), np.max(t), ls='-.', color='C1', lw=1)
ax1.legend()
plt.show()
```

### Periodična funkcija

Tukaj si bomo pogledali periodično funkcijo, ki pa ni kompleksno harmonska. Primer takšne funkcije je naraščajoča žaga prikazana na spodnji sliki.

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

Tp = 2
t, dt = np.linspace(-0.5*Tp,2.5*Tp,301, retstep=True)
x = t%Tp

fig, ax1 = plt.subplots()

ax1.set_xlabel('$t\\quad $[s]')
ax1.set_ylabel('$x(t)\quad$[-]', color='C1')
ax1.tick_params(axis='y', labelcolor='C1')
ax1.vlines(0, -0.5, 2.5, 'k', lw=0.5)
ax1.hlines(0, np.min(t), np.max(t), 'k', lw=0.5)
ax1.plot(t, x, label='Periodični podatki', c='C0', lw=2, alpha=1)
ax1.legend(loc=1)
plt.ylim(-0.5,2.5)
plt.show()
```

Kljub temu, da funkcija ni kompleksno periodična, teorija Fourierovih vrst pa je bila izpeljana ob tej predpostavki, poskusimo identificirati harmonske komponente. Najprej to naredimo na diskretni časovni vrsti.
Osredotočimo se na del podatkov, ki se ponavlja:

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

Tp = 2
t, dt = np.linspace(0,Tp,101, retstep=True)
x = t

fig, ax1 = plt.subplots()

ax1.set_xlabel('$t\\quad $[s]')
ax1.set_ylabel('$x(t)\quad$[-]', color='C1')
ax1.tick_params(axis='y', labelcolor='C1')
ax1.vlines(0, -0.5, 2.5, 'k', lw=0.5)
ax1.hlines(0, np.min(t), np.max(t), 'k', lw=0.5)
ax1.plot(t, x, label='Periodični podatki', c='C0', lw=2, alpha=1)
ax1.legend(loc=1)
plt.ylim(-0.5,2.5)
plt.show()
```

In identificiramo Fourierove koeficiente:

```python
N = 10
c = np.zeros(2*N+1, dtype='complex')
n = np.arange(-N,N+1)
for i in n:
    c[i+N] = np.trapz(x*np.exp(-2j*np.pi*i*t/Tp), dx=dt)/Tp
c
```

    array([-3.85108612e-16-0.03077684j, -3.83373888e-16-0.03442023j,
           -5.03069808e-16-0.03894743j, -2.68882139e-16-0.04473743j,
            1.52655666e-16-0.05242184j, -5.37764278e-17-0.06313752j,
           -4.16333634e-17-0.07915815j, -5.63785130e-17-0.10578895j,
           -6.24500451e-17-0.15894545j, -1.38777878e-17-0.31820516j,
            1.00000000e+00+0.j        , -1.38777878e-17+0.31820516j,
           -6.24500451e-17+0.15894545j, -5.63785130e-17+0.10578895j,
           -4.16333634e-17+0.07915815j, -5.37764278e-17+0.06313752j,
            1.52655666e-16+0.05242184j, -2.68882139e-16+0.04473743j,
           -5.03069808e-16+0.03894743j, -3.83373888e-16+0.03442023j,
           -3.85108612e-16+0.03077684j])

Na podlagi identificiranih Fourierovih koeficientov, lahko definiramo kompleksno periodično funkcijo:

$$
x(t)=\sum_{n=-N}^{N}
c_n\,e^{\mathrm{i}\,2\pi\,n\,t/T_p},\qquad c_n=c_{-n}^*.
$$

Konkretna numerična koda je prikazana spodaj (pri tem smo uporabili novo numerično polje `x_r`, da ga ločimo od podatkov v obliki `x`):

```python
x_r = np.zeros(len(t), 'complex')
for n in range(-N,N+1):
    x_r += c[N+n]*np.exp(2j*np.pi*n*t/Tp)
x_r = np.real(x_r) # teoretično pričakujemo samo realno rezultat (imaginarni del mora biti na nivoju numerične napake)

```

```python
fig, ax1 = plt.subplots()
ax1.set_xlabel('$t\\quad $[s]')
ax1.set_ylabel('$x(t)\quad$[-]')
ax1.vlines(0, -0.5, 2.5, 'k', lw=0.5)
ax1.hlines(0, np.min(t), np.max(t), 'k', lw=0.5)
ax1.plot(t, x, label='Izvorni periodični podatki', c='C0', lw=2, alpha=1)
ax1.plot(t, x_r, label='Kompleksno periodična funkcija', c='C1', lw=2, alpha=.6)
ax1.legend(loc=1)
plt.ylim(-0.5,2.5)
plt.show()
```

Opazimo, da identificirana kompleksno harmonska funkcija relativno dobro opiše izvorne periodične podatke. Če bi število harmonskih ($N$) komponent povečali, bi se identificirana funkcija izvorni bolje prilegala, vendar pa bi na mestu nezveznosti še vedno imeli odstopanje. Odstopanje na mestu nezveznosti je znano pod terminom **Gibbsov** fenomen. Resnici na ljubo je večina inženirskih aplikacij zveznih in zato ponavadi Gibbsov fenomen v praksi nima večjega pomena.

### Analitična obravnava periodične funkcije

Zgoraj smo Fourierovo vrsto izračunali na diskrenih podatkih in smo zato omejeni koliko harmonskih komponent lahko izračunamo (`N<len(t)/2`; zakaj je $N$ vezan na dolžino časovne vrste, bomo spoznali pozneje). Tukaj si bomo pogledali obravnavo zveznih sistemov in s pomočjo strojnega izpeljevanja določili Fourierovo vrsto. Kot primer periodične funkcija si bomo izbrali naraščajočo žago, ki bo proporcionalna s časom `t`, perioda bo `Tp`:

```python
import sympy as sym
sym.init_printing()

t, T_p = sym.symbols('t, T_p', real=True)
x_r = sym.fourier_series(t, limits=(t, 0, T_p))
```

Vrsta sestavljena iz prvih 5 členov torej je:

```python
x_r.truncate(5)
```

Posamezni člen lahko izračunamo tudi sami:

```python
n = 1
c_n = sym.integrate(t*sym.exp(-sym.I*2*n*sym.pi*t/T_p), (t, 0, T_p)).simplify()/T_p
c_n
```

Da dobimo harmonsko komponento, moramo `c_n` pomnožiti s pripadajočo pozitivno in negativno frekvenco:

```python
sym.simplify(c_n*sym.exp(sym.I*2*sym.pi*n*t/T_p)+sym.conjugate(c_n)*sym.exp(-sym.I*2*sym.pi*n*t/T_p))
```

In slika pri `T_p=2` in različnem številu členov v vrsti:

```python
import sympy as sym
sym.init_printing()

t, T_p = sym.symbols('t, T_p')
x_r = sym.fourier_series(t, limits=(t, 0, T_p))

p0 = sym.plot(t, (t, 0 ,2), label='x(t)=t', show=False, line_color='k', ylabel='x(t)', legend=True)
p1 = sym.plot(x_r.truncate(5).subs(T_p,2), (t, 0 ,2), line_color='C0', label='5 členov',  show=False)
p2 = sym.plot(x_r.truncate(20).subs(T_p,2), (t, 0 ,2), line_color='C1', label='20 členov', show=False)
p0.append(p1[0])
p0.append(p2[0])
p0.show()
```

(sec:dirac_cas)=
## Diracova delta funkcija

:::{note}
Diracova delta funkcija je definirana z:

$$
\delta(t)=0 \quad\textrm{ za } t\ne 0 \qquad\textrm{ in } \qquad \int_{-\infty}^{+\infty}\delta(t)\,\textrm{d}t=1.
$$

:::

Opomba: dejansko to ni navadna funkcije, ampak t.i. generalizirana funkcija.

Ker je integral Diracove delta funkcije 1, se včasih imenuje tudi funkcija enotskega impulza, definirana tudi kot:

$$
\delta_{\varepsilon}=
\begin{cases}
\frac{1}{\varepsilon}&\textrm{ za }\quad -\frac{\varepsilon}{2}\le t\le+\frac{\varepsilon}{2}\\
0& \textrm{sicer}.
\end{cases}.
$$

Pri definiranju enotskega impulza si lahko pomagamo tudi z **koračno funkcijo** (**Heaviside funkcija**), ki je definirana kot:

$$
u(t)=
\begin{cases}
1&\textrm{ za }\quad t>0\\
0& \textrm{sicer}.
\end{cases}.
$$

Dve koračni funkciji ste definirani spodaj:

```python
import sympy as sym
t, ε = sym.symbols(r't, \varepsilon')
unit_step = sym.Heaviside(t+ε/2)
p1 = sym.plot(unit_step.subs(ε, +0.5), (t,-1,1), line_color='C0', line_style=':',
              show=False, label='Koračna funkcija $u(t+\\varepsilon/2)$')
p2 = sym.plot(-unit_step.subs(ε, -0.5), (t,-1,1), line_color='C1',
              show=False, label='Koračna funkcija $-u(t-\\varepsilon/2)$')
p1.extend(p2)
p1.legend = True
p1.show()
```

Do enotskega impulza pridemo z uteženim seštevanjem dveh enotskih korakov:

$$
\delta_{\varepsilon}=\frac{1}{\varepsilon}\Big(u\big(t+\frac{\varepsilon}{2}\big)-u\big(t-\frac{\varepsilon}{2}\big)\Big)
$$

Poglejmo si kako koračno funkcijo pretvorimo v enotski impulz:

```python
import sympy as sym
t, ε = sym.symbols(r't, \varepsilon')
podatki = {ε: 0.5}
unit_step = sym.Heaviside(t+ε/2) # tukaj moramo pravilno normirati (H0), glej help!
δ_ε = 1/ε * (unit_step-unit_step.subs(ε, -ε))
δ_ε
```

```python
p1 = sym.plot(δ_ε.subs(podatki), (t,-.5,.5), line_color='C0', line_style=':',
              ylabel='$\\delta_{\\varepsilon}(t)$')
```

in nato integriramo (rezultat je pričakovano enak 1; spremenite $\varepsilon$ v `podatki` zgoraj!):

```python
sym.integrate(δ_ε.subs(podatki), (t, -sym.oo, sym.oo))
```

Med Diracovo delta funkcijo in enotskim korakom velja tudi sledeča povezava ($u(t)$ je koračna funkcija):

$$
\delta(t)=\lim_{\varepsilon\rightarrow 0}\delta_{\varepsilon}(t)=\frac{\textrm{d}}{\textrm{d}t}u(t).
$$

(sec:dirac_lastnosti)=
### Lastnosti Diracove delta funkcije

:::{note}

1. je soda: $\delta(t)=\delta(-t)$

1. *sejalna lastnost*, to pomeni, da integral navadne funkcije $x(t)$ in premaknjene Diracove funkcije $\delta(t-a)$ vrne vrednost funkcije $x(a)$:$\int_{-\infty}^{+\infty}x(t)\,\delta(t-a)\,\textrm{d}t= x(a)$.

1. $\int_{-\infty}^{+\infty}e^{\pm\textrm{i}\,2\,\pi\,a\,t}\,\textrm{d}t= \delta(a)$ ali tudi: $\int_{-\infty}^{+\infty}e^{\pm\textrm{i}\,a\,t}\,\textrm{d}t= 2\,\pi\,\delta(a)$, za dokaz glejte {cite:t}`bendat_2011,shin2008` (str. 40).

1. $\delta(a\,t)=\delta(t)/|a|$.

1. $\int_{-\infty}^{+\infty}f(t)\,\delta^{(n)}(t)\,\textrm{d}t= (-1)^n\,f^{(n)}(a)$, kjer $n$ označuje odvod.

:::

### Padajoča žaga in njen odvod

Predpostavimo periodično padajočo žagasto funkcijo, kot je prikazana spodaj:

```python
import sympy as sym
sym.init_printing()

t, T_p = sym.symbols('t, T_p')
podatki = {T_p: 2}
x = 1-t/T_p
x_r = sym.fourier_series(x, limits=(t, 0, T_p))

p0 = sym.plot((x).subs(podatki), (t, 0 ,2), label='x(t)=t', show=False, line_color='k', ylabel='x(t)', legend=True)
p1 = sym.plot(x_r.truncate(5).subs(podatki), (t, 0 ,2), line_color='C0', label='5 členov',  show=False)
p2 = sym.plot(x_r.truncate(20).subs(podatki), (t, 0 ,2), line_color='C1', label='20 členov', show=False)
p0.append(p1[0])
p0.append(p2[0])
p0.show()
```

Vrsta padajoče žagaste funkcije je definirana kot:

$$
x(t) = \frac{1}{2}+\frac{1}{\pi}\sum_{n=1}^{\infty}\frac{1}{n}\sin\Big(\frac{2\pi\,n\,t}{T_p}\Big)
$$

Ali s pomočjo strojnega izpeljevanja:

```python
import sympy as sym
sym.init_printing()

t, T_p = sym.symbols('t, T_p')
podatki = {T_p: 2}
x = 1-t/T_p
x_r = sym.fourier_series(x, limits=(t, 0, T_p))

x_r.truncate(5)
```

Če odvajamo padajočo žagasto funkcijo v času, je rezultat enak:

$$
\dot x(t) = -\frac{1}{T_p} + \sum_{n=-\infty}^{+\infty}\delta(t-n\,T_p)
$$

ker pa lahko odvajamo tudi vrsto, velja tudi:

$$
\dot x(t) = \frac{2}{T_p}\sum_{n=1}^{\infty}\cos\Big(\frac{2\pi\,n\,t}{T_p}\Big)
$$

enačenje obeh izrazov vodi v izraz:

:::{note}

Periodični *vlak impulzov* (ang. *train of impulses*) ima v obliki Fourierovih vrst konstantne koeficiente ($2/T_p$):

$$
\sum_{n=-\infty}^{+\infty}\delta(t-n\,T_p) =  \frac{1}{T_p} +\frac{2}{T_p}\sum_{n=1}^{\infty}\cos\Big(\frac{2\pi\,n\,t}{T_p}\Big)
$$

:::

Slednjo ugotovitev bomo pozneje potrebovali, ko bomo obravnavali vzorčenje signalov.

```python
import sympy as sym
sym.init_printing()

t, T_p = sym.symbols('t, T_p')
podatki = {T_p: 2}
x = 1-t/T_p
x_r = sym.fourier_series(x, limits=(t, 0, T_p))

p0 = sym.plot(sym.diff(x_r.truncate(20),t).subs(podatki), (t, 0 ,6), line_color='C1', label='20 členov', show=False,
              ylabel='Vrsta: $\\dot x(t)$', legend=True, adaptive=False, nb_of_points=800)
p1 = sym.plot(sym.diff(x_r.truncate(5),t).subs(podatki), (t, 0 ,6), line_color='C0', label='5 členov',  show=False)
p0.append(p1[0])
p0.show()
```

## Amplitudni in fazni spekter

Fourierovi koeficienti $c_n$, ki jih obravnavamo pri Fourierovih vrstah, predstavljajo amplitudno in fazno informacijo o določeni harmonski komponenti. Poglejmo si primer periodičnega pravokotnega vala: slika spodaj.

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

# signal
T_p = 2
n = 200
x = np.ones(n)
x[:n//4] = -1.
x[n//4:2*n//4] = 1.
x[2*n//4:3*n//4] = -1.
x[3*n//4:n] = 1.

t = np.arange(n)*2*T_p/(n-1) - T_p/2
dt = t[1]-t[0]

# zakaj je ta koda napačna?
# t, dt = np.linspace(-0.5*Tp,1.5*Tp,201, retstep=True)
# x = np.sign(np.sin(2*np.pi*t/Tp))

# Fourierove vrste
N = 10
c = np.zeros(2*N+1, dtype='complex')
n = np.arange(-N,N+1)
#omejiti se moramo samo na eno periodo, sicer koeficienti niso pravilno izračunani
sel = np.logical_and(t>=0,t<=T_p)
for i in n:
    c[i+N] = np.trapz(x[sel]*np.exp(-2j*np.pi*i*t[sel]/T_p), dx=dt)/T_p

# rekonstrukcija
x_r = np.zeros(len(t), 'complex')
for _ in range(-N,N+1):
    x_r += c[N+_]*np.exp(2j*np.pi*_*t/T_p)
x_r = np.real(x_r)

fig, ax1 = plt.subplots()

ax1.set_xlabel('$t\\quad $[s]')
ax1.set_ylabel('$x(t)\quad$[-]')
ax1.tick_params(axis='y')
ax1.plot(t, x, label='Periodični podatki', c='C0', lw=2, alpha=1)
ax1.plot(t, x_r, label='Rekonstukcija preko Fourierovih vrst', c='C1', lw=2, alpha=1)
ax1.legend(loc=4)
plt.show()
```

Pri pripravi podatkov zgoraj smo izračunali Fourierove koeficiente $c$:

```python
c
```

    array([-2.77555756e-17-1.97261469e-03j, -3.64291930e-17+6.95625956e-02j,
           -1.70002901e-16-1.57829366e-03j,  3.15719673e-16+8.98508111e-02j,
            1.11022302e-16-1.18384586e-03j,  4.85722573e-17+1.26228518e-01j,
           -2.77555756e-17-7.89293161e-04j,  6.76542156e-17+2.10869640e-01j,
           -2.08166817e-17-3.94665909e-04j,  4.33680869e-17+6.33344596e-01j,
           -5.55111512e-17+0.00000000e+00j,  4.33680869e-17-6.33344596e-01j,
           -2.08166817e-17+3.94665909e-04j,  6.76542156e-17-2.10869640e-01j,
           -2.77555756e-17+7.89293161e-04j,  4.85722573e-17-1.26228518e-01j,
            1.11022302e-16+1.18384586e-03j,  3.15719673e-16-8.98508111e-02j,
           -1.70002901e-16+1.57829366e-03j, -3.64291930e-17-6.95625956e-02j,
           -2.77555756e-17+1.97261469e-03j])

Fourierove koeficiente lahko prikažemo v grafični obliki. Če prikažemo amplitudo kompleksnega števila $|c|$, potem govorimo o amplitudnem spektru: za določeni večkratnih osnovne frekvence $1/T_p$ prikažemo vsebnost amplitude harmonske komponente. Podobno govorimo o faznem spektru, če prikažemo $\angle c_n$.

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

fig, ax1 = plt.subplots()

ax1.set_xlabel('$n\\quad $[-]')
ax1.set_ylabel('$|c_n|\\quad$[-]', color='C0')
ax1.tick_params(axis='y', labelcolor='C0')
ax1.vlines(0, -2, 2.5, 'k', lw=0.5)
ax1.hlines(0, np.min(n), np.max(n), 'k', lw=0.5)
ax1.plot(n, np.abs(c), 'o-', label='Amplituda $c_n$', c='C0', lw=2, alpha=1)
ax1.set_xticks(ticks=n)

ax2 = ax1.twinx()
ax2.plot(n, np.angle(c), 'o-', label='Faza $c_n$', c='C1', lw=2, alpha=1)
ax2.set_ylabel('$\\angle c_n \quad$[-]', color='C1')
ax2.tick_params(axis='y', labelcolor='C1')

ax2.set_ylim(-2.,2.)
plt.show()
```

(sec:parseval)=
## Parsevalov teorem in močnostni spekter

### Parsevalov teorem

Če je $x(t)$ napetost merjena čez upor upornosti $1\Omega$, potem je trenutna moč enaka $P(t)=R\,x^2(t)$, povprečna moč periodičnega signala znotraj ene periode pa je:

$$
\frac{1}{T_p}\int_0^{T_p}x^2(t)\,\textrm{d}t
$$

ker velja $x(t)=\sum_{n=-\infty}^{\infty}c_n\,e^{\mathrm{i}\,2\pi\,n\,t/T_p}$ in $x^2(t)=x(t)\,x^*(t)$ je povprečna moč definirana tudi kot:

$$
\frac{1}{T_p}\int_0^{T_p}x^2(t)\,\textrm{d}t=
\frac{1}{T_p}\int_0^{T_p}\sum_{n=-\infty}^{\infty}c_n\,e^{\mathrm{i}\,2\pi\,n\,t/T_p}\,\sum_{m=-\infty}^{\infty}c_m^*\,e^{-\mathrm{i}\,2\pi\,m\,t/T_p}\,\textrm{d}t
$$

kar poenostavimo v:

$$
\frac{1}{T_p}\int_0^{T_p}x^2(t)\,\textrm{d}t=
\frac{1}{T_p}\sum_{n=-\infty}^{\infty}\,\sum_{m=-\infty}^{\infty}c_n\,c_m^*\,\int_0^{T_p}e^{\mathrm{i}\,2\pi\,(n-m)\,t/T_p}\,\textrm{d}t
$$

Integral harmonske funkcije na zaključeni periodi je vedno nič razen, ko je frekvenca 0. Ko je frekvenca enaka 0, je podintegralska funkcija enaka 1 in integral $T_p$:

$$
\int_0^{T_p}e^{\mathrm{i}\,2\pi\,(n-m)\,t/T_p}\,\textrm{d}t=
\begin{cases}
T_p,&\textrm{če}\quad m=n,\\
0,&\textrm{sicer.}\\
\end{cases}
$$

:::{note}

Sledi **Parsevalov teorem** ali tudi Parsevalova enakost:

$$
\frac{1}{T_p}\int_0^{T_p}x^2(t)\,\textrm{d}t=
\sum_{n=-\infty}^{\infty}c_n\,c_n^*,
$$

ki poveže povprečno moč v času z močjo v frekvenčni domeni.
:::

Poglejmo si konkreten zgled:

```python
T_p = 2
t, dt = np.linspace(0,T_p,101, retstep=True)
a0 = 1.
N = 10
seed = 0
rg = np.random.default_rng(seed)
a = rg.normal(size=N)*1/np.arange(1,N+1)**2 #skaliranje na koncu, da imajo višje komponente imajo manjšo amplitudo
b = rg.normal(size=N)*1/np.arange(1,N+1)**2

c = np.zeros(2*N+1, dtype='complex')
c[N+1:] = 0.5*a-0.5j*b
c[N] = a0/2
c[:N] = np.conj(c[N+1:])[::-1]

x = np.zeros(len(t), 'complex')
for n in range(-N,N+1):
    x += c[N+n]*np.exp(2j*np.pi*n*t/T_p)
x = np.real(x) # teoretično pričakujemo samo realno rezultat (imaginarni del mora biti na nivoju numerične napake)
```

Vrednost iz časa:

```python
np.trapz(x**2,dx=dt)/T_p
```

Vrednost na podlagi Fourierovih koeficientov:

```python
np.dot(c,np.conjugate(c))
```

    (0.4912052334566629+0j)

Po pričakovanjih dobimo enako vrednost (do nivoja numerične napake)!

### Močnostni spekter

Parsevalov teorem povprečno moč definira kot vsoto kvadratov Fourierovih koeficientov oz. frekvenčnih komponent. Prikaz kvadratov posameznih frekvenčnih komponent imenujemo močnostni spekter. Pri tem je treba poudariti, da močnostni spekter izgubi informacijo o fazi in je vedno definiran z realnimi vrednostmi.

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

# signal
T_p = 2
t, dt = np.linspace(-0.5*T_p,1.5*T_p,201, retstep=True)
x = np.sign(np.sin(2*np.pi*t/T_p))

# Fourierove vrste
N = 10
c = np.zeros(2*N+1, dtype='complex')
n = np.arange(-N,N+1)
#omejiti se moramo samo na eno periodo, sicer koeficienti niso pravilno izračunani
sel = np.logical_and(t>=0,t<=T_p)
for i in n:
    c[i+N] = np.trapz(x[sel]*np.exp(-2j*np.pi*i*t[sel]/T_p), dx=dt)/T_p

fig, ax1 = plt.subplots()

ax1.set_xlabel('$n\\quad $[-]')
ax1.set_ylabel('$|c_n|^2\\quad$[-]')
ax1.plot(n, np.real(c*np.conjugate(c)), 'o', label='Amplituda $c_n$', c='C0', lw=2, alpha=1)
ax1.set_xticks(ticks=n)
plt.show()
```

```python

```


```python
import numpy as np
from numba import jit
import sympy
```

# Item XV

Considering the following inner product:
$$
\langle p(x),q(x) \rangle =\int_{-1}^{1} \overline{p(x)}q(x) dx
$$

* Let $A= [1|x|x^2|...|x^{n-1}]$ be the "matrix" whose "columns" are the monomials $x^j$, for $j=0,...,n-1$. Each column is a function in $L^2[-1,1]$. compute the $QR$ decomposition of $A$.
* Let $A=[1|\sin(2\pi x)|\sin(4\pi x)|...|x^{n-1}]$ be the "matrix" whose "columns" are the functions $1$ and $\sin(2\pi x)$, for $j=1,...,n-1$. Each column is a function in $L^2[-1,1]$. Compute the $QR$ decomposition of $A$.
* Do part (a) numerically. Make sure you understand what you are doing since this is a important concept that links symbolic computing with numerical computing.

---

```python
# This is a generic version of Gram-Schmidt, by default it works on matrices.
# For other uses, replace default argument functions.
def generic_gs(
        elems,
        scalar = lambda a,x : a*x,
        prod = lambda x,y : np.sum(x*y),
        neg = lambda x,y : x-y,
    ):
    """
    elems = [T]
    scalar :: T -> Float -> T
    prod :: T -> T -> Float
    neg :: T -> T -> T
    NOTE: if is used for a regular matrix, elems must be row-wise.
    """
    n = len(elems)
    r = np.zeros((n,n))
    for i in range(n):
        for j in range(i):
            projection = prod(elems[j],elems[i])/prod(elems[j],elems[j])
            r[j,i] = projection
            elems[i] = neg(elems[i],scalar(projection,elems[j]))
        norm2 = prod(elems[i],elems[i])
        if norm2<0:
            print("Warning: negative norm2=%f at i=%d!"%(norm2,i))
            return None
        norm = norm2**0.5
        r[i,i] = norm
        elems[i] = scalar(norm**-1,elems[i])
    return r
```

```python
def symbolic_inner_product(f,q):
    x = sympy.Symbol('x')
    v = sympy.integrate(f*q,(x,-1,1))
    # We evaluate the expresion as a number because we can't afford the whole symbolic
    # expression...
    return float(v)
```

```python
# We define the list of functions for part a
def part_a_funcs(n):
    x = sympy.Symbol('x')
    part = [x**i for i in range(n)]
    return part
```

```python
def part_b_funcs(n):
    x = sympy.Symbol('x')
    part = [x**0] + [sympy.sin(2*i*sympy.pi*x) for i in range(1,n)]
    return part
```

```python
# We can print an array of functions:
print(part_a_funcs(10))
print(part_b_funcs(10))
```

    [1, x, x**2, x**3, x**4, x**5, x**6, x**7, x**8, x**9]
    [1, sin(2*pi*x), sin(4*pi*x), sin(6*pi*x), sin(8*pi*x), sin(10*pi*x), sin(12*pi*x), sin(14*pi*x), sin(16*pi*x), sin(18*pi*x)]

```python
# We compute the decompositions
FUNCS = [part_a_funcs(5),part_b_funcs(5),
         part_a_funcs(20),part_b_funcs(20),
         part_a_funcs(100),part_b_funcs(100)]
for funcs in FUNCS:
    # Print Original matrix
    print("-"*20+"Functions:")
    print(funcs)
    # Perform QR decomposition using generic G-S
    r = generic_gs(funcs,prod=symbolic_inner_product)
    if r is None:
        print("Couldn't compute R!!!:")
    else:
        # Print Q
        print("Q:")
        print(funcs)
        # Print R
        print("R:")
        print(r)
```

    --------------------Functions:
    [1, x, x**2, x**3, x**4]
    Q:
    [0.707106781186547, 1.22474487139159*x, 2.37170824512628*x**2 - 0.790569415042095, 4.67707173346743*x**3 - 2.80624304008046*x, 9.28077650307342*x**4 - 7.95495128834865*x**2 + 0.795495128834865]
    R:
    [[1.41421356 0.         0.47140452 0.         0.28284271]
     [0.         0.81649658 0.         0.48989795 0.        ]
     [0.         0.         0.42163702 0.         0.36140316]
     [0.         0.         0.         0.21380899 0.        ]
     [0.         0.         0.         0.         0.1077496 ]]
    --------------------Functions:
    [1, sin(2*pi*x), sin(4*pi*x), sin(6*pi*x), sin(8*pi*x)]
    Q:
    [0.707106781186547, 1.0*sin(2*pi*x), 1.0*sin(4*pi*x), 1.0*sin(6*pi*x), 1.0*sin(8*pi*x)]
    R:
    [[1.41421356 0.         0.         0.         0.        ]
     [0.         1.         0.         0.         0.        ]
     [0.         0.         1.         0.         0.        ]
     [0.         0.         0.         1.         0.        ]
     [0.         0.         0.         0.         1.        ]]
    --------------------Functions:
    [1, x, x**2, x**3, x**4, x**5, x**6, x**7, x**8, x**9, x**10, x**11, x**12, x**13, x**14, x**15, x**16, x**17, x**18, x**19]
    Q:
    [0.707106781186547, 1.22474487139159*x, 2.37170824512628*x**2 - 0.790569415042095, 4.67707173346743*x**3 - 2.80624304008046*x, 9.28077650307342*x**4 - 7.95495128834865*x**2 + 0.795495128834865, 18.4685120543046*x**5 - 20.5205689492274*x**3 + 4.39726477483447*x, 36.8085471137496*x**6 - 50.1934733369312*x**4 + 16.731157778977*x**2 - 0.796721798998903, 73.4290553655101*x**7 - 118.616166359671*x**5 + 53.9164392543961*x**3 - 5.9907154727108*x, 146.570997825597*x**8 - 273.599195941143*x**6 + 157.845689966067*x**4 - 28.6992163574728*x**2 + 0.797200454374528, 292.689266429782*x**9 - 619.812564204472*x**7 + 433.868794943332*x**5 - 111.248408959896*x**3 + 7.58511879272651*x, 584.646351835467*x**10 - 1384.68872802706*x**8 + 1140.33189366472*x**6 - 380.110631219486*x**4 + 43.8589189865218*x**2 - 0.79743489065461, 1168.08413179724*x**11 - 3059.26796431391*x**9 + 2898.25386102658*x**7 - 1193.39864870916*x**5 + 198.899774796064*x**3 - 9.17998960667973*x, 2334.1394542848*x**12 - 6697.96538974081*x**10 + 7176.39148793533*x**8 - 3525.24494077784*x**6 + 777.627560274535*x**4 - 62.2102048010054*x**2 + 0.797566727821615, 4664.8247961001*x**13 - 14554.2533510225*x**11 + 17401.824641446*x**9 - 9943.89978380936*x**7 + 2747.65651566431*x**5 - 323.25370725723*x**3 + 10.7751235584056*x, 9323.69774897047*x**14 - 31424.3145392121*x**12 + 41480.0950357262*x**10 - 27052.2357651977*x**8 + 9017.41186696691*x**6 - 1423.8018622434*x**4 + 83.7530497845978*x**2 - 0.797648080132224, 18637.0288570101*x**15 - 67478.8968030333*x**13 + 97469.5161890552*x**11 - 71477.643886015*x**9 + 27969.5121573176*x**7 - 5593.90225536597*x**5 + 490.69315947496*x**3 - 12.3704150610011*x, 37255.8196048632*x**16 - 144216.132304494*x**14 + 226270.239829557*x**12 - 184368.438761031*x**10 + 82965.8474262938*x**8 - 20200.3946203341*x**6 + 2404.8109348184*x**4 - 108.487824155802*x**2 + 0.797705620723317, 74479.6642568353*x**17 - 306946.405948985*x**15 + 519828.403493602*x**13 - 466052.839993619*x**11 + 237341.58597819*x**9 - 68354.3246558744*x**7 + 10401.73440399*x**5 - 707.59997666341*x**3 + 13.9657605808844*x, 148890.480388317*x**18 - 650867.820226831*x**16 + 1183404.11036741*x**14 - 1157964.1066223*x**12 + 658848.305301026*x**10 - 219618.867623267*x**8 + 40996.1779316701*x**6 - 3819.59915037231*x**4 + 136.418051302983*x**2 - 0.797797143973976, 297830.260121396*x**19 - 1376466.304985*x**17 + 2674292.07935306*x**15 - 2836385.47335974*x**13 + 1784186.56529593*x**11 - 676762.982328085*x**9 + 150392.04700999*x**7 - 18047.0137902493*x**5 + 980.807898993961*x**3 - 15.5680677297265*x]
    R:
    [[1.41421356e+00 0.00000000e+00 4.71404521e-01 0.00000000e+00
      2.82842712e-01 0.00000000e+00 2.02030509e-01 0.00000000e+00
      1.57134840e-01 0.00000000e+00 1.28564869e-01 0.00000000e+00
      1.08785659e-01 0.00000000e+00 9.42809042e-02 0.00000000e+00
      8.31890331e-02 0.00000000e+00 7.44322928e-02 0.00000000e+00]
     [0.00000000e+00 8.16496581e-01 0.00000000e+00 4.89897949e-01
      0.00000000e+00 3.49927106e-01 0.00000000e+00 2.72165527e-01
      0.00000000e+00 2.22680886e-01 0.00000000e+00 1.88422288e-01
      0.00000000e+00 1.63299316e-01 0.00000000e+00 1.44087632e-01
      0.00000000e+00 1.28920513e-01 0.00000000e+00 1.16642369e-01]
     [0.00000000e+00 0.00000000e+00 4.21637021e-01 0.00000000e+00
      3.61403161e-01 0.00000000e+00 3.01169301e-01 0.00000000e+00
      2.55537589e-01 0.00000000e+00 2.21138298e-01 0.00000000e+00
      1.94601702e-01 0.00000000e+00 1.73615244e-01 0.00000000e+00
      1.56645333e-01 0.00000000e+00 1.42659143e-01 0.00000000e+00]
     [0.00000000e+00 0.00000000e+00 0.00000000e+00 2.13808994e-01
      0.00000000e+00 2.37565548e-01 0.00000000e+00 2.26767114e-01
      0.00000000e+00 2.09323490e-01 0.00000000e+00 1.91879866e-01
      0.00000000e+00 1.76077995e-01 0.00000000e+00 1.62177100e-01
      0.00000000e+00 1.50041399e-01 0.00000000e+00 1.39440648e-01]
     [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      1.07749605e-01 0.00000000e+00 1.46931279e-01 0.00000000e+00
      1.58233685e-01 0.00000000e+00 1.58233685e-01 0.00000000e+00
      1.53579753e-01 0.00000000e+00 1.47113237e-01 0.00000000e+00
      1.40107845e-01 0.00000000e+00 1.33145965e-01 0.00000000e+00]
     [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 5.41462137e-02 0.00000000e+00 8.74669606e-02
      0.00000000e+00 1.04960353e-01 0.00000000e+00 1.13192537e-01
      0.00000000e+00 1.16171288e-01 0.00000000e+00 1.16171288e-01
      0.00000000e+00 1.14487646e-01 0.00000000e+00 1.11870786e-01]
     [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 2.71676031e-02 0.00000000e+00
      5.07128592e-02 0.00000000e+00 6.71199607e-02 0.00000000e+00
      7.77178492e-02 0.00000000e+00 8.41943367e-02 0.00000000e+00
      8.78549600e-02 0.00000000e+00 8.96120592e-02 0.00000000e+00]
     [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 1.36185873e-02
      0.00000000e+00 2.88393613e-02 0.00000000e+00 4.17411809e-02
      0.00000000e+00 5.16795572e-02 0.00000000e+00 5.89821034e-02
      0.00000000e+00 6.41725285e-02 0.00000000e+00 6.77376689e-02]
     [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      6.82263214e-03 0.00000000e+00 1.61588656e-02 0.00000000e+00
      2.53925031e-02 0.00000000e+00 3.34886635e-02 0.00000000e+00
      4.01863962e-02 0.00000000e+00 4.55445823e-02 0.00000000e+00]
     [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 3.41659266e-03 0.00000000e+00 8.94821888e-03
      0.00000000e+00 1.51730668e-02 0.00000000e+00 2.12422935e-02
      0.00000000e+00 2.67495548e-02 0.00000000e+00 3.15460267e-02]
     [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 1.71043571e-03 0.00000000e+00
      4.90820683e-03 0.00000000e+00 8.93293643e-03 0.00000000e+00
      1.32339799e-02 0.00000000e+00 1.74551631e-02 0.00000000e+00]
     [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 8.56102718e-04
      0.00000000e+00 2.67104048e-03 0.00000000e+00 5.19368982e-03
      0.00000000e+00 8.11887144e-03 0.00000000e+00 1.11961856e-02]
     [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      4.28423417e-04 0.00000000e+00 1.44394559e-03 0.00000000e+00
      2.98747362e-03 0.00000000e+00 4.91487596e-03 0.00000000e+00]
     [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 2.14370323e-04 0.00000000e+00 7.76168403e-04
      0.00000000e+00 1.70256298e-03 0.00000000e+00 2.94079059e-03]
     [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 1.07253584e-04 0.00000000e+00
      4.15175326e-04 0.00000000e+00 9.62451912e-04 0.00000000e+00]
     [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 5.36566213e-05
      0.00000000e+00 2.21130254e-04 0.00000000e+00 5.40189792e-04]
     [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      2.68414441e-05 0.00000000e+00 1.17336126e-04 0.00000000e+00]
     [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 1.34264837e-05 0.00000000e+00 6.20524672e-05]
     [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 6.71634612e-06 0.00000000e+00]
     [0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00
      0.00000000e+00 0.00000000e+00 0.00000000e+00 3.35761719e-06]]
    --------------------Functions:
    [1, sin(2*pi*x), sin(4*pi*x), sin(6*pi*x), sin(8*pi*x), sin(10*pi*x), sin(12*pi*x), sin(14*pi*x), sin(16*pi*x), sin(18*pi*x), sin(20*pi*x), sin(22*pi*x), sin(24*pi*x), sin(26*pi*x), sin(28*pi*x), sin(30*pi*x), sin(32*pi*x), sin(34*pi*x), sin(36*pi*x), sin(38*pi*x)]
    Q:
    [0.707106781186547, 1.0*sin(2*pi*x), 1.0*sin(4*pi*x), 1.0*sin(6*pi*x), 1.0*sin(8*pi*x), 1.0*sin(10*pi*x), 1.0*sin(12*pi*x), 1.0*sin(14*pi*x), 1.0*sin(16*pi*x), 1.0*sin(18*pi*x), 1.0*sin(20*pi*x), 1.0*sin(22*pi*x), 1.0*sin(24*pi*x), 1.0*sin(26*pi*x), 1.0*sin(28*pi*x), 1.0*sin(30*pi*x), 1.0*sin(32*pi*x), 1.0*sin(34*pi*x), 1.0*sin(36*pi*x), 1.0*sin(38*pi*x)]
    R:
    [[1.41421356 0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.        ]
     [0.         1.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.        ]
     [0.         0.         1.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.        ]
     [0.         0.         0.         1.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.        ]
     [0.         0.         0.         0.         1.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.        ]
     [0.         0.         0.         0.         0.         1.
      0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.        ]
     [0.         0.         0.         0.         0.         0.
      1.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.        ]
     [0.         0.         0.         0.         0.         0.
      0.         1.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.        ]
     [0.         0.         0.         0.         0.         0.
      0.         0.         1.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.        ]
     [0.         0.         0.         0.         0.         0.
      0.         0.         0.         1.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.        ]
     [0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         1.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.        ]
     [0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         1.
      0.         0.         0.         0.         0.         0.
      0.         0.        ]
     [0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      1.         0.         0.         0.         0.         0.
      0.         0.        ]
     [0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         1.         0.         0.         0.         0.
      0.         0.        ]
     [0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.         1.         0.         0.         0.
      0.         0.        ]
     [0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.         0.         1.         0.         0.
      0.         0.        ]
     [0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         1.         0.
      0.         0.        ]
     [0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         1.
      0.         0.        ]
     [0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      1.         0.        ]
     [0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         0.         0.         0.         0.         0.
      0.         1.        ]]
    --------------------Functions:
    [1, x, x**2, x**3, x**4, x**5, x**6, x**7, x**8, x**9, x**10, x**11, x**12, x**13, x**14, x**15, x**16, x**17, x**18, x**19, x**20, x**21, x**22, x**23, x**24, x**25, x**26, x**27, x**28, x**29, x**30, x**31, x**32, x**33, x**34, x**35, x**36, x**37, x**38, x**39, x**40, x**41, x**42, x**43, x**44, x**45, x**46, x**47, x**48, x**49, x**50, x**51, x**52, x**53, x**54, x**55, x**56, x**57, x**58, x**59, x**60, x**61, x**62, x**63, x**64, x**65, x**66, x**67, x**68, x**69, x**70, x**71, x**72, x**73, x**74, x**75, x**76, x**77, x**78, x**79, x**80, x**81, x**82, x**83, x**84, x**85, x**86, x**87, x**88, x**89, x**90, x**91, x**92, x**93, x**94, x**95, x**96, x**97, x**98, x**99]
    Warning: negative norm2=-0.000000 at i=24!
    Couldn't compute R!!!:
    --------------------Functions:
    [1, sin(2*pi*x), sin(4*pi*x), sin(6*pi*x), sin(8*pi*x), sin(10*pi*x), sin(12*pi*x), sin(14*pi*x), sin(16*pi*x), sin(18*pi*x), sin(20*pi*x), sin(22*pi*x), sin(24*pi*x), sin(26*pi*x), sin(28*pi*x), sin(30*pi*x), sin(32*pi*x), sin(34*pi*x), sin(36*pi*x), sin(38*pi*x), sin(40*pi*x), sin(42*pi*x), sin(44*pi*x), sin(46*pi*x), sin(48*pi*x), sin(50*pi*x), sin(52*pi*x), sin(54*pi*x), sin(56*pi*x), sin(58*pi*x), sin(60*pi*x), sin(62*pi*x), sin(64*pi*x), sin(66*pi*x), sin(68*pi*x), sin(70*pi*x), sin(72*pi*x), sin(74*pi*x), sin(76*pi*x), sin(78*pi*x), sin(80*pi*x), sin(82*pi*x), sin(84*pi*x), sin(86*pi*x), sin(88*pi*x), sin(90*pi*x), sin(92*pi*x), sin(94*pi*x), sin(96*pi*x), sin(98*pi*x), sin(100*pi*x), sin(102*pi*x), sin(104*pi*x), sin(106*pi*x), sin(108*pi*x), sin(110*pi*x), sin(112*pi*x), sin(114*pi*x), sin(116*pi*x), sin(118*pi*x), sin(120*pi*x), sin(122*pi*x), sin(124*pi*x), sin(126*pi*x), sin(128*pi*x), sin(130*pi*x), sin(132*pi*x), sin(134*pi*x), sin(136*pi*x), sin(138*pi*x), sin(140*pi*x), sin(142*pi*x), sin(144*pi*x), sin(146*pi*x), sin(148*pi*x), sin(150*pi*x), sin(152*pi*x), sin(154*pi*x), sin(156*pi*x), sin(158*pi*x), sin(160*pi*x), sin(162*pi*x), sin(164*pi*x), sin(166*pi*x), sin(168*pi*x), sin(170*pi*x), sin(172*pi*x), sin(174*pi*x), sin(176*pi*x), sin(178*pi*x), sin(180*pi*x), sin(182*pi*x), sin(184*pi*x), sin(186*pi*x), sin(188*pi*x), sin(190*pi*x), sin(192*pi*x), sin(194*pi*x), sin(196*pi*x), sin(198*pi*x)]
    Q:
    [0.707106781186547, 1.0*sin(2*pi*x), 1.0*sin(4*pi*x), 1.0*sin(6*pi*x), 1.0*sin(8*pi*x), 1.0*sin(10*pi*x), 1.0*sin(12*pi*x), 1.0*sin(14*pi*x), 1.0*sin(16*pi*x), 1.0*sin(18*pi*x), 1.0*sin(20*pi*x), 1.0*sin(22*pi*x), 1.0*sin(24*pi*x), 1.0*sin(26*pi*x), 1.0*sin(28*pi*x), 1.0*sin(30*pi*x), 1.0*sin(32*pi*x), 1.0*sin(34*pi*x), 1.0*sin(36*pi*x), 1.0*sin(38*pi*x), 1.0*sin(40*pi*x), 1.0*sin(42*pi*x), 1.0*sin(44*pi*x), 1.0*sin(46*pi*x), 1.0*sin(48*pi*x), 1.0*sin(50*pi*x), 1.0*sin(52*pi*x), 1.0*sin(54*pi*x), 1.0*sin(56*pi*x), 1.0*sin(58*pi*x), 1.0*sin(60*pi*x), 1.0*sin(62*pi*x), 1.0*sin(64*pi*x), 1.0*sin(66*pi*x), 1.0*sin(68*pi*x), 1.0*sin(70*pi*x), 1.0*sin(72*pi*x), 1.0*sin(74*pi*x), 1.0*sin(76*pi*x), 1.0*sin(78*pi*x), 1.0*sin(80*pi*x), 1.0*sin(82*pi*x), 1.0*sin(84*pi*x), 1.0*sin(86*pi*x), 1.0*sin(88*pi*x), 1.0*sin(90*pi*x), 1.0*sin(92*pi*x), 1.0*sin(94*pi*x), 1.0*sin(96*pi*x), 1.0*sin(98*pi*x), 1.0*sin(100*pi*x), 1.0*sin(102*pi*x), 1.0*sin(104*pi*x), 1.0*sin(106*pi*x), 1.0*sin(108*pi*x), 1.0*sin(110*pi*x), 1.0*sin(112*pi*x), 1.0*sin(114*pi*x), 1.0*sin(116*pi*x), 1.0*sin(118*pi*x), 1.0*sin(120*pi*x), 1.0*sin(122*pi*x), 1.0*sin(124*pi*x), 1.0*sin(126*pi*x), 1.0*sin(128*pi*x), 1.0*sin(130*pi*x), 1.0*sin(132*pi*x), 1.0*sin(134*pi*x), 1.0*sin(136*pi*x), 1.0*sin(138*pi*x), 1.0*sin(140*pi*x), 1.0*sin(142*pi*x), 1.0*sin(144*pi*x), 1.0*sin(146*pi*x), 1.0*sin(148*pi*x), 1.0*sin(150*pi*x), 1.0*sin(152*pi*x), 1.0*sin(154*pi*x), 1.0*sin(156*pi*x), 1.0*sin(158*pi*x), 1.0*sin(160*pi*x), 1.0*sin(162*pi*x), 1.0*sin(164*pi*x), 1.0*sin(166*pi*x), 1.0*sin(168*pi*x), 1.0*sin(170*pi*x), 1.0*sin(172*pi*x), 1.0*sin(174*pi*x), 1.0*sin(176*pi*x), 1.0*sin(178*pi*x), 1.0*sin(180*pi*x), 1.0*sin(182*pi*x), 1.0*sin(184*pi*x), 1.0*sin(186*pi*x), 1.0*sin(188*pi*x), 1.0*sin(190*pi*x), 1.0*sin(192*pi*x), 1.0*sin(194*pi*x), 1.0*sin(196*pi*x), 1.0*sin(198*pi*x)]
    R:
    [[1.41421356 0.         0.         ... 0.         0.         0.        ]
     [0.         1.         0.         ... 0.         0.         0.        ]
     [0.         0.         1.         ... 0.         0.         0.        ]
     ...
     [0.         0.         0.         ... 1.         0.         0.        ]
     [0.         0.         0.         ... 0.         1.         0.        ]
     [0.         0.         0.         ... 0.         0.         1.        ]]

We can see that the functions of the second item are ortogonal, so the $QR$ decomposition gives the indentity (besides the first function $y(x)=1$ that has to be normalized).

Around $i=25$ the function coeficients become too small to handle. The norm (inner product with itself) of the functions after substracting the projections becomes small, and negative.

```python
FUNCS = [part_a_funcs(100),part_b_funcs(100)]
for funcs in FUNCS:
    # Print Original matrix
    print("-"*20+"Functions:")
    print(funcs)
    # Perform QR decomposition using generic G-S
    r = generic_gs(funcs,prod=symbolic_inner_product)
    if r is None:
        print("Couldn't compute R!!!:")
    else:
        # Print Q
        print("Q:")
        print(funcs)
        # Print R
        print("R:")
        print(r)
```

    --------------------Functions:
    [1, x, x**2, x**3, x**4, x**5, x**6, x**7, x**8, x**9, x**10, x**11, x**12, x**13, x**14, x**15, x**16, x**17, x**18, x**19, x**20, x**21, x**22, x**23, x**24, x**25, x**26, x**27, x**28, x**29, x**30, x**31, x**32, x**33, x**34, x**35, x**36, x**37, x**38, x**39, x**40, x**41, x**42, x**43, x**44, x**45, x**46, x**47, x**48, x**49, x**50, x**51, x**52, x**53, x**54, x**55, x**56, x**57, x**58, x**59, x**60, x**61, x**62, x**63, x**64, x**65, x**66, x**67, x**68, x**69, x**70, x**71, x**72, x**73, x**74, x**75, x**76, x**77, x**78, x**79, x**80, x**81, x**82, x**83, x**84, x**85, x**86, x**87, x**88, x**89, x**90, x**91, x**92, x**93, x**94, x**95, x**96, x**97, x**98, x**99]
    Warning: negative norm2=-0.000000 at i=24!
    Couldn't compute R!!!:
    --------------------Functions:
    [1, sin(2*pi*x), sin(4*pi*x), sin(6*pi*x), sin(8*pi*x), sin(10*pi*x), sin(12*pi*x), sin(14*pi*x), sin(16*pi*x), sin(18*pi*x), sin(20*pi*x), sin(22*pi*x), sin(24*pi*x), sin(26*pi*x), sin(28*pi*x), sin(30*pi*x), sin(32*pi*x), sin(34*pi*x), sin(36*pi*x), sin(38*pi*x), sin(40*pi*x), sin(42*pi*x), sin(44*pi*x), sin(46*pi*x), sin(48*pi*x), sin(50*pi*x), sin(52*pi*x), sin(54*pi*x), sin(56*pi*x), sin(58*pi*x), sin(60*pi*x), sin(62*pi*x), sin(64*pi*x), sin(66*pi*x), sin(68*pi*x), sin(70*pi*x), sin(72*pi*x), sin(74*pi*x), sin(76*pi*x), sin(78*pi*x), sin(80*pi*x), sin(82*pi*x), sin(84*pi*x), sin(86*pi*x), sin(88*pi*x), sin(90*pi*x), sin(92*pi*x), sin(94*pi*x), sin(96*pi*x), sin(98*pi*x), sin(100*pi*x), sin(102*pi*x), sin(104*pi*x), sin(106*pi*x), sin(108*pi*x), sin(110*pi*x), sin(112*pi*x), sin(114*pi*x), sin(116*pi*x), sin(118*pi*x), sin(120*pi*x), sin(122*pi*x), sin(124*pi*x), sin(126*pi*x), sin(128*pi*x), sin(130*pi*x), sin(132*pi*x), sin(134*pi*x), sin(136*pi*x), sin(138*pi*x), sin(140*pi*x), sin(142*pi*x), sin(144*pi*x), sin(146*pi*x), sin(148*pi*x), sin(150*pi*x), sin(152*pi*x), sin(154*pi*x), sin(156*pi*x), sin(158*pi*x), sin(160*pi*x), sin(162*pi*x), sin(164*pi*x), sin(166*pi*x), sin(168*pi*x), sin(170*pi*x), sin(172*pi*x), sin(174*pi*x), sin(176*pi*x), sin(178*pi*x), sin(180*pi*x), sin(182*pi*x), sin(184*pi*x), sin(186*pi*x), sin(188*pi*x), sin(190*pi*x), sin(192*pi*x), sin(194*pi*x), sin(196*pi*x), sin(198*pi*x)]
    Q:
    [0.707106781186547, 1.0*sin(2*pi*x), 1.0*sin(4*pi*x), 1.0*sin(6*pi*x), 1.0*sin(8*pi*x), 1.0*sin(10*pi*x), 1.0*sin(12*pi*x), 1.0*sin(14*pi*x), 1.0*sin(16*pi*x), 1.0*sin(18*pi*x), 1.0*sin(20*pi*x), 1.0*sin(22*pi*x), 1.0*sin(24*pi*x), 1.0*sin(26*pi*x), 1.0*sin(28*pi*x), 1.0*sin(30*pi*x), 1.0*sin(32*pi*x), 1.0*sin(34*pi*x), 1.0*sin(36*pi*x), 1.0*sin(38*pi*x), 1.0*sin(40*pi*x), 1.0*sin(42*pi*x), 1.0*sin(44*pi*x), 1.0*sin(46*pi*x), 1.0*sin(48*pi*x), 1.0*sin(50*pi*x), 1.0*sin(52*pi*x), 1.0*sin(54*pi*x), 1.0*sin(56*pi*x), 1.0*sin(58*pi*x), 1.0*sin(60*pi*x), 1.0*sin(62*pi*x), 1.0*sin(64*pi*x), 1.0*sin(66*pi*x), 1.0*sin(68*pi*x), 1.0*sin(70*pi*x), 1.0*sin(72*pi*x), 1.0*sin(74*pi*x), 1.0*sin(76*pi*x), 1.0*sin(78*pi*x), 1.0*sin(80*pi*x), 1.0*sin(82*pi*x), 1.0*sin(84*pi*x), 1.0*sin(86*pi*x), 1.0*sin(88*pi*x), 1.0*sin(90*pi*x), 1.0*sin(92*pi*x), 1.0*sin(94*pi*x), 1.0*sin(96*pi*x), 1.0*sin(98*pi*x), 1.0*sin(100*pi*x), 1.0*sin(102*pi*x), 1.0*sin(104*pi*x), 1.0*sin(106*pi*x), 1.0*sin(108*pi*x), 1.0*sin(110*pi*x), 1.0*sin(112*pi*x), 1.0*sin(114*pi*x), 1.0*sin(116*pi*x), 1.0*sin(118*pi*x), 1.0*sin(120*pi*x), 1.0*sin(122*pi*x), 1.0*sin(124*pi*x), 1.0*sin(126*pi*x), 1.0*sin(128*pi*x), 1.0*sin(130*pi*x), 1.0*sin(132*pi*x), 1.0*sin(134*pi*x), 1.0*sin(136*pi*x), 1.0*sin(138*pi*x), 1.0*sin(140*pi*x), 1.0*sin(142*pi*x), 1.0*sin(144*pi*x), 1.0*sin(146*pi*x), 1.0*sin(148*pi*x), 1.0*sin(150*pi*x), 1.0*sin(152*pi*x), 1.0*sin(154*pi*x), 1.0*sin(156*pi*x), 1.0*sin(158*pi*x), 1.0*sin(160*pi*x), 1.0*sin(162*pi*x), 1.0*sin(164*pi*x), 1.0*sin(166*pi*x), 1.0*sin(168*pi*x), 1.0*sin(170*pi*x), 1.0*sin(172*pi*x), 1.0*sin(174*pi*x), 1.0*sin(176*pi*x), 1.0*sin(178*pi*x), 1.0*sin(180*pi*x), 1.0*sin(182*pi*x), 1.0*sin(184*pi*x), 1.0*sin(186*pi*x), 1.0*sin(188*pi*x), 1.0*sin(190*pi*x), 1.0*sin(192*pi*x), 1.0*sin(194*pi*x), 1.0*sin(196*pi*x), 1.0*sin(198*pi*x)]
    R:
    [[1.41421356 0.         0.         ... 0.         0.         0.        ]
     [0.         1.         0.         ... 0.         0.         0.        ]
     [0.         0.         1.         ... 0.         0.         0.        ]
     ...
     [0.         0.         0.         ... 1.         0.         0.        ]
     [0.         0.         0.         ... 0.         1.         0.        ]
     [0.         0.         0.         ... 0.         0.         1.        ]]

---
To do it numerically, let's define a polynomial
$$
p(x) = \sum_{i=0}^{n-1} p_i x^i
$$
as the array of the $p_i$'s.

Then the multiplication becomes:
$$
p(x)q(x) =  \sum_{i=0}^{2n-2} \left( \sum_{k=0}^{i} p_k q_{i-k} \right) x^{i} \,,
$$
then the inner product becomes:
\begin{align*}
\int_{-1}^{1} p(x)q(x) \, dx &= \sum_{i=0}^{2n-2} \left( \sum_{k=0}^{i} p_k q_{i-k} \right) \frac{x^{i+1}}{i+1} |_{x=-1}^{1}
\\ &= \sum_{i=0}^{2n-2} [i \, \text{mod} \, 2= 0] \left( \sum_{k=0}^{i} p_k q_{i-k} \right) \frac{2}{i+1}
\end{align*}

```python
@jit(nopython=True)
def poly_mult(a,b):
    assert(len(a)==len(b))
    n = len(a)
    total = 0
    for i in range(0,2*n-1,2):
        term = 0
        for k in range(0,i+1):
            if k>=0 and i-k>=0 and k<n and i-k<n:
                term += a[k]*b[i-k]
        term *= 2.0/(i+1.0)
        total += term
    return total
```

```python
def poly_print(poly):
    stri = []
    if len(poly)>0 and poly[0] != 0: stri.append("%.3f"%poly[0])
    if len(poly)>1 and poly[1] != 0: stri.append("%.3fx"%poly[1])
    for i in range(2,len(poly)):
        if poly[i]!=0:
            stri.append("%+.3fx%d"%(poly[i],i))
    if len(stri)==0: return "0"
    return " ".join(stri)

def poly_matrix_print(polys,limit=10):
    print("[")
    if len(polys)>2*limit:
        for poly in polys[:limit]:
            print("  "+poly_print(poly)+" |")
        print("  ...")
        for poly in polys[-limit:]:
            print("  "+poly_print(poly)+" |")
    else:
        for poly in polys:
            print("  "+poly_print(poly)+" |")
    print("]")
```

```python
poly_mult([1,2,3],[2,5,1])
```

    16.53333333333333

```python
for N in (5,10,100):
    # Print Original matrix
    polys = np.eye(N)
    print("-"*20+" Original (N=%d):"%N)
    poly_matrix_print(polys)
    # Perform QR decomposition using generic G-S
    r = generic_gs(polys,prod=poly_mult)
    # Print Q
    print("Q:")
    poly_matrix_print(polys)
    # Assert that Q is orthonormal
    for i in range(N):
        for j in range(N):
            if i==j:
                assert(np.abs(poly_mult(polys[i],polys[j])-1)<1e-5)
            else:
                assert(np.abs(poly_mult(polys[i],polys[j]))<1e-5)
    # Print R
    print("R:")
    print(r)
```

We can see that the numerical method fails for $N=100$. After a closer inspection, this was because, after substracting the projection with the previous functions, the remaining polynomial had very small coefficients, around $i=25$ too.

```python

```