from sympy import *

class ActivationRegionPlotter:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.h = self.euclideanHeuristic
        self.rows, self.cols = 100, 100

    def setGoal(self, goal):
        self.goal = goal

    def setGridSize( self, rows, cols ):
        self.rows, self.cols = rows, cols

    def circle(self, (x0, y0), radius=1):
        return ( (self.x-x0)**2 + (self.y-y0)**2 )**S.Half < radius

    def euclideanHeuristic(self, currPoint, goalPoint):
        return ((currPoint[0] - goalPoint[0])**2 + (currPoint[1] -
            goalPoint[1])**2)**S.Half

    def diagonalHeuristic(self, currPoint, goalPoint):
        dr = abs(currPoint[0] - goalPoint[0])
        dc = abs(currPoint[1] - goalPoint[1])
        D = 1
        D2 = 1.5
        return D * (dr + dc) + (D2 - 2 * D) * min(dr, dc)

    def setHeuristic(heurType):
        if heurType == 0:
            self.h = self.euclideanHeuristic
        else:
            self.h = self.diagonalHeuristic

    def getActivationRegion(self, island, epsilon ):
        heurToGoal = self.h( (self.x, self.y), (self.goal) )
        heurViaIsland = self.h( (self.x, self.y), island ) + self.h( island,
                self.goal )
        return heurViaIsland < epsilon * heurToGoal

    def plotRegion( self, island, epsilon, color, show_=False ):
        region = plot_implicit( self.getActivationRegion( island, epsilon ), (self.x,
                       0, self.cols), (self.y, 0, self.cols), show=show_,
                       line_color=color )
        return region

    def plotRegions( self, islands, epsilons, colors ):
        regions = []
        for island, epsilon, color in zip( islands, epsilons, colors ):
            regions.append( self.plotRegion( island, epsilon, color) )
        return regions

    def plotPoint( self, point, radius=1, color='r' ):
        return plot_implicit( self.circle( point, radius), (self.x, 0,
            self.cols), (self.y, 0, self.rows), line_color=color, show=False)

#if __name__ == "main":
def main():
    x, y = symbols( 'x, y' )
    plotter = ActivationRegionPlotter( x, y )

    print( "Plotting" )
    plotter.setGoal( (50, 5) )
    island = (80, 50)
    islands = [ (80, 50), (20, 50) ]
    epsilons = [1.5, 1.5]
    colors = ['r', 'b']
    #plotter.plotRegion( island, 1.2 )
    regions = plotter.plotRegions( islands, epsilons, colors )
    goalPoint = plotter.plotPoint( plotter.goal )
    finalRegion = regions[0]
    for region in regions + [ goalPoint, ]:
        finalRegion.extend(region)
    finalRegion.show()

main()


import numpy as np

def get_deviation_for_dfa(signal, window_length):
    """Given a signal, compute the trend value for one window length, as per
    https://link.springer.com/article/10.1186/1475-925X-6-23
    In order to get the overall DFA (detrended fluctuation analysis),
    compute this for a variety of window lengths, then plot that on a
    log-log graph, and get the slope.

    Args:
        signal (np.array, [T, ]): waveform
        window_length (int > 0): L in the paper linked above. Length of windows for trend.

    Returns:
        float: average rmse for fitting lines on chunks of window lengths on the
            cumulative sums of this signal.
    """
    rmse = 0
    # Step 1, integrate time series (cumulative sum)
    Y = np.cumsum(signal)
    # Step 2, separate into chunks of length window_length +/- 1.
    chunks = np.array_split(Y, np.ceil(Y.shape[0] / window_length))
    # Step 3, fit a line for all of these chunks.
    for chunk in chunks:
        slope, offset = np.polyfit(np.arange(chunk.shape[0]), chunk, 1)
        rmse += np.sqrt(
            (((offset + slope * np.arange(chunk.shape[0])) - chunk) ** 2).sum()
        )
    rmse /= len(chunks)
    return rmse

def get_dfa(signal, window_lengths):
    """Given a signal, compute the DFA (detrended fluctuation analysis)
    as per https://link.springer.com/article/10.1186/1475-925X-6-23
    See paper equations (13) to (16) for more information.
    """
    fl_list = []
    for length in window_lengths:
        fl_list.append(get_deviation_for_dfa(signal, length))

    log_l = [np.log(length) for length in window_lengths]
    log_fl = [np.log(fl) for fl in fl_list]

    # Fit a line.
    slope, _ = np.polyfit(log_l, log_fl, 1)
    return slope


import numpy as np
from .metrics import r2_score

class LinearRegression:
    def __init__(self):
        self.coef_=None
        self.interception_=None
        self._theta=None

    def fit_normal(self,X_train,y_train):
        assert X_train.shape[0]==y_train.shape[0],\
            'the size of X_train must be equal to the sizeof y_train'
        X_b=np.hstack([np.ones((len(X_train),1)),X_train])
        self._theta=np.linalg.inv(X_b.T.dot(X_b)).dot((X_b.T)).dot(y_train)
        self.interception_=self._theta[0]
        self.coef_=self._theta[1:]
        return self

    def fit_gd(self,X_train,y_train,eta=0.01,n_iters=1e4):
        assert X_train.shape[0]==y_train.shape[0],\
            'the size of X_train must be equal to the size of y_train'
        def J(theta, x_b, y):
            try:
                return np.sum((y - x_b.dot(theta)) ** 2) / len(x_b)
            except:
                return float('inf')

        def dJ(theta, x_b, y):
            # res = np.empty(len(theta))
            # res[0] = np.sum(x_b.dot(theta) - y)
            # for i in range(1, len(theta)):
            #     res[i] = (x_b.dot(theta) - y).dot(x_b[:, i])
            # return res * 2 / len(x_b)
            return x_b.T.dot(x_b.dot(theta)-y)*2/len(x_b)
        def gradient_descent(x_b, y, initial_theta, eta, n_iters=1e4, epsilon=1e-8):
            theta = initial_theta
            i_iter = 0
            while i_iter < n_iters:
                gradient = dJ(theta, x_b, y)
                last_theta = theta
                theta = theta - eta * gradient
                if (abs(J(theta, x_b, y) - J(last_theta, x_b, y)) < epsilon):
                    break
                i_iter += 1
            return theta
        x_b=np.hstack([np.ones((len(X_train),1)),X_train])
        initial_theta=np.zeros(x_b.shape[1])
        self._theta=gradient_descent(x_b,y_train,initial_theta,eta,n_iters=1e4, epsilon=1e-8)
        self.interception_=self._theta[0]
        self.coef_=self._theta[1:]
        return self

    def fit_sgd(self,X_train,y_train,n_iters=5,t0=5,t1=50):
        assert X_train.shape[0]==y_train.shape[0],\
            'the size of X_train must be equal to the size of y_train'
        assert n_iters>=1
        def dJ_sgd(theta,x_b_i,y_i):
            return x_b_i*(x_b_i.dot(theta)-y_i)*2.
        def sgd(x_b,y,initial_theta,n_iters,t0=5,t1=50):
            def learning_rate(t):
                return t0/(t+t1)

            theta=initial_theta
            m=len(x_b)
            for cur_iter in range(n_iters):
                indexs=np.random.permutation(m)
                x_b_new=x_b[indexs]
                y_new=y[indexs]
                for i in range(m):
                    gradient=dJ_sgd(theta,x_b_new[i],y_new[i])
                    theta=theta-learning_rate(cur_iter*m+i)*gradient

            return theta
        x_b=np.hstack([np.ones((len(X_train),1)),X_train])
        initial_theta=np.random.randint(x_b.shape[1])
        self._theta=sgd(x_b,y_train,initial_theta,n_iters,t0,t1)
        self.interception_=self._theta[0]
        self.coef_=self._theta[1:]
        return self

    def predict(self,X_predict):
        assert self.interception_ is not None and self.coef_ is not None,\
            'must fit before predict'
        assert X_predict.shape[1]==len(self.coef_),\
            'the feature number of X_predict must be equal to X_train'
        X_b = np.hstack([np.ones((len(X_predict), 1)), X_predict])
        return X_b.dot(self._theta)

    def score(self,X_test,y_test):
        y_predict=self.predict(X_test)
        return r2_score(y_test,y_predict)

    def __repr__(self):
        return 'LinearRegression()'


"""
    Name: lagrange.py
    Goal: interpolation of newton
    Author: HOUNSI Madouvi antoine-sebastien
    Date: 02/03/2022
"""
import numpy as np
import matplotlib.pyplot as plt
import sys
from math import pow
from linearEq.utils.gaussForVal import gauss
from os.path import dirname, join
from interpolation.polynom import Polynom

class MoindreCarre:

    def __init__(self, file, deg):
        self.file = file
        try:
            sys.stdin = open(join(dirname(__file__), self.file))
            self.X = [float(i) for i in sys.stdin.readline().split()]
            self.Y = [float(i) for i in sys.stdin.readline().split()]

            if len(self.X) != len(self.Y):
                print("Vos tableaux ne sont pas de meme taille")
            else:
                self.X = [k for k in self.X]
                self.Y = [k for k in self.Y]
                self.dim = len(self.X)
                Xval = np.arange(-2, 2, 0.1)

                self.polyM2 = self.moindreCarre(2)
                self.polyM3 = self.moindreCarre(3)
                self.polyM4 = self.moindreCarre(4)
                self.polyM5 = self.moindreCarre(5)

                # print the graph
                plt.plot(Xval, self.givenFunc(Xval, "(x**3 - 1)/(x**2 + 1)"), label='Courbe', c='black')
                plt.plot(Xval, self.calc(Xval, self.polyM2), label='Courbe moindreCarre 2')
                plt.plot(Xval, self.calc(Xval, self.polyM3), label='Courbe moindreCarre 3')
                plt.plot(Xval, self.calc(Xval, self.polyM4), label='Courbe moindreCarre 4')
                plt.plot(Xval, self.calc(Xval, self.polyM5), label='Courbe moindreCarre 5')
                plt.scatter(self.X, self.Y, c='red', label='Points Données')
                plt.title("Interpolation de Moindre carré:\nPx1 = {}\nPx2 = {}\nPx3 = {}\nPx4 = {}".format(self.polyM2, self.polyM3, self.polyM4, self.polyM5))
                plt.xlabel("X --->")
                plt.ylabel("Y --->")
                plt.legend()
                plt.show()
        except ValueError:
            print("Valeurs mal définies")
        except TypeError:
            print("None Type n'est pas un type primitif")

    def calc(self, x, poly):
        return eval(poly)

    def givenFunc(self, x, poly):
        return eval(poly)

    def moindreCarre(self, deg):
        deg = deg
        matrix = np.zeros((deg + 1, deg + 1))
        vect = list()
        maxDeg = 2 * deg

        # matrix
        for i in range(maxDeg, deg - 1, -1):
            for j in range(deg + 1):
                temp = [pow(self.X[k], i - j) for k in range(self.dim)]
                matrix[maxDeg - i][j] = sum(temp)

        # vector
        for i in range(deg, -1, -1):
            temp = [pow(self.X[k], i) * self.Y[k] for k in range(self.dim)]
            vect.append(sum(temp))
        Coefs = gauss(matrix, vect).showResult()
        return Polynom().build(Coefs)


from sympy import symbols, solve, pprint, Derivative
import math

dec_places = 4
value = .6

x = symbols( 'x' )

fX = ( x ** 2 ) / ( 2 * ( 1 - x ) )
pprint( fX )

dX = Derivative( fX, x ).doit()
pprint( dX )

res = dX.subs( { x: value } )

# (Simplify your answer. Type an integer or decimal rounded to four decimal places as needed.)
print( 'The rate of change for {0} at {1} is {2}'.format( fX, value, round( res, dec_places ) ) )


from math import exp, log, pi

import sympy

import identinum

def test_rational():
    sols = identinum.identify(3.0 / 7.0)
    assert sols[0] == sympy.Rational(3, 7)

def test_root2():
    sols = identinum.identify((3.0 / 7.0) ** 0.5)
    assert sols[0] == sympy.sqrt(sympy.Rational(3, 7))

def test_root3():
    sols = identinum.identify((3.0 / 7.0) ** (1.0 / 3.0))
    assert sols[0] == sympy.Rational(3, 7) ** sympy.Rational(1, 3)

def test_root34():
    sols = identinum.identify(3.0 ** 0.5 / 2.0)
    assert sols[0] == sympy.sqrt(sympy.Rational(3, 4))

def test_pi():
    sols = identinum.identify(3.0 / 4.0 * pi)
    assert sols[0] == sympy.pi * sympy.Rational(3, 4)

def test_sqrt_pi_2():
    sols = identinum.identify((0.5 * pi) ** 0.5)
    assert sols[0] == sympy.sqrt(sympy.pi * sympy.Rational(1, 2))

def test_exp_2_pi():
    sols = identinum.identify(exp(2 * pi))
    assert sols[0] == sympy.exp(sympy.pi * 2)

def test_logn_0_5():
    sols = identinum.identify(log(0.5))
    assert sols[0] == sympy.log(sympy.Rational(1, 2))

def test_pi2():
    sols = identinum.identify(pi ** 2 / 4.0)
    assert sols[0] == sympy.pi ** 2 * sympy.Rational(1, 4)

def test_poly():
    sols = identinum.findpoly(0.17852201277, tol=1.0e-10)
    assert sols == [-79852, -12268, 4735]

if __name__ == "__main__":
    test_rational()


"""
Roughly speaking, a distribution function is a function f(x) that describes the
probability that a random variable has a value that is below x. In practical
terms, the distribution describes the spread of the random data over a range.
This module illustrates how to generate data following the normal distribution.
"""
import numpy as np
import matplotlib.pyplot as plt

from numpy.random import default_rng

def normal_dist_curve(x):
    return 10000*np.exp(-0.5*((x-mu)/sigma)**2)/(sigma*np.sqrt(2*np.pi))

rng = default_rng(12345)

mu = 5.0 # mean value
sigma = 3.0 # standard deviation
rands = rng.normal(loc=mu, scale=sigma, size=10000)

x_range = np.linspace(-5, 15)
y = normal_dist_curve(x_range)

fig, ax = plt.subplots()

ax.hist(rands, bins=20)
ax.plot(x_range, y, "k--")
ax.set_title("Histogram of normally distributed data")
ax.set_xlabel("Value")
ax.set_ylabel("Density")

plt.show()


#############################################################################
#                                                                           #
#   Copyright 2020 Daniel Bryce Beetham                                     #
#                                                                           #
# Licensed under the Apache License, Version 2.0 (the "License");           #
# you may not use this file except in compliance with the License.          #
# You may obtain a copy of the License at                                   #
#                                                                           #
#    http://www.apache.org/licenses/LICENSE-2.0                             #
#                                                                           #
# Unless required by applicable law or agreed to in writing, software       #
# distributed under the License is distributed on an "AS IS" BASIS,         #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  #
# See the License for the specific language governing permissions and       #
# limitations under the License.                                            #
#                                                                           #
#############################################################################

#############################################################################
#                                                                           #
# Author: Daniel Beetham                                                    #
#                                                                           #
# Purpose: To quickly calculate an Approximation of a root using the        #
#          Newton-Raphson Method                                            #
#                                                                           #
# Requires: SymPy (https://www.sympy.org) and PrettyTable modules           #
#           (https://github.com/ronisbr/PrettyTables.jl)                    #
#                                                                           #
#           Written & Tested on Python 3.4.10/3.8.5 on MacOS and Win10      #
#                                                                           #
#############################################################################

# SymPy is a symbolic manipulation library (ie. a CAS) written in Python!
import sympy as sp

#Pretty Table will allow us to show our data in a nice way
import prettytable as pt

# Define SymPy variables for use with our program
x = sp.symbols('x') # generic x for our calculations (

# f, f_prime = symbols('f f_prime', cls=Function)
f = None
f_prime = None

xold = None #This is our first guess at a root
xnew = None #This is our calculated value for Xn+1
tolerance = 6 # How many places to round to
max_tries = 50 # Maximum number of times to iterate before giving up

#Initalise pretty printing from SymPy. Will Use LATEX if available!
sp.init_printing()

print("Newton Raphson Solver")
print("---------------------")
print("\n")

#Try and get an expression for the f(x) to solve roots for
goodexpression = False
while goodexpression == False:

    print("Please enter a function to find roots: (Use Phython Synxtax like the following\na*x**2 + b*x + c -sqrt(x) -cos(x):")
    f_text = input("")

    try:
        # This is quite likely to fail, so we need to catch an exception for sure!
        f = sp.sympify(f_text)
    except:
        print("\n")
        print("Error! This function is ill defined - remember that you need to use phython syntax (with an x for the function variable)")
        print("\n")
    else:
        #differentiate our original function
        f_prime = sp.diff(f, x)

        print("\n")
        print("---------------------")
        print("\n")

        print("This is f(x) as it's been interpreted:\n")
        sp.pprint(f)

        print("\n")
        print("Here is f'(x):\n")
        sp.pprint(f_prime)

        print("\n")

        print("Are the functions correct? y/n")
        proceed = input("")

        if proceed == 'n':
            goodexpression = False
        else:
            goodexpression = True

#Enter a value to round to, and check the value to make sure it's an integer
goodexpression = False
while goodexpression == False:
    print("\n")
    print("Desired precision (in decimal places):\n")
    rounding = input()

    try:
        tolerance = sp.sympify(rounding)
    except:
        print("This can't be processed by SymPy is it neither a valid expression nor an integer\nTry again...")
    else:
        if tolerance.is_integer == True:
            goodexpression = True
        else:
           print("This isn't an integer value, try again...")

#Enter a guess, and check the value to make sure it's a real number
goodexpression = False
while goodexpression == False:
    print("\n")
    print("Enter a guess for the root:\n")
    guess = input()

    try:
        xold = sp.sympify(guess)
    except:
        print("This can't be processed by SymPy (it probably isn't even an expression)\nTry again...")
    else:
        if xold.is_real == True:
            goodexpression = True
        else:
           print("This isn't a real value, try again...")

n = 0
t = pt.PrettyTable(["X_n","F(n)","F'(n)","X_n+1"])

while n <= max_tries:

    #This is the actual Newton/Raphson Method (one line of code!)
    xnew = xold - (f.subs(x, xold)/f_prime.subs(x, xold))

    #print loading dots (only useful for functions that will require a lot of computation time)
    print(".", end="")

    t.add_row([xold, f.evalf(subs={x: xold}), f_prime.evalf(subs={x: xold}), xnew.evalf()])

    if xold.round(tolerance) == xnew.round(tolerance):
        break

    xold = xnew.evalf()

    n = n + 1

print("\n")

#Finally print our output table
print(t)


import numpy as np
import os

##Creating the identity matrix to answer the questions.

# =============================================================================
# This was the approach that I used to find the solution.
# Read this link dude https://towardsdatascience.com/linear-regression-from-scratch-with-numpy-implementation-finally-8e617d8e274c
# XT= np.transpose(train_x)
# XTX= np.dot(XT, train_x)
#
# ##What must be the size of the identity matrix - It has to be the same size as the matrix X transpose dot X
# XTX.shape
# n, m= XTX.shape
# identm= np.identity(n)
#
# lmda= 0.01
# RIXTX = np.linalg.inv(XTX + (lmda*identm))
# theta= np.dot(RIXTX, np.dot(XT,train_y))
# pred_y= np.dot(test_x, theta)
# error= test_y- pred_y
# =============================================================================

### Functions for you to fill in ###

def closed_form(X, Y, lambda_factor):
    """
    Computes the closed form solution of linear regression with L2 regularization

    Args:
        X - (n, d + 1) NumPy array (n datapoints each with d features plus the bias feature in the first dimension)
        Y - (n, ) NumPy array containing the labels (a number from 0-9) for each
            data point
        lambda_factor - the regularization constant (scalar)
    Returns:
        theta - (d + 1, ) NumPy array containing the weights of linear regression. Note that theta[0]
        represents the y-axis intercept of the model and therefore X[0] = 1
    """
    # YOUR CODE HERE
    transpose_x= np.transpose(X)
    xtx= np.dot(transpose_x, X)
    n,m= xtx.shape
    identity_matrix= np.identity(n)
    rtxtx= np.linalg.inv(xtx + (lambda_factor*identity_matrix))
    theta= np.dot(rtxtx, np.dot(transpose_x, Y))
    return theta
    raise NotImplementedError

### Functions which are already complete, for you to use ###
def compute_test_error_linear(test_x, Y, theta):
    test_y_predict = np.round(np.dot(test_x, theta))
    test_y_predict[test_y_predict < 0] = 0
    test_y_predict[test_y_predict > 9] = 9
    return 1 - np.mean(test_y_predict == Y)

Machine-Learning-with-Python- From-LM-to-DL/Unit 2. Non Linear Classification, Linear Regression, Colaborative Filtering/mnist/part1/linear_regression.py


# -*- coding: utf-8 -*-
"""
Created on Thu Jul  5 11:09:04 2018
Helper functions for ppLFER-MUM
@author: Tim Rodgers
"""
#
import numpy as np

def arr_conv(Ea,T2,k1,T1 = 298.15,):
    """Arrhenius equation conversion of rate reaction constants (k) from T1 to T2 (K)
    The default value for T1 is 298.15K. Activation energy should be in J. The
    result (res) will be k2 at T2
    """
    R = 8.314 #J/mol/K
    res =  k1 * np.exp((Ea / R) * (1 / T1 - 1 / T2))
    return res

def vant_conv(dU,T2,k1,T1 = 298.15,):
    """Van't Hoff equation conversion of partition coefficients (Kij) from T1 to T2 (K)
    The default value for T1 is 298.15K. Activation energy should be in J. The
    result (res) will be K2 at T2
    Yes this is literally the same as the Arrhenius but ¯\_(ツ)_/¯
    """
    R = 8.314 #J/mol/K
    res =  k1 * np.exp((dU / R) * (1 / T1 - 1 / T2))
    return res

def ppLFER(L,S,A,B,V,l,s,a,b,v,c):
    """polyparameter linear free energy relationship (ppLFER) in the 1 equation form from Goss (2005)
    Upper case letters represent Abraham's solute descriptors (compund specific)
    while the lower case letters represent the system parameters.
    """
    res = L*l+S*s+A*a+B*b+V*v+c
    return res


import numpy as np
import pandas as pd

## Centroid point for camera
def centeroidnp(arr):
    # Calculate the centroid of an array of points
    length = arr.shape[0]
    sum_x = np.sum(arr[:, 0])
    sum_y = np.sum(arr[:, 1])
    return sum_x/length, sum_y/length

def min_max_bounds(arr):
    # Calculate the min and max of an array of points
    max_x = np.amax(arr[:, 0])
    max_y = np.amax(arr[:, 1])
    min_x = np.amin(arr[:, 0])
    min_y = np.amin(arr[:, 1])
    return [min_x, max_x, min_y, max_y]

def bounds_midpoint(arr):
    # calculates the (x, y) midpoint given input in the format [min_x, max_x, min_y, max_y]
    min_x, max_x, min_y, max_y = arr
    return [(min_x + max_x)/2.0, (min_y + max_y)/2.0]

## Pandas DataFrame helpers:
def partition(df: pd.DataFrame, partitionColumn: str):
    # splits a DataFrame df on the unique values of a specified column (partitionColumn) to return a unique DataFrame for each unique value in the column.
    unique_values = np.unique(df[partitionColumn]) # array([ 0,  1,  2,  3,  4,  7, 11, 12, 13, 14])
    grouped_df = df.groupby([partitionColumn]) #  Groups on the specified column.
    return unique_values, np.array([grouped_df.get_group(aValue) for aValue in unique_values], dtype=object) # dataframes split for each unique value in the column


import matplotlib.pyplot as plt
import seaborn as sns

from IMLearn.learners import UnivariateGaussian, MultivariateGaussian
import numpy as np
import plotly.graph_objects as go
import plotly.io as pio
pio.templates.default = "simple_white"

def test_univariate_gaussian():
    # Question 1 - Draw samples and print fitted model
    # raise NotImplementedError()
    uni = UnivariateGaussian()

    samples = np.random.normal(10, 1, 1000)
    uni.fit(samples)

    est_mean = uni.mu_
    est_var = uni.var_

    print("(" + str(est_mean) + ", " + str(est_var) + ")")

    # Question 2 - Empirically showing sample mean is consistent
    # raise NotImplementedError()
    distances = []
    Xs = np.arange(10, 1001, 10)

    for x in Xs:
        distances.append(np.abs(est_mean - np.mean(samples[0:int(x)])))

    plt.plot(Xs, distances)
    plt.title("Q2 - distance from estimated mean")
    plt.xlabel("number of samples")
    plt.ylabel("distance")
    plt.show()

    # Question 3 - Plotting Empirical PDF of fitted model
    # raise NotImplementedError()
    samples.sort()
    plt.plot(samples, uni.pdf(samples))
    plt.title("Q3 - estimated PDF")
    plt.xlabel("sorted samples")
    plt.ylabel("PDF")
    plt.show()

def test_multivariate_gaussian():
    # Question 4 - Draw samples and print fitted model
    # raise NotImplementedError()
    multi = MultivariateGaussian()

    mu = np.zeros(4)
    mu[2] = 4
    mu = np.transpose(mu)

    cov = np.zeros((4, 4))
    cov[0, 0] = 1
    cov[0, 1] = 0.2
    cov[1, 0] = 0.2
    cov[1, 1] = 2
    cov[3, 0] = 0.5
    cov[0, 3] = 0.5
    cov[2, 2] = 1
    cov[3, 3] = 1

    samples = np.random.multivariate_normal(mu, cov, 1000)

    multi.fit(samples)
    print(multi.mu_)
    print(multi.cov_)

    # Question 5 - Likelihood evaluation
    # raise NotImplementedError()
    F1s = np.linspace(-10, 10, 200)
    F3s = np.linspace(-10, 10, 200)

    comb_and_log = np.zeros((200, 200))
    optional_mu = np.zeros(4)

    max_val = None
    max_row = 0
    max_col = 0

    for row, f1 in enumerate(F1s):
        optional_mu[0] = f1
        for col, f3 in enumerate(F3s):
            optional_mu[2] = f3
            comb_and_log[row, col] = multi.log_likelihood(optional_mu, cov, samples)
            if max_val is None or max_val < comb_and_log[row, col]:
                max_val = comb_and_log[row, col]
                max_row = f1
                max_col = f3

    go.Figure(go.Heatmap(x=F1s, y=F3s, z=comb_and_log), layout=go.Layout(title="Q5 - log likelihood - heatmap",
                                                                  xaxis_title="f1 indexes",
                                                                  yaxis_title="f3 indexes")).show()

    # Question 6 - Maximum likelihood

    print(max_row, max_col)

if __name__ == '__main__':
    np.random.seed(0)
    test_univariate_gaussian()
    test_multivariate_gaussian()


import argparse
import sys
import random
import math
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import logging
from pprint import PrettyPrinter
pp = PrettyPrinter(indent=4)

logging.basicConfig(filename='linear_regression.log', filemode='w', format='%(levelname)s - %(message)s', level=logging.INFO)

# predictors
def simple_predictor(x, v0, v1):
    return v0 + v1*x

def multi_predictor(x1, x2, v0, v1, v2):
    return v0 + v1*x1 + v2*x2

#cost functions
def simple_cost_function(X, Y, v0, v1):
    return (1 / 2 * len(X)) * sum([(simple_predictor(x, v0, v1) - y)**2 for x, y in zip(X, Y)])

def multi_cost_function(X1, X2, Y, v0, v1, v2):
    return (1 / 2 * len(X1)) * sum([(multi_predictor(x1, x2, v0, v1, v2) - y)**2 for x1, x2, y in zip(X1, X2, Y)])

#partial derivatives for gradients
def simple_cost_derived_with_respect_to_v0(x, y, v0, v1):
    return simple_predictor(x, v0, v1) - y

def simple_cost_derived_with_respect_to_v1(x, y, v0, v1):
    return x * (simple_predictor(x, v0, v1) - y)

def multi_cost_derived_with_respect_to_v0(x1, x2, y, v0, v1, v2):
    return multi_predictor(x1, x2, v0, v1, v2) - y

def multi_cost_derived_with_respect_to_v1(x1, x2, y, v0, v1, v2):
    return x1 * (multi_predictor(x1, x2, v0, v1, v2) - y)

def multi_cost_derived_with_respect_to_v2(x1, x2, y, v0, v1, v2):
    return x2 * (multi_predictor(x1, x2, v0, v1, v2) - y)

# use the gradient of the cost function to get new values with respect to the learning rate
def update_vs_for_simple_linear_regression(x, y, v0, v1, learning_rate):
    d_dv0 = sum([simple_cost_derived_with_respect_to_v0(a, b, v0, v1) for a, b in zip(x, y)])
    d_dv1 = sum([simple_cost_derived_with_respect_to_v1(a, b, v0, v1) for a, b in zip(x, y)])

    # We subtract because the derivatives point in direction of steepest ascent
    v0 = v0 - (learning_rate * (d_dv0 / len(x)))
    v1 = v1 - (learning_rate * (d_dv1 / len(x)))

    return v1, v0

def update_vs_for_multiple_linear_regression(x1, x2, y, v0, v1, v2, learning_rate):
    d_dv0 = sum([multi_cost_derived_with_respect_to_v0(a, b, c, v0, v1, v2) for a, b, c in zip(x1, x2, y)])
    d_dv1 = sum([multi_cost_derived_with_respect_to_v1(a, b, c, v0, v1, v2) for a, b, c in zip(x1, x2, y)])
    d_dv2 = sum([multi_cost_derived_with_respect_to_v2(a, b, c, v0, v1, v2) for a, b, c in zip(x1, x2, y)])

    # We subtract because the derivatives point in direction of steepest ascent
    v0 = v0 - (learning_rate * (d_dv0 / len(x1)))
    v1 = v1 - (learning_rate * (d_dv1 / len(x1)))
    v2 = v2 - (learning_rate * (d_dv2 / len(x1)))

    return v2, v1, v0

def simple_linear_regression(X, Y, alpha_init=.1, convergence=.01):
    logging.info(f"-----------------------Starting Simple Linear Regression----------------------------------")
    v1 = Y[0]
    v0 = Y[1] - Y[0] / X[1] - X[0]
    learning_rate = alpha_init

    logging.info(f"Initializing simple linear regression with v1={v1}, v0={v0}, learning rate={learning_rate}")
    logging.info(f"Algorithm is considered converged when difference between costs is less than {convergence}")
    costs = []
    i = 0
    cost = simple_cost_function(X, Y, v0, v1)
    logging.info(f"Initial cost={cost}")
    costs.append(cost)
    i = i + 1

    initial_values = {
        'X': X,
        'Y': Y,
        'v1': v1,
        'v0': v0,
        'learning_rate': learning_rate,
        'convergence': convergence
    }

    while True:
        v1, v0 = update_vs_for_simple_linear_regression(X, Y, v1, v0, learning_rate)

        #Calculate cost for auditing purposes
        cost = simple_cost_function(X, Y, v0, v1)
        costs.append(cost)
        logging.info(f"Update for iteration {i}. New values are v1={v1}, v0={v0}.")
        logging.info(f"Cost for iteration {i} is {cost}. This is {abs(costs[i] - costs[i-1])}")
        logging.info(f"difference from {i-1} iteration cost")

        if abs(costs[i] - costs[i-1]) < convergence:
            logging.info(f"Convergence reached, returning parameters and costs.")
            break

        i = i + 1

    output_values = {
        'v1': v1,
        'v0': v0,
        'costs': costs
    }

    return_vals = {
        'in': initial_values,
        'out': output_values
    }

    return return_vals

def multiple_linear_regression(X1, X2, Y, alpha_init=.001, convergence=.001):
    logging.info(f"-----------------------Starting Multiple Linear Regression--------------------------------")
    v0 = 0
    v1 = 1
    v2 = 1
    learning_rate = alpha_init

    logging.info(f"Initializing multiple linear regression with v2={v2}, v1={v1}, v0={v0}, learning rate={learning_rate}")
    logging.info(f"Algorithm is considered converged when difference between costs is less than {convergence}")
    costs = []
    i = 0
    cost = multi_cost_function(X1, X2, Y, v0, v1, v2)
    logging.info(f"Initial cost={cost}")
    costs.append(cost)
    i = i + 1

    initial_values = {
        'X1': X1,
        'X2': X2,
        'Y': Y,
        'v2': v2,
        'v1': v1,
        'v0': v0,
        'learning_rate': learning_rate,
        'convergence': convergence
    }

    while True:
        v2, v1, v0 = update_vs_for_multiple_linear_regression(X1, X2, Y, v0, v1, v2, learning_rate)

        #Calculate cost for auditing purposes
        cost = multi_cost_function(X1, X2, Y, v0, v1, v2)
        costs.append(cost)
        logging.info(f"Update for iteration {i}. New values are v2={v2}, v1={v1}, v0={v0}.")
        logging.info(f"Cost for iteration {i} is {cost}. This is {abs(costs[i] - costs[i-1])}")
        logging.info(f"difference from {i-1} iteration cost")

        if abs(costs[i] - costs[i-1]) < convergence:
            logging.info(f"Convergence reached, returning parameters and costs.")
            break

        i = i + 1

    output_values = {
        'v2': v2,
        'v1': v1,
        'v0': v0,
        'costs': costs
    }

    return_vals = {
        'in': initial_values,
        'out': output_values
    }

    return return_vals

def create_line(x, v0, v1):
    return [(x, v0+v1*x) for x in x]

def create_plane(x1, x2, v0, v1, v2):
    print(x1)
    print(x2)
    return [(a, b, v0+v1*a+v2*b) for a in x1 for b in x2]

def create_simple_linear_regression_plots():
    #solve the simple linear regression
    x = [ 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0 ]
    y = [ 5.1, 6.1, 6.9, 7.8, 9.2, 9.9, 11.5, 12.0, 12.8 ]
    logging.info(f"Simple linear regression training data")
    logging.info(f"X: {x}")
    logging.info(f"Y: {y}")
    scatter_data = { 'x': x, 'y': y}

    # create best fit using ML
    simple_linear_reg_info = simple_linear_regression(scatter_data['x'], scatter_data['y'])
    v0, v1, costs = (
        simple_linear_reg_info['out']['v0'],
        simple_linear_reg_info['out']['v1'],
        simple_linear_reg_info['out']['costs']
    )
    logging.info(f"Simple linear regression returned the following parameters")
    logging.info(f"v0: {v0}")
    logging.info(f"v1: {v1}")
    logging.info(f"costs: {costs}")
    x, y = zip(*create_line(scatter_data['x'], v0, v1))
    regression_line_data = {'x': x, 'y': y}

    #plot cost as a measure of iterations
    x, y = zip(*list(enumerate(costs, 1)))
    cost_per_iter_line_data = {'x': x, 'y': y}

    # actually graph the stuff
    plt.figure()
    plt.plot(scatter_data['x'], scatter_data['y'], '.')
    plt.plot(regression_line_data['x'], regression_line_data['y'], 'b-', label='simple linear regression')
    plt.xlabel('x')
    plt.ylabel('y')
    plt.legend(loc='upper left')
    logging.info(f"Writing linear regression graph to simple_linear_regression.png")
    plt.savefig('simple_linear_regression.png')
    plt.figure()
    plt.plot(cost_per_iter_line_data['x'], cost_per_iter_line_data['y'], 'b-', label='Cost for iteration')
    plt.xlabel('Iteration')
    plt.ylabel('Cost (J)')
    plt.legend(loc='upper right')
    logging.info(f"Writing cost over time for linear regression graph to simple_linear_regression_cost_per_iteration.png")
    plt.savefig('simple_linear_regression_cost_per_iteration.png')

    return simple_linear_reg_info

def create_multiple_linear_regression_plots():
    mlr_returns = []

    #solve the multiple linear regression
    x1 = [ 0, 1, 1, 2, 1 ]
    x2 = [ 1, 0, 1, 1, 2 ]
    y = [ 0.05, 2.05, 1.05, 1.95, -0.05 ]
    logging.info(f"Multiple linear regression training data:")
    logging.info(f"X1: {x1}")
    logging.info(f"X2: {x2}")
    logging.info(f"Y: {y}")
    scatter_data = { 'x1': x1, 'x2': x2, 'y': y}

    # create best fit using ML
    logging.info(f"Calling multiple linear regression with learning rate=0.001:")
    multi_linear_reg_info = multiple_linear_regression(scatter_data['x1'], scatter_data['x2'], scatter_data['y'])
    mlr_returns.append(multi_linear_reg_info)
    v0, v1, v2, costs1 = (
        multi_linear_reg_info['out']['v0'],
        multi_linear_reg_info['out']['v1'],
        multi_linear_reg_info['out']['v2'],
        multi_linear_reg_info['out']['costs']
    )
    x, y = zip(*list(enumerate(costs1, 1)))
    logging.info(f"Multiple linear regression returned the following parameters:")
    logging.info(f"v0: {v0}")
    logging.info(f"v1: {v1}")
    logging.info(f"v2: {v2}")
    costs1_per_iter_line_data = {'x': x, 'y': y}

    logging.info(f"Calling multiple linear regression with learning rate=0.01:")
    multi_linear_reg_info = multiple_linear_regression(scatter_data['x1'], scatter_data['x2'], scatter_data['y'], alpha_init=0.01)
    mlr_returns.append(multi_linear_reg_info)
    v0, v1, v2, costs2 = (
        multi_linear_reg_info['out']['v0'],
        multi_linear_reg_info['out']['v1'],
        multi_linear_reg_info['out']['v2'],
        multi_linear_reg_info['out']['costs']
    )
    x, y = zip(*list(enumerate(costs2, 1)))
    logging.info(f"Multiple linear regression returned the following parameters:")
    logging.info(f"v0: {v0}")
    logging.info(f"v1: {v1}")
    logging.info(f"v2: {v2}")
    costs2_per_iter_line_data = {'x': x, 'y': y}

    logging.info(f"Calling multiple linear regression with learning rate=0.1:")
    multi_linear_reg_info = multiple_linear_regression(scatter_data['x1'], scatter_data['x2'], scatter_data['y'], alpha_init=0.1)
    mlr_returns.append(multi_linear_reg_info)
    v0, v1, v2, costs3 = (
        multi_linear_reg_info['out']['v0'],
        multi_linear_reg_info['out']['v1'],
        multi_linear_reg_info['out']['v2'],
        multi_linear_reg_info['out']['costs']
    )
    x, y = zip(*list(enumerate(costs3, 1)))
    logging.info(f"Multiple linear regression returned the following parameters:")
    logging.info(f"v0: {v0}")
    logging.info(f"v1: {v1}")
    logging.info(f"v2: {v2}")
    costs3_per_iter_line_data = {'x': x, 'y': y}

    logging.info(f"Calling multiple linear regression with learning rate=0.2:")
    multi_linear_reg_info = multiple_linear_regression(scatter_data['x1'], scatter_data['x2'], scatter_data['y'], alpha_init=0.2)
    mlr_returns.append(multi_linear_reg_info)
    v0, v1, v2, costs4 = (
        multi_linear_reg_info['out']['v0'],
        multi_linear_reg_info['out']['v1'],
        multi_linear_reg_info['out']['v2'],
        multi_linear_reg_info['out']['costs']
    )
    x, y = zip(*list(enumerate(costs4, 1)))
    logging.info(f"Multiple linear regression returned the following parameters:")
    logging.info(f"v0: {v0}")
    logging.info(f"v1: {v1}")
    logging.info(f"v2: {v2}")
    costs4_per_iter_line_data = {'x': x, 'y': y}

    logging.info(f"Calling multiple linear regression with learning rate=0.3:")
    multi_linear_reg_info = multiple_linear_regression(scatter_data['x1'], scatter_data['x2'], scatter_data['y'], alpha_init=0.3)
    mlr_returns.append(multi_linear_reg_info)
    v0, v1, v2, costs5 = (
        multi_linear_reg_info['out']['v0'],
        multi_linear_reg_info['out']['v1'],
        multi_linear_reg_info['out']['v2'],
        multi_linear_reg_info['out']['costs']
    )
    x, y = zip(*list(enumerate(costs5, 1)))
    logging.info(f"Multiple linear regression returned the following parameters:")
    logging.info(f"v0: {v0}")
    logging.info(f"v1: {v1}")
    logging.info(f"v2: {v2}")
    costs5_per_iter_line_data = {'x': x, 'y': y}

    plt.figure()
    plt.plot(costs1_per_iter_line_data['x'], costs1_per_iter_line_data['y'], 'b-', label='alpha=0.001')
    plt.plot(costs2_per_iter_line_data['x'], costs2_per_iter_line_data['y'], 'r-', label='alpha=0.01')
    plt.plot(costs3_per_iter_line_data['x'], costs3_per_iter_line_data['y'], 'g-', label='alpha=0.1')
    plt.plot(costs4_per_iter_line_data['x'], costs4_per_iter_line_data['y'], 'y-', label='alpha=0.2')
    plt.plot(costs5_per_iter_line_data['x'], costs5_per_iter_line_data['y'], 'k-', label='alpha=0.3')
    plt.xlabel('Iteration')
    plt.ylabel('Cost (J)')
    plt.legend(loc='upper right')
    logging.info(f"Writing cost over time for multiple linear regression graph to multiple_linear_regression_cost_per_iteration.png")
    plt.savefig('multiple_linear_regression_cost_per_iteration.png')

    return mlr_returns

def create_plot():
    slr_data = create_simple_linear_regression_plots()
    slr_data['out'].pop('costs')
    mlr_data = create_multiple_linear_regression_plots()
    [data['out'].pop('costs') for data in mlr_data]
    logging.info(f"-----------------------Review--------------------------------")
    logging.info(f"(Note: hiding costs for succinctness, scroll up for more     ")
    logging.info(f" details on what the specific costs were on each regression.)")
    logging.info(f"---------------Simple Linear Regression----------------------")
    logging.info(pp.pformat(slr_data))
    logging.info(f"--------------Multiple Linear Regression---------------------")
    logging.info(pp.pformat(mlr_data))

def main():
    create_plot()

if __name__ == "__main__":
    main(sys.argv[1:])


import numpy as np

from collections import Counter
from pathlib import Path
from typing import Iterable, List

Sample_Input = """3,4,3,1,2
"""

def parse_input(input: str) -> tuple:
    lines = input.strip().split(",")
    return list(map(int, lines))

def simulate(fish: Iterable[int], days: int) -> int:
    """Don't brute force it, use math

    Resources
        https://numpy.org/doc/stable/reference/routines.linalg.html
        https://numpy.org/doc/stable/reference/generated/numpy.linalg.matrix_power.html#numpy.linalg.matrix_power
    """
    cache = Counter(fish)
    init = np.array([cache[i] for i in range(9)])
    A = np.array(
        [
            [0, 1, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 1, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 1, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 1, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 1, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 1, 0, 0],
            [1, 0, 0, 0, 0, 0, 0, 1, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 1],
            [1, 0, 0, 0, 0, 0, 0, 0, 0],
        ]
    )
    B = np.linalg.matrix_power(A, days)
    return B.dot(init).sum()

def brute_force_simulate(fish: Iterable[int], days: int) -> int:
    d = np.array(fish)
    for _ in range(days):
        d = brute_force_simulate_one_day(d)
    return d

def brute_force_simulate_one_day(fish: Iterable[int]) -> List[int]:
    new_fish = fish.size - np.count_nonzero(fish)
    fish[fish == 0] = 7
    fish = np.subtract(fish, 1)
    fish = np.append(fish, [8] * new_fish)

    return fish

if __name__ == "__main__":

    fish_input = (Path.cwd() / "2021" / "data" / f"{Path(__file__).stem}_input.txt").read_text()
    initial_fish = parse_input(fish_input)

    print("========= NEW ============")
    days = 18
    simulated_fish = simulate(list(initial_fish), days=days)
    print(f"Starting with {len(initial_fish):,} fish, after {days} days the projection is {simulated_fish:,}")

    days = 256
    simulated_fish = simulate(list(initial_fish), days=days)
    print(f"Starting with {len(initial_fish):,} fish, after {days} days the projection is {simulated_fish:,}")


"""
M. Yaşar Polatlı
250201075
HW07
"""

import numpy as np
import random
import matplotlib.pyplot as plt
import scipy.stats as stats
from scipy.stats import binom
from math import pow

# 2 paramaters; n = 40, p = 0.3
# theoretical expected value of this distribution is 40*0.3 = 12
# theoretical variance of this distribution is 40*0.3*0.7 = 8.4

# I created myself the simulation function for binomial distribution, due to the negligence of being requested.
# But i didn't use it in my code becasue it is slower than binom.rvs in my old computer. But it is completely working, you can try.

# Samples is a list of the number of successes from each trial of N experiments.
def binomial_dist(N):
    samples = []
    for i in range(N):
        successes = 0
        for i in range(40):
            if random.uniform(0, 1) < 0.3:
                successes += 1
        samples.append(successes)
    return samples

# in order to apply Method of Moments we need the mean of the sample, this function handles it.
def sample_mean(N):
    expc = np.mean(binom.rvs(40, 0.3, size=N)) # you can replace binom.rvs with my function 'binomial_dist(N)'
    return expc                                 # but it is slower.

# in order to apply Method of Moments we need the variance of the sample, this function handles it.
def sample_variance(N):
    varc = np.var(binom.rvs(40, 0.3, size=N))
    return varc

# estimated p parameter using outcome equation from Method of Moments.
def estimated_p(N):
    return 1 - (sample_variance(N)/sample_mean(N))

# estimated n parameter using outcome equation from Method of Moments.
def estimated_n(N):
    return pow(sample_mean(N),2)/(sample_mean(N) - sample_variance(N))

# histogram plotting and printing the results for p.
def printry_p():
    plt.figure()
    plt.title("Histogram for estimated p")
    for N in [200,800,3200]:
        arr1 = []

        for i in range(1000): # simulation loop for 1000 times.
            arr1.append(estimated_p(N))

        if N == 200: # plots the graphs according to N with coloring.
            plt.hist(arr1, 100, density=True, color="skyblue", label='200', alpha=0.7)
        if N == 800:
            plt.hist(arr1, 100, density=True, color="navajowhite", label='800', alpha=0.7)
        if N == 3200:
            plt.hist(arr1, 100, density=True, color="lightgreen", label='3200', alpha=0.7)
        # prints the results for each N value.
        print("Mean for estimated p for sample size of ", N, "is ", np.mean(arr1))
        print("Standard deviation for estimated p for sample size of ", N, "is ", np.std(arr1))
    plt.legend()
    plt.show()

# histogram plotting and printing the results for p.
def printry_n():
    plt.figure()
    plt.title("Histogram for estimated n")
    for N in [200,800,3200]:
        arr2 = []

        for i in range(1000):
            arr2.append(estimated_n(N))

        if N == 200:
            plt.hist(arr2, 100, density=True, color="skyblue", label='200', alpha=0.7)
        if N == 800:
            plt.hist(arr2, 100, density=True, color="navajowhite", label='800', alpha=0.7)
        if N == 3200:
            plt.hist(arr2, 100, density=True, color="lightgreen", label='3200', alpha=0.7)
        print("Mean for estimated n for sample size of ", N, "is ", np.mean(arr2))
        print("Standard deviation for estimated n for sample size of ", N, "is ", np.std(arr2))
    plt.legend()
    plt.show()

# function calls to observe the results.
printry_n()
printry_p()


import numpy as np
import numpy.polynomial.polynomial as pol
import matplotlib.pyplot as plt

def NPV(r, CF):
    x = 1.0 / (1.0 + 0.01 * r)
    return pol.polyval(x, CF)

def IRR(CF):
    Roots = pol.polyroots(CF)
    Real = np.real(Roots[np.isreal(Roots)])
    Positive = (Real[Real > 0.0]).item(0)
    return (1.0 / Positive - 1.0) * 100

Periods = np.linspace(0, 4, 5)
V_CF = np.array([[-5.0, 1.5, 1.5, 1.5, 1.5],
                 [-7.0, 2.0, 2.0, 2.0, 2.0],
                 [-9.0, 4.0, 3.0, 2.0, 1.0],
                 [-9.0, 1.0, 2.0, 3.0, 4.0]])
V_Title = ['project A', 'project B', 'project C', 'project D']
fig1 = plt.figure(num=1, facecolor='w')
for fig_num in range(4):
    plt.subplot(2, 2, fig_num + 1)
    plt.bar(Periods, V_CF[fig_num, :], color=(0.5, 0.5, 0.5))
    plt.title(V_Title[fig_num])
    plt.axhline(color='k', linewidth=0.5)
    plt.ylim(-10, 5)
    if fig_num == 2 or fig_num == 3:
        plt.xlabel('time')
    if fig_num == 0 or fig_num == 2:
        plt.ylabel('cash flow')
    if fig_num == 1 or fig_num == 0:
        plt.xticks([])
plt.show()
r = 5
V_NPV = np.zeros(4)
for cf_num in range(4):
    V_NPV[cf_num] = NPV(r, V_CF[cf_num, :])
V_IRR = np.zeros(4)
for cf_num in range(4):
    V_IRR[cf_num] = IRR(V_CF[cf_num, :])


import numpy as np
import time

# For printing, set precision and suppress scientific notation
np.set_printoptions(precision=4, suppress=True)

# Mosaic resolution
n = 5

# Integration quadrature parameter
qn = 3

# Construct centerpoints of the mosaic tiles (patches),
# d denotes the length of the side of a patch
d = 2/n
tmp = [-1-d/2 + i*d for i in range(1,n+1)]

# Centerpoint coordinate matrices
Xmat = np.zeros((n**2, 6))
Ymat = np.zeros((n**2, 6))
Zmat = np.zeros((n**2, 6))

# Construct the centerpoints for all the tiles in all the six walls

# The back wall (0)
X, Z = np.meshgrid(tmp, tmp)
Xmat[:,0] = np.concatenate(np.transpose(X)).flat
Zmat[:,0] = np.concatenate(np.transpose(Z)).flat
Ymat[:,0] = np.ones(n**2)

# Roof (1)
X, Y = np.meshgrid(tmp, tmp)
Xmat[:,1] = np.concatenate(np.transpose(X)).flat
Ymat[:,1] = np.concatenate(np.transpose(Y)).flat
Zmat[:,1] = np.ones(n**2)

# Floor (2)
Xmat[:,2] = np.concatenate(np.transpose(X)).flat
Ymat[:,2] = np.concatenate(np.transpose(Y)).flat
Zmat[:,2] = -np.ones(n**2)

# Right-hand-side wall (3)
Y, Z = np.meshgrid(tmp, tmp)
Ymat[:,3] = np.concatenate(np.transpose(Y)).flat
Zmat[:,3] = np.concatenate(np.transpose(Z)).flat
Xmat[:,3] = np.ones(n**2)

# Left-hand-side wall (4)
Ymat[:,4] = np.concatenate(np.transpose(Y)).flat
Zmat[:,4] = np.concatenate(np.transpose(Z)).flat
Xmat[:,4] = -np.ones(n**2)

# Front wall (5)
X, Z = np.meshgrid(tmp, tmp)
Xmat[:,5] = np.concatenate(np.transpose(X)).flat
Zmat[:,5] = np.concatenate(np.transpose(Z)).flat
Ymat[:,5] = -np.ones(n**2)

# Formula for view factor between square-shaped pixels sharing an edge.
# From Cohen & Wallace: Radiosity and realistic image synthesis
# (Academic Press Professional 1993), Figure 4.4
atan = np.arctan
shared_edge_F = (2 * atan(1) - np.sqrt(2) * atan(1/np.sqrt(2) )+ .25 * np.log(3/4)) / np.pi

# Quadrature points and weights for integrating over a square
# of size d x d centered at the origin
tt = [i/qn*d - .5*d/qn - d/2 for i in range (1,qn+1)]
q1, q2 = np.meshgrid(tt, tt)
qw = (d/qn) ** 4 # Area of quadrature pixel, squared, serves as the weight

# Form the geometrical view factor matrix F.
# See http://en.wikipedia.org/wiki/View_factor for details of computation.

# Initialize the matrix
F = np.zeros((6*n**2, 6*n**2))

epsilon = 10 ** -8

start = time.time()

# From the roof (j) to the back wall (i)
for i in range(0, n*n):
  for j in range(0, n*n):
    # Centerpoint of the current pixel in the back wall
    pi = [Xmat[i, 0], Ymat[i, 0], Zmat[i, 0]]
    # Centerpoint of the current pixel in the roof
    pj = [Xmat[j, 1], Ymat[j, 1], Zmat[j, 1]]
    # Distance between the points
    difvec0 = [i-j for i, j in zip(pi, pj)]
    r0 = np.linalg.norm(difvec0)

    # Check if the two pixels share an edge
    if r0 < np.sqrt(2) * d/2 + epsilon: # Edge shared
      # Calculate element of F analytically
      F[i, n**2 + j] = shared_edge_F
    else: # Edge not shared: integrate for F using quadrature
      # Initalize matrix of integrand values at quadrature points
      intgndmat = np.zeros((qn**2, qn**2))
      # Double loop over four-dimensional quadrature
      for k in range(0, qn**2):
        for l in range(0, qn**2):
          # Quadrature point in the back wall pixel
          qpi = pi[:]
          qpi[0] += q1[k%qn][k//qn]
          qpi[2] += q2[k%qn][k//qn]
          # Quadrature point in the roof pixel
          qpj = pj[:]
          qpj[0] += q1[l%qn][l//qn]
          qpj[1] += q2[l%qn][l//qn]
          # Vector connecting the quadrature points
          difvec = [qpi[i]-qpj[i] for i in range(0, len(qpi))]
          r = np.linalg.norm(difvec)
          tmp2 = difvec / r # Unit direction vector
          cos_i = abs(tmp2[1])
          cos_j = abs(tmp2[2])
          # Evaluate integrand
          intgndmat[k, l] = np.dot(cos_i, cos_j) / (np.pi * r**2)

      # Calculate element of F
      viewfactor = qw * sum(sum(intgndmat)) / d**2
      F[i, n**2 + j] = viewfactor

print("Geometric view factors roof->back done (1/15)")

# From the floor (j) to the back wall (i)
for i in range(0, n*n):
  for j in range(0, n*n):
    # Centerpoint of the current pixel in the back wall
    pi = [Xmat[i, 0], Ymat[i, 0], Zmat[i, 0]]
    # Centerpoint of the current pixel in the floor
    pj = [Xmat[j, 2], Ymat[j, 2], Zmat[j, 2]]
    # Distance between the points
    difvec0 = [i-j for i, j in zip(pi, pj)]
    r0 = np.linalg.norm(difvec0)

    # Check if the two pixels share an edge
    if r0 < np.sqrt(2) * d/2 + epsilon: # Edge shared
      # Calculate element of F analytically
      F[i, 2 * n**2 + j] = shared_edge_F
    else: # Edge not shared: integrate for F using quadrature
      # Initalize matrix of integrand values at quadrature points
      intgndmat = np.zeros((qn**2, qn**2))
      # Double loop over four-dimensional quadrature
      for k in range(0, qn**2):
        for l in range(0, qn**2):
          # Quadrature point in the back wall pixel
          qpi = pi[:]
          qpi[0] += q1[k%qn][k//qn]
          qpi[2] += q2[k%qn][k//qn]
          # Quadrature point in the floor pixel
          qpj = pj[:]
          qpj[0] += q1[l%qn][l//qn]
          qpj[1] += q2[l%qn][l//qn]
          # Vector connecting the quadrature points
          difvec = [qpi[i]-qpj[i] for i in range(0, len(qpi))]
          r = np.linalg.norm(difvec)
          tmp2 = difvec / r # Unit direction vector
          cos_i = abs(tmp2[2])
          cos_j = abs(tmp2[1])
          # Evaluate integrand
          intgndmat[k, l] = np.dot(cos_i, cos_j) / (np.pi * r**2)

      # Calculate element of F
      viewfactor = qw * sum(sum(intgndmat)) / d**2
      F[i, 2 * n**2 + j] = viewfactor

print("Geometric view factors floor->back done (2/15)")

# From the right-hand-side wall (j) to the back wall (i)
for i in range(0, n*n):
  for j in range(0, n*n):
    # Centerpoint of the current pixel in the back wall
    pi = [Xmat[i, 0], Ymat[i, 0], Zmat[i, 0]]
    # Centerpoint of the current pixel in the right-hand-side wall
    pj = [Xmat[j, 3], Ymat[j, 3], Zmat[j, 3]]
    # Distance between the points
    difvec0 = [i-j for i, j in zip(pi, pj)]
    r0 = np.linalg.norm(difvec0)

    # Check if the two pixels share an edge
    if r0 < np.sqrt(2) * d/2 + epsilon: # Edge shared
      # Calculate element of F analytically
      F[i, 3 * n**2 + j] = shared_edge_F
    else: # Edge not shared: integrate for F using quadrature
      # Initalize matrix of integrand values at quadrature points
      intgndmat = np.zeros((qn**2, qn**2))
      # Double loop over four-dimensional quadrature
      for k in range(0, qn**2):
        for l in range(0, qn**2):
          # Quadrature point in the back wall pixel
          qpi = pi[:]
          qpi[0] += q1[k%qn][k//qn]
          qpi[2] += q2[k%qn][k//qn]
          # Quadrature point in the right wall pixel
          qpj = pj[:]
          qpj[1] += q1[l%qn][l//qn]
          qpj[2] += q2[l%qn][l//qn]
          # Vector connecting the quadrature points
          difvec = [qpi[i]-qpj[i] for i in range(0, len(qpi))]
          r = np.linalg.norm(difvec)
          tmp2 = difvec / r # Unit direction vector
          cos_i = abs(tmp2[0])
          cos_j = abs(tmp2[1])
          # Evaluate integrand
          intgndmat[k, l] = np.dot(cos_i, cos_j) / (np.pi * r**2)

      # Calculate element of F
      viewfactor = qw * sum(sum(intgndmat)) / d**2
      F[i, 3 * n**2 + j] = viewfactor

print("Geometric view factors right->back done (3/15)")

# From the left-hand-side wall (j) to the back wall (i)
for i in range(0, n*n):
  for j in range(0, n*n):
    # Centerpoint of the current pixel in the back wall
    pi = [Xmat[i, 0], Ymat[i, 0], Zmat[i, 0]]
    # Centerpoint of the current pixel in the left-hand-side wall
    pj = [Xmat[j, 4], Ymat[j, 4], Zmat[j, 4]]
    # Distance between the points
    difvec0 = [i-j for i, j in zip(pi, pj)]
    r0 = np.linalg.norm(difvec0)

    # Check if the two pixels share an edge
    if r0 < np.sqrt(2) * d/2 + epsilon: # Edge shared
      # Calculate element of F analytically
      F[i, 4 * n**2 + j] = shared_edge_F
    else: # Edge not shared: integrate for F using quadrature
      # Initalize matrix of integrand values at quadrature points
      intgndmat = np.zeros((qn**2, qn**2))
      # Double loop over four-dimensional quadrature
      for k in range(0, qn**2):
        for l in range(0, qn**2):
          # Quadrature point in the back wall pixel
          qpi = pi[:]
          qpi[0] += q1[k%qn][k//qn]
          qpi[2] += q2[k%qn][k//qn]
          # Quadrature point in the left wall pixel
          qpj = pj[:]
          qpj[1] += q1[l%qn][l//qn]
          qpj[2] += q2[l%qn][l//qn]
          # Vector connecting the quadrature points
          difvec = [qpi[i]-qpj[i] for i in range(0, len(qpi))]
          r = np.linalg.norm(difvec)
          tmp2 = difvec / r # Unit direction vector
          cos_i = abs(tmp2[0])
          cos_j = abs(tmp2[1])
          # Evaluate integrand
          intgndmat[k, l] = np.dot(cos_i, cos_j) / (np.pi * r**2)

      # Calculate element of F
      viewfactor = qw * sum(sum(intgndmat)) / d**2
      F[i, 4 * n**2 + j] = viewfactor

print("Geometric view factors left->back done (4/15)")

# From the front wall (j) to the back wall (i)
for i in range(0, n*n):
  for j in range(0, n*n):
    # Centerpoint of the current pixel in the back wall
    pi = [Xmat[i, 0], Ymat[i, 0], Zmat[i, 0]]
    # Centerpoint of the current pixel in the front wall
    pj = [Xmat[j, 5], Ymat[j, 5], Zmat[j, 5]]
    # Distance between the points
    difvec0 = [i-j for i, j in zip(pi, pj)]
    r0 = np.linalg.norm(difvec0)

    # Check if the two pixels share an edge
    if r0 < np.sqrt(2) * d/2 + epsilon: # Edge shared
      # Calculate element of F analytically
      F[i, 5 * n**2 + j] = shared_edge_F
    else: # Edge not shared: integrate for F using quadrature
      # Initalize matrix of integrand values at quadrature points
      intgndmat = np.zeros((qn**2, qn**2))
      # Double loop over four-dimensional quadrature
      for k in range(0, qn**2):
        for l in range(0, qn**2):
          # Quadrature point in the back wall pixel
          qpi = pi[:]
          qpi[0] += q1[k%qn][k//qn]
          qpi[2] += q2[k%qn][k//qn]
          # Quadrature point in the front wall pixel
          qpj = pj[:]
          qpj[0] += q1[l%qn][l//qn]
          qpj[2] += q2[l%qn][l//qn]
          # Vector connecting the quadrature points
          difvec = [qpi[i]-qpj[i] for i in range(0, len(qpi))]
          r = np.linalg.norm(difvec)
          tmp2 = difvec / r # Unit direction vector
          cos_i = abs(tmp2[1])
          cos_j = abs(tmp2[1])
          # Evaluate integrand
          intgndmat[k, l] = np.dot(cos_i, cos_j) / (np.pi * r**2)

      # Calculate element of F
      viewfactor = qw * sum(sum(intgndmat)) / d**2
      F[i, 5 * n**2 + j] = viewfactor

print("Geometric view factors front->back done (5/15)")

####################################
# From the floor (j) to the roof (i)
for i in range(0, n*n):
  for j in range(0, n*n):
    # Centerpoint of the current pixel in the roof
    pi = [Xmat[i, 1], Ymat[i, 1], Zmat[i, 1]]
    # Centerpoint of the current pixel in the floor
    pj = [Xmat[j, 2], Ymat[j, 2], Zmat[j, 2]]
    # Distance between the points
    difvec0 = [i-j for i, j in zip(pi, pj)]
    r0 = np.linalg.norm(difvec0)

    # Check if the two pixels share an edge
    if r0 < np.sqrt(2) * d/2 + epsilon: # Edge shared
      # Calculate element of F analytically
      F[n**2 + i, 2 * n**2 + j] = shared_edge_F
    else: # Edge not shared: integrate for F using quadrature
      # Initalize matrix of integrand values at quadrature points
      intgndmat = np.zeros((qn**2, qn**2))
      # Double loop over four-dimensional quadrature
      for k in range(0, qn**2):
        for l in range(0, qn**2):
          # Quadrature point in the roof pixel
          qpi = pi[:]
          qpi[0] += q1[k%qn][k//qn]
          qpi[1] += q2[k%qn][k//qn]
          # Quadrature point in the floor pixel
          qpj = pj[:]
          qpj[0] += q1[l%qn][l//qn]
          qpj[1] += q2[l%qn][l//qn]
          # Vector connecting the quadrature points
          difvec = [qpi[i]-qpj[i] for i in range(0, len(qpi))]
          r = np.linalg.norm(difvec)
          tmp2 = difvec / r # Unit direction vector
          cos_i = abs(tmp2[2])
          cos_j = abs(tmp2[2])
          # Evaluate integrand
          intgndmat[k, l] = np.dot(cos_i, cos_j) / (np.pi * r**2)

      # Calculate element of F
      viewfactor = qw * sum(sum(intgndmat)) / d**2
      F[n**2 + i, 2 * n**2 + j] = viewfactor

print("Geometric view factors floor->roof done (6/15)")

# From the right-hand-side wall (j) to the roof (i)
for i in range(0, n*n):
  for j in range(0, n*n):
    # Centerpoint of the current pixel in the roof
    pi = [Xmat[i, 1], Ymat[i, 1], Zmat[i, 1]]
    # Centerpoint of the current pixel in the right wall
    pj = [Xmat[j, 3], Ymat[j, 3], Zmat[j, 3]]
    # Distance between the points
    difvec0 = [i-j for i, j in zip(pi, pj)]
    r0 = np.linalg.norm(difvec0)

    # Check if the two pixels share an edge
    if r0 < np.sqrt(2) * d/2 + epsilon: # Edge shared
      # Calculate element of F analytically
      F[n**2 + i, 3 * n**2 + j] = shared_edge_F
    else: # Edge not shared: integrate for F using quadrature
      # Initalize matrix of integrand values at quadrature points
      intgndmat = np.zeros((qn**2, qn**2))
      # Double loop over four-dimensional quadrature
      for k in range(0, qn**2):
        for l in range(0, qn**2):
          # Quadrature point in the roof pixel
          qpi = pi[:]
          qpi[0] += q1[k%qn][k//qn]
          qpi[1] += q2[k%qn][k//qn]
          # Quadrature point in the right wall pixel
          qpj = pj[:]
          qpj[1] += q1[l%qn][l//qn]
          qpj[2] += q2[l%qn][l//qn]
          # Vector connecting the quadrature points
          difvec = [qpi[i]-qpj[i] for i in range(0, len(qpi))]
          r = np.linalg.norm(difvec)
          tmp2 = difvec / r # Unit direction vector
          cos_i = abs(tmp2[0])
          cos_j = abs(tmp2[2])
          # Evaluate integrand
          intgndmat[k, l] = np.dot(cos_i, cos_j) / (np.pi * r**2)

      # Calculate element of F
      viewfactor = qw * sum(sum(intgndmat)) / d**2
      F[n**2 + i, 3 * n**2 + j] = viewfactor

print("Geometric view factors right->roof done (7/15)")

# From the left-hand-side wall (j) to the roof (i)
for i in range(0, n*n):
  for j in range(0, n*n):
    # Centerpoint of the current pixel in the roof
    pi = [Xmat[i, 1], Ymat[i, 1], Zmat[i, 1]]
    # Centerpoint of the current pixel in the left wall
    pj = [Xmat[j, 4], Ymat[j, 4], Zmat[j, 4]]
    # Distance between the points
    difvec0 = [i-j for i, j in zip(pi, pj)]
    r0 = np.linalg.norm(difvec0)

    # Check if the two pixels share an edge
    if r0 < np.sqrt(2) * d/2 + epsilon: # Edge shared
      # Calculate element of F analytically
      F[n**2 + i, 4 * n**2 + j] = shared_edge_F
    else: # Edge not shared: integrate for F using quadrature
      # Initalize matrix of integrand values at quadrature points
      intgndmat = np.zeros((qn**2, qn**2))
      # Double loop over four-dimensional quadrature
      for k in range(0, qn**2):
        for l in range(0, qn**2):
          # Quadrature point in the roof pixel
          qpi = pi[:]
          qpi[0] += q1[k%qn][k//qn]
          qpi[1] += q2[k%qn][k//qn]
          # Quadrature point in the left wall pixel
          qpj = pj[:]
          qpj[1] += q1[l%qn][l//qn]
          qpj[2] += q2[l%qn][l//qn]
          # Vector connecting the quadrature points
          difvec = [qpi[i]-qpj[i] for i in range(0, len(qpi))]
          r = np.linalg.norm(difvec)
          tmp2 = difvec / r # Unit direction vector
          cos_i = abs(tmp2[0])
          cos_j = abs(tmp2[2])
          # Evaluate integrand
          intgndmat[k, l] = np.dot(cos_i, cos_j) / (np.pi * r**2)

      # Calculate element of F
      viewfactor = qw * sum(sum(intgndmat)) / d**2
      F[n**2 + i, 4 * n**2 + j] = viewfactor

print("Geometric view factors left->roof done (8/15)")

# From the front wall (j) to the roof (i)
for i in range(0, n*n):
  for j in range(0, n*n):
    # Centerpoint of the current pixel in the roof
    pi = [Xmat[i, 1], Ymat[i, 1], Zmat[i, 1]]
    # Centerpoint of the current pixel in the front wall
    pj = [Xmat[j, 5], Ymat[j, 5], Zmat[j, 5]]
    # Distance between the points
    difvec0 = [i-j for i, j in zip(pi, pj)]
    r0 = np.linalg.norm(difvec0)

    # Check if the two pixels share an edge
    if r0 < np.sqrt(2) * d/2 + epsilon: # Edge shared
      # Calculate element of F analytically
      F[n**2 + i, 5 * n**2 + j] = shared_edge_F
    else: # Edge not shared: integrate for F using quadrature
      # Initalize matrix of integrand values at quadrature points
      intgndmat = np.zeros((qn**2, qn**2))
      # Double loop over four-dimensional quadrature
      for k in range(0, qn**2):
        for l in range(0, qn**2):
          # Quadrature point in the roof pixel
          qpi = pi[:]
          qpi[0] += q1[k%qn][k//qn]
          qpi[1] += q2[k%qn][k//qn]
          # Quadrature point in the front wall pixel
          qpj = pj[:]
          qpj[0] += q1[l%qn][l//qn]
          qpj[2] += q2[l%qn][l//qn]
          # Vector connecting the quadrature points
          difvec = [qpi[i]-qpj[i] for i in range(0, len(qpi))]
          r = np.linalg.norm(difvec)
          tmp2 = difvec / r # Unit direction vector
          cos_i = abs(tmp2[2])
          cos_j = abs(tmp2[1])
          # Evaluate integrand
          intgndmat[k, l] = np.dot(cos_i, cos_j) / (np.pi * r**2)

      # Calculate element of F
      viewfactor = qw * sum(sum(intgndmat)) / d**2
      F[n**2 + i, 5 * n**2 + j] = viewfactor

print("Geometric view factors front->roof done (9/15)")

# From the right-hand-side wall (j) to the floor (i)
for i in range(0, n*n):
  for j in range(0, n*n):
    # Centerpoint of the current pixel in the floor
    pi = [Xmat[i, 2], Ymat[i, 2], Zmat[i, 2]]
    # Centerpoint of the current pixel in the right wall
    pj = [Xmat[j, 3], Ymat[j, 3], Zmat[j, 3]]
    # Distance between the points
    difvec0 = [i-j for i, j in zip(pi, pj)]
    r0 = np.linalg.norm(difvec0)

    # Check if the two pixels share an edge
    if r0 < np.sqrt(2) * d/2 + epsilon: # Edge shared
      # Calculate element of F analytically
      F[2 * n**2 + i, 3 * n**2 + j] = shared_edge_F
    else: # Edge not shared: integrate for F using quadrature
      # Initalize matrix of integrand values at quadrature points
      intgndmat = np.zeros((qn**2, qn**2))
      # Double loop over four-dimensional quadrature
      for k in range(0, qn**2):
        for l in range(0, qn**2):
          # Quadrature point in the floor pixel
          qpi = pi[:]
          qpi[0] += q1[k%qn][k//qn]
          qpi[1] += q2[k%qn][k//qn]
          # Quadrature point in the right wall pixel
          qpj = pj[:]
          qpj[1] += q1[l%qn][l//qn]
          qpj[2] += q2[l%qn][l//qn]
          # Vector connecting the quadrature points
          difvec = [qpi[i]-qpj[i] for i in range(0, len(qpi))]
          r = np.linalg.norm(difvec)
          tmp2 = difvec / r # Unit direction vector
          cos_i = abs(tmp2[0])
          cos_j = abs(tmp2[2])
          # Evaluate integrand
          intgndmat[k, l] = np.dot(cos_i, cos_j) / (np.pi * r**2)

      # Calculate element of F
      viewfactor = qw * sum(sum(intgndmat)) / d**2
      F[2 * n**2 + i, 3 * n**2 + j] = viewfactor

print("Geometric view factors right->floor done (10/15)")

# From the left-hand-side wall (j) to the floor (i)
for i in range(0, n*n):
  for j in range(0, n*n):
    # Centerpoint of the current pixel in the floor
    pi = [Xmat[i, 2], Ymat[i, 2], Zmat[i, 2]]
    # Centerpoint of the current pixel in the left wall
    pj = [Xmat[j, 4], Ymat[j, 4], Zmat[j, 4]]
    # Distance between the points
    difvec0 = [i-j for i, j in zip(pi, pj)]
    r0 = np.linalg.norm(difvec0)

    # Check if the two pixels share an edge
    if r0 < np.sqrt(2) * d/2 + epsilon: # Edge shared
      # Calculate element of F analytically
      F[2 * n**2 + i, 4 * n**2 + j] = shared_edge_F
    else: # Edge not shared: integrate for F using quadrature
      # Initalize matrix of integrand values at quadrature points
      intgndmat = np.zeros((qn**2, qn**2))
      # Double loop over four-dimensional quadrature
      for k in range(0, qn**2):
        for l in range(0, qn**2):
          # Quadrature point in the floor pixel
          qpi = pi[:]
          qpi[0] += q1[k%qn][k//qn]
          qpi[1] += q2[k%qn][k//qn]
          # Quadrature point in the left wall pixel
          qpj = pj[:]
          qpj[1] += q1[l%qn][l//qn]
          qpj[2] += q2[l%qn][l//qn]
          # Vector connecting the quadrature points
          difvec = [qpi[i]-qpj[i] for i in range(0, len(qpi))]
          r = np.linalg.norm(difvec)
          tmp2 = difvec / r # Unit direction vector
          cos_i = abs(tmp2[2])
          cos_j = abs(tmp2[0])
          # Evaluate integrand
          intgndmat[k, l] = np.dot(cos_i, cos_j) / (np.pi * r**2)

      # Calculate element of F
      viewfactor = qw * sum(sum(intgndmat)) / d**2
      F[2 * n**2 + i, 4 * n**2 + j] = viewfactor

print("Geometric view factors left->floor done (11/15)")

# From the front wall (j) to the floor (i)
for i in range(0, n*n):
  for j in range(0, n*n):
    # Centerpoint of the current pixel in the floor
    pi = [Xmat[i, 2], Ymat[i, 2], Zmat[i, 2]]
    # Centerpoint of the current pixel in the front wall
    pj = [Xmat[j, 5], Ymat[j, 5], Zmat[j, 5]]
    # Distance between the points
    difvec0 = [i-j for i, j in zip(pi, pj)]
    r0 = np.linalg.norm(difvec0)

    # Check if the two pixels share an edge
    if r0 < np.sqrt(2) * d/2 + epsilon: # Edge shared
      # Calculate element of F analytically
      F[2 * n**2 + i, 5 * n**2 + j] = shared_edge_F
    else: # Edge not shared: integrate for F using quadrature
      # Initalize matrix of integrand values at quadrature points
      intgndmat = np.zeros((qn**2, qn**2))
      # Double loop over four-dimensional quadrature
      for k in range(0, qn**2):
        for l in range(0, qn**2):
          # Quadrature point in the floor pixel
          qpi = pi[:]
          qpi[0] += q1[k%qn][k//qn]
          qpi[1] += q2[k%qn][k//qn]
          # Quadrature point in the front wall pixel
          qpj = pj[:]
          qpj[0] += q1[l%qn][l//qn]
          qpj[2] += q2[l%qn][l//qn]
          # Vector connecting the quadrature points
          difvec = [qpi[i]-qpj[i] for i in range(0, len(qpi))]
          r = np.linalg.norm(difvec)
          tmp2 = difvec / r # Unit direction vector
          cos_i = abs(tmp2[2])
          cos_j = abs(tmp2[1])
          # Evaluate integrand
          intgndmat[k, l] = np.dot(cos_i, cos_j) / (np.pi * r**2)

      # Calculate element of F
      viewfactor = qw * sum(sum(intgndmat)) / d**2
      F[2 * n**2 + i, 5 * n**2 + j] = viewfactor

print("Geometric view factors front->floor done (12/15)")

##################################################################
# From the left-hand-side wall (j) to the right-hand-side-wall (i)
for i in range(0, n*n):
  for j in range(0, n*n):
    # Centerpoint of the current pixel in the right wall
    pi = [Xmat[i, 3], Ymat[i, 3], Zmat[i, 3]]
    # Centerpoint of the current pixel in the left wall
    pj = [Xmat[j, 4], Ymat[j, 4], Zmat[j, 4]]
    # Distance between the points
    difvec0 = [i-j for i, j in zip(pi, pj)]
    r0 = np.linalg.norm(difvec0)

    # Check if the two pixels share an edge
    if r0 < np.sqrt(2) * d/2 + epsilon: # Edge shared
      # Calculate element of F analytically
      F[3 * n**2 + i, 4 * n**2 + j] = shared_edge_F
    else: # Edge not shared: integrate for F using quadrature
      # Initalize matrix of integrand values at quadrature points
      intgndmat = np.zeros((qn**2, qn**2))
      # Double loop over four-dimensional quadrature
      for k in range(0, qn**2):
        for l in range(0, qn**2):
          # Quadrature point in the right wall pixel
          qpi = pi[:]
          qpi[1] += q1[k%qn][k//qn]
          qpi[2] += q2[k%qn][k//qn]
          # Quadrature point in the left wall pixel
          qpj = pj[:]
          qpj[1] += q1[l%qn][l//qn]
          qpj[2] += q2[l%qn][l//qn]
          # Vector connecting the quadrature points
          difvec = [qpi[i]-qpj[i] for i in range(0, len(qpi))]
          r = np.linalg.norm(difvec)
          tmp2 = difvec / r # Unit direction vector
          cos_i = abs(tmp2[0])
          cos_j = abs(tmp2[0])
          # Evaluate integrand
          intgndmat[k, l] = np.dot(cos_i, cos_j) / (np.pi * r**2)

      # Calculate element of F
      viewfactor = qw * sum(sum(intgndmat)) / d**2
      F[3 * n**2 + i, 4 * n**2 + j] = viewfactor

print("Geometric view factors left->right done (13/15)")

# From the front wall (j) to the right-hand-side-wall (i)
for i in range(0, n*n):
  for j in range(0, n*n):
    # Centerpoint of the current pixel in the right wall
    pi = [Xmat[i, 3], Ymat[i, 3], Zmat[i, 3]]
    # Centerpoint of the current pixel in the front wall
    pj = [Xmat[j, 5], Ymat[j, 5], Zmat[j, 5]]
    # Distance between the points
    difvec0 = [i-j for i, j in zip(pi, pj)]
    r0 = np.linalg.norm(difvec0)

    # Check if the two pixels share an edge
    if r0 < np.sqrt(2) * d/2 + epsilon: # Edge shared
      # Calculate element of F analytically
      F[3 * n**2 + i, 5 * n**2 + j] = shared_edge_F
    else: # Edge not shared: integrate for F using quadrature
      # Initalize matrix of integrand values at quadrature points
      intgndmat = np.zeros((qn**2, qn**2))
      # Double loop over four-dimensional quadrature
      for k in range(0, qn**2):
        for l in range(0, qn**2):
          # Quadrature point in the right wall pixel
          qpi = pi[:]
          qpi[1] += q1[k%qn][k//qn]
          qpi[2] += q2[k%qn][k//qn]
          # Quadrature point in the front wall pixel
          qpj = pj[:]
          qpj[0] += q1[l%qn][l//qn]
          qpj[2] += q2[l%qn][l//qn]
          # Vector connecting the quadrature points
          difvec = [qpi[i]-qpj[i] for i in range(0, len(qpi))]
          r = np.linalg.norm(difvec)
          tmp2 = difvec / r # Unit direction vector
          cos_i = abs(tmp2[0])
          cos_j = abs(tmp2[1])
          # Evaluate integrand
          intgndmat[k, l] = np.dot(cos_i, cos_j) / (np.pi * r**2)

      # Calculate element of F
      viewfactor = qw * sum(sum(intgndmat)) / d**2
      F[3 * n**2 + i, 5 * n**2 + j] = viewfactor

print("Geometric view factors front->right done (14/15)")

# From the front wall (j) to the left-hand-side-wall (i)
for i in range(0, n*n):
  for j in range(0, n*n):
    # Centerpoint of the current pixel in the left wall
    pi = [Xmat[i, 4], Ymat[i, 4], Zmat[i, 4]]
    # Centerpoint of the current pixel in the front wall
    pj = [Xmat[j, 5], Ymat[j, 5], Zmat[j, 5]]
    # Distance between the points
    difvec0 = [i-j for i, j in zip(pi, pj)]
    r0 = np.linalg.norm(difvec0)

    # Check if the two pixels share an edge
    if r0 < np.sqrt(2) * d/2 + epsilon: # Edge shared
      # Calculate element of F analytically
      F[4 * n**2 + i, 5 * n**2 + j] = shared_edge_F
    else: # Edge not shared: integrate for F using quadrature
      # Initalize matrix of integrand values at quadrature points
      intgndmat = np.zeros((qn**2, qn**2))
      # Double loop over four-dimensional quadrature
      for k in range(0, qn**2):
        for l in range(0, qn**2):
          # Quadrature point in the left wall pixel
          qpi = pi[:]
          qpi[1] += q1[k%qn][k//qn]
          qpi[2] += q2[k%qn][k//qn]
          # Quadrature point in the front wall pixel
          qpj = pj[:]
          qpj[0] += q1[l%qn][l//qn]
          qpj[2] += q2[l%qn][l//qn]
          # Vector connecting the quadrature points
          difvec = [qpi[i]-qpj[i] for i in range(0, len(qpi))]
          r = np.linalg.norm(difvec)
          tmp2 = difvec / r # Unit direction vector
          cos_i = abs(tmp2[0])
          cos_j = abs(tmp2[1])
          # Evaluate integrand
          intgndmat[k, l] = np.dot(cos_i, cos_j) / (np.pi * r**2)

      # Calculate element of F
      viewfactor = qw * sum(sum(intgndmat)) / d**2
      F[4 * n**2 + i, 5 * n**2 + j] = viewfactor

print("Geometric view factors front->left done (15/15)")

end = time.time()
print("View factors calculated in", end-start, "seconds")
# Use symmetry to finish the construction of F.
# F is symmetric since all the pixels in our model have equal size.

F = F + np.transpose(F)

# Check the matrix f, the row sums should all be one
print("Check values: all should ideally be one")
print(sum(F))

# Save matrix to disc
print("Saving to file...")
with open("../data/F_emptyroom", 'wb') as f:
  np.save(f, F)
  np.save(f, n)
  np.save(f, qn)
  np.save(f, d)
  np.save(f, Xmat)
  np.save(f, Ymat)
  np.save(f, Zmat)
print("Data saved!")


# functions for caluate the lan radius im meters

import numpy as np

# ym_per_pix = 30/720
# xm_per_pix = 3.7/700

Y_MAX = 720 # this is the image height
X_MAX = 1280 # this is the image width

QUADRATIC_COEFF = 3e-4 # this is for the test data genrate

def generate_data():
	'''
	Generates fake data to use for calculating lane curvature
	only for testing
	'''
	# Set random seed number
	# Comment this out if want to see result s on different
	np.random.seed(0)
	# Generate some fake data to repreent lane-line pixels
	ploty = np.linspace(0, Y_MAX-1, num=Y_MAX) # to cover y range
	quadratic_coeff = QUADRATIC_COEFF # arbitray quadratic coefficient

	# For each y postion generate random x postion with +/- 50
	# of the line base postion in each case (x=200 for left, x =900 for right)

	# Try to use in line function lambad to instead of list compression, not work, reason not found 2018/9/30 chengzhong.shen
	# line_fun = lambad y: (y**2)*quadratic_coeff + np.random.randint(-50, high=51)
	# leftx = 200 + line_fun(ploty)
	# rightx = 900 + line_fun(ploty)

	leftx = np.array([200 + (y**2)*quadratic_coeff + np.random.randint(-50, high=51)
						for y in ploty])
	rightx = np.array([900 + (y**2)*quadratic_coeff + np.random.randint(-50, high=51)
						for y in ploty])

	leftx = leftx[::-1]	# revers to match top-to-bottom in y
	rightx = rightx[::-1]	# revers to match top-to-bottom in y

	return leftx, ploty, rightx, ploty

def measure_curv(leftx, lefty, rightx, righty, ym_per_pix=30/720, xm_per_pix=3.7/700):
	'''
	Calcualtes the curvature of polynomial functions in meters and the offset from the lancenter
	'''
	# Transform pixel to meters
	leftx = leftx * xm_per_pix
	lefty = lefty * ym_per_pix
	rightx = rightx * xm_per_pix
	righty = righty * ym_per_pix

	# fit the polynomial
	left_fit_cr = np.polyfit(lefty, leftx, 2)
	right_fit_cr = np.polyfit(righty, rightx, 2)

	# Define y-value where we want radius of curvature
	# choose the maximum y-value
	y_eval = Y_MAX * ym_per_pix

	# Implement the caculation of R_curve
	# Caluate the radius R = (1+(2Ay+B)^2)^3/2 / (|2A|)
	radius_fun = lambda A, B, y: (1+(2*A*y+B)**2)**(3/2) / abs(2*A)

	left_curverad = radius_fun(left_fit_cr[0], left_fit_cr[1], y_eval)
	right_curverad = radius_fun(right_fit_cr[0], right_fit_cr[1], y_eval)

	return left_curverad, right_curverad

def measure_offset(leftx, lefty, rightx, righty, ym_per_pix=30/720, xm_per_pix=3.7/700):
	'''
	calculate the the offest from lane center
	'''
	# HYPOTHESIS : the camera is mounted at the center of the car
	# the offset of the lane center from the center of the image is
	# distance from the center of lane

	# Transform pixel to meters
	leftx = leftx * xm_per_pix
	lefty = lefty * ym_per_pix
	rightx = rightx * xm_per_pix
	righty = righty * ym_per_pix

	# fit the polynomial
	left_fit_cr = np.polyfit(lefty, leftx, 2)
	right_fit_cr = np.polyfit(righty, rightx, 2)

	# Define y-value where we want radius of curvature
	# choose the maximum y-value
	y_eval = Y_MAX * ym_per_pix

	left_point = np.poly1d(left_fit_cr)(y_eval)
	right_point = np.poly1d(right_fit_cr)(y_eval)

	lane_center = (left_point + right_point) / 2
	image_center = X_MAX * xm_per_pix / 2

	offset = lane_center - image_center

	return offset

def test():
	leftx, lefty, rightx, righty = generate_data()
	left_curverad, right_curverad = measure_curv(leftx, lefty, rightx, righty)
	offset = measure_offset(leftx, lefty, rightx, righty)

	print(left_curverad, 'm', right_curverad, 'm')
	print("offset is: ", offset, "m")

if __name__ == '__main__':
	test()

# #######################################################################################
# # Calculate the radius of curvature in meters for both lane line
# # Should see values of 533.75 and 648.16 here, if using
# # the default `generate_data` function with given seed number
# # the offset should be 1280/2 - (200+900)/2 = 90 90*3.7/700=-0.4757(-0.4925)
# ########################################################################################


## 3. Finding Correlations With the r Value ##

correlations = combined.corr()

correlations = correlations['sat_score']
print(correlations)

## 5. Plotting Enrollment With the Plot() Accessor ##

import matplotlib.pyplot as plt

plt.scatter(x= combined['total_enrollment'], y= combined['sat_score'])
plt.xlabel('total_enrollment')
plt.ylabel('sat_score')
plt.show()

## 6. Exploring Schools With Low SAT Scores and Enrollment ##

low_enrollment = combined[combined['total_enrollment']<1000]

low_enrollment = low_enrollment[combined['sat_score'] <1000]

print(low_enrollment['School Name'])

## 7. Plotting Language Learning Percentage ##

plt.scatter(x= combined['ell_percent'] , y=combined['sat_score'])
plt.xlabel('ell_percent')
plt.ylabel('sat_score')
plt.show()

## 8. Calculating District-Level Statistics ##

import numpy as np

districts = combined.groupby('school_dist').agg(np.mean).reset_index()

print(districts)

2. Intermediate Python and Pandas/data-exploration/Data Cleaning Walkthrough_ Analyzing and Visualizing the Data-138.py


import numpy as np

def vectorize(array, m=1):
  """
  Argument:
  image -- a numpy array of shape (a,b, ..., z)

  Returns:
  v -- a vector of shape (a*b* ... *z, 1)
  """

  assert(type(array) is np.ndarray)
  return array.reshape(m, -1).T

def normalize_rows(X):
  """
  Argument:
  X -- A numpy matrix of shape (n, m)

  Returns:
  The numpy matrix X normalized (by row) numpy matrix
  """
  # Each row of X is divided by the sqare root of the squared sums of its elements
  # ||[a,b,c]|| == np.sqrt(a**2 + b**2 + c**2)
  X_norm = np.linalg.norm(X, axis=1, ord=2, keepdims=True)

  # Replace zeros to avoid division error
  X_norm[X_norm == 0] = 1
  return X / X_norm

def softmax(X):
  """
  Calculates the softmax for each row of the input x.

  Your code should work for a row vector and also for matrices of shape (n, m).

  Argument:
  x -- A numpy matrix of shape (n,m)

  Returns:
  s -- A numpy matrix equal to the softmax of x, of shape (n,m)
  """

  # Array of elements e raised to power of corresponding element in X
  X_exp = np.exp(X)

  # A vector of the sum of each row of x_exp
  X_sum = np.sum(X_exp, axis=1, keepdims=True)

  return X_exp / X_sum

def loss(yhat, y, L=2):
  assert(L in [1,2])
  if L == 1:
    return np.sum(np.absolute(y - yhat))
  elif L == 2:
    return np.sum(np.power(y - yhat, 2))


import warnings

import numpy as np
from mnist import MNIST
from skimage.feature import hog
from sklearn.metrics import accuracy_score
from sklearn.svm import LinearSVC
import matplotlib.pyplot as plt

import time

warnings.filterwarnings("ignore")

# --------------------------------------------------------

# Global Variables

training_size = 5000
testing_size = 10000

alpha = 0.01
iterations = 2000  # epochs for batch mode gradient descent

gradient_curve = True
epoch_curve = True

"""Since we are not sure that the dataset is linearly separable or not, therefore to avoid infinite looping in weights training
we iterate over the training set epoch times for tuning weights"""
epochs = 15  # epochs for perceptron rule (currently stochastic mode is used)

labels = 10

# --------------------------------------------------------

"""Predicts the labels by choosing the label of the classifier with highest confidence(probability)"""

def predict(all_weights, test_images, learning_algo):
    test_images = np.hstack((np.ones((testing_size, 1)), test_images))

    predicted_labels = np.dot(all_weights, test_images.T)

    # sigmoid activation function
    if learning_algo == 1:
        predicted_labels = sigmoid(predicted_labels)

    # signum activation function
    elif learning_algo == 2:
        predicted_labels = signum(predicted_labels)

    predicted_labels = np.argmax(predicted_labels, axis=0)

    return predicted_labels.T

# --------------------------------------------------------

"""Activation Functions"""

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def signum(x):
    x[x > 0] = 1
    x[x <= 0] = -1

    return x

# --------------------------------------------------------
def learn_using_perceptron_rule_with_signum_function(train_images, train_labels, weights):
    epochs_values = []
    error_values = []

    for k in range(epochs):
        missclassified = 0

        for t, l in zip(train_images, train_labels):
            h = np.dot(t, weights)

            h = signum(h)

            if h[0] != l[0]:
                missclassified += 1

            gradient = t * (h - l)

            # reshape gradient
            gradient = gradient.reshape(gradient.shape[0], 1)

            weights = weights - (gradient * alpha)

        error_values.append(missclassified / training_size)
        epochs_values.append(k)

    global epoch_curve

    if epoch_curve:
        """"Plotting Epochs vs Training Error"""

        plt.ylabel('Training Error', fontsize=14)
        plt.xlabel('Epoch', fontsize=14)
        plt.title("Epochs vs Training Error for Perceptron Rule with Signum Func.", fontsize=16, color='green')
        plt.plot(epochs_values, error_values)
        figure = plt.gcf()  # get current figure
        figure.set_size_inches(13, 7)

        plt.savefig("Epochs vs Training Error for Perceptron Rule with Signum Func..png", dpi=300)
        plt.clf()

        epoch_curve = False

    return weights

def learn_using_gradient_descent_with_sigmoid_function(train_images, train_labels, weights):
    iters = []
    error_values = []

    for i in range(iterations):
        h = np.dot(train_images, weights)

        h = sigmoid(h)

        error_value = (np.dot(-1 * train_labels.T, np.log(h)) - np.dot((1 - train_labels).T,
                                                                       np.log(1 - h))) / training_size

        gradient = np.dot(train_images.T, h - train_labels) / training_size

        weights = weights - (gradient * alpha)

        iters.append(i)
        error_values.append(error_value[0, 0])

    global gradient_curve

    if gradient_curve:
        """"Plotting Iterations vs Training Error"""

        plt.ylabel('Training Error', fontsize=14)
        plt.xlabel('Iteration', fontsize=14)
        plt.title("Iterations vs Training Error for Gradient Descent with Sigmoid Func.", fontsize=16, color='green')
        plt.plot(iters, error_values)
        figure = plt.gcf()  # get current figure
        figure.set_size_inches(13, 7)

        plt.savefig("Iterations vs Training Error for Gradient Descent with Sigmoid Func.png", dpi=300)
        plt.clf()

        gradient_curve = False

    return weights

# --------------------------------------------------------

"""Find optimal weights for each logistic binary classifier"""

def train(train_images, train_labels, learning_algo):
    # add 1's as x0
    train_images = np.hstack((np.ones((training_size, 1)), train_images))

    # add w0 as 0 initially
    all_weights = np.zeros((labels, train_images.shape[1]))

    train_labels = train_labels.reshape((training_size, 1))

    train_labels_copy = np.copy(train_labels)

    for j in range(labels):

        print("Training Classifier: ", j+1)

        train_labels = np.copy(train_labels_copy)

        # initialize all weights to zero
        weights = np.zeros((train_images.shape[1], 1))

        if learning_algo == 1:
            for k in range(training_size):
                if train_labels[k, 0] == j:
                    train_labels[k, 0] = 1
                else:
                    train_labels[k, 0] = 0

            weights = learn_using_gradient_descent_with_sigmoid_function(train_images, train_labels, weights)

        elif learning_algo == 2:
            for k in range(training_size):
                if train_labels[k, 0] == j:
                    train_labels[k, 0] = 1
                else:
                    train_labels[k, 0] = -1

            weights = learn_using_perceptron_rule_with_signum_function(train_images, train_labels, weights)

        all_weights[j, :] = weights.T

    return all_weights

# --------------------------------------------------------

def run_experiment(train_images, train_labels, test_images, test_labels, learning_algo):
    if learning_algo == 1:
        s = "Gradient Descent with Sigmoid Activation Function"
    elif learning_algo == 2:
        s = "Perceptron Learning Rule for Thresholded Unit"

    print("------------------------------------------------------------------------------------")
    print("Running Experiment using %s" % s)
    print("------------------------------------------------------------------------------------")

    print("Training ...")
    start_time = time.clock()
    all_weights = train(train_images, train_labels, learning_algo)
    print("Training Time: %.2f seconds" % (time.clock() - start_time))
    print("Weights Learned!")

    print("Classifying Test Images ...")
    start_time = time.clock()
    predicted_labels = predict(all_weights, test_images, learning_algo)
    print("Prediction Time: %.2f seconds" % (time.clock() - start_time))

    print("Test Images Classified!")

    accuracy = accuracy_score(test_labels, predicted_labels) * 100

    print("Accuracy: %f" % accuracy, "%")
    print("---------------------\n")

# --------------------------------------------------------

def calculate_hog_features(images):
    list_hog_fd = [hog(t.reshape((28, 28)), orientations=9, pixels_per_cell=(14, 14), cells_per_block=(1, 1),
                       visualise=False) for t in images]

    return np.array(list_hog_fd, dtype=np.float64)

def run_svm_experiment(train_images, train_labels, test_images, test_labels):
    s = "SVM with HOG based feature descriptor"

    print("------------------------------------------------------------------------------------")
    print("Running Experiment using %s" % s)
    print("------------------------------------------------------------------------------------")

    print("Calculating HOG based Feature Descriptor")
    start_time = time.clock()
    train_hog_features = calculate_hog_features(train_images)
    print("Feature Descriptor calculated !")

    print("Training ...")
    clf = LinearSVC()
    clf.fit(train_hog_features, train_labels)
    print("Training Time: %.2f seconds" % (time.clock() - start_time))
    print("Training Done!")

    print("Classifying Test Images ...")
    start_time = time.clock()
    test_hog_features = calculate_hog_features(test_images)
    predicted_labels = clf.predict(test_hog_features)
    print("Prediction Time: %.2f seconds" % (time.clock() - start_time))

    print("Test Images Classified!")
    accuracy = accuracy_score(test_labels, predicted_labels) * 100

    print("Accuracy: %f" % accuracy, "%")
    print("---------------------\n")

# --------------------------------------------------------

def main():
    # load data
    data = MNIST('samples')

    train_images, train_labels = data.load_training()
    test_images, test_labels = data.load_testing()

    train_images = np.array(train_images[:training_size])
    train_labels = np.array(train_labels[:training_size], dtype=np.int32)

    test_images = np.array(test_images[:testing_size])
    test_labels = np.array(test_labels[:testing_size], dtype=np.int32)

    """Rescaling Data"""
    train_images = train_images / 255
    test_images = test_images / 255

    """ run one vs all with learning a perceptron with sigmoid activation function using gradient descent """
    run_experiment(train_images, train_labels, test_images, test_labels, 1)

    """ run one vs all with learning a perceptron with perceptron learning rule """
    run_experiment(train_images, train_labels, test_images, test_labels, 2)

    """ run svm using HOG as feature descriptor """
    run_svm_experiment(train_images, train_labels, test_images, test_labels)

# --------------------------------------------------------

# get things rolling
main()


import numpy as np
import math as m
import time
import matplotlib.pyplot as plt
import unittest
import random

###*************************************
###*************************************
###*************************************

###******************/PART II : Conjugate gradient method

"""QUESTION 3"""
"""Resolution of the linear system Ax=b by the conjugate gradient method where A is a symmetric positive definite matrix and b a vector (without preconditioning)"""

def conjgrad(A,b,x) :
    r=b-np.dot(A,x)
    p=r
    rsOld= float(np.dot(np.transpose(r),r))
    for i in range(1,100001):
        Ap=np.dot(A,p)
        n = float(np.dot(np.transpose(p),Ap))
        alpha=rsOld/n
        x=x+ alpha*p
        r=r- alpha*Ap
        rsNew=float(np.dot(np.transpose(r),r))
        if m.sqrt(rsNew) < 1e-10 :
            break
        p=r+rsNew/rsOld*p
        rsOld=rsNew
    return x                      #x is the solution found by Conjugate gradient Method

def MdpGenerator(size) :          #this auxiliary function generates, for a given size, a matrix A
                                  #symmetric definite positive
                                  #T is an upper trinagular matrix with positive diagonal coefficients
                                  #The diagonal coefficients of A are generated randomly between 5 and 10
    T=np.zeros((size,size))
    for i in range(size):
        for j in range(size):
            if j>=i:
                T[i,j] = random.randint(5,10)
            else:
                T[i,j] = 0
    A = np.dot(T,T.transpose())
    return A

"""QUESTION 4"""
"""Resolution of the linear system Ax=b by the conjugate gradient method where A is a symmetric positive definite matrix and b a vector (with preconditioning)"""

def somme(T, i, j):
    return sum(T[i][k] * T[j][k] for k in range(j))

def facto_dense_inc(A):
    (n, n1) = A.shape
    T = np.zeros((n,n))
    for i in range(n):
        for j in range(i + 1):
            if A[i][j] != 0:
                if i==j:
                    T[i][j] = m.sqrt((A[i][i] - somme(T, i, j)))
                else:
                    T[i][j] = (A[i][j] - somme(T, i, j)) / T[j][j]
    return T

def preconditioner(A):
    T= facto_dense_inc(A)
    return np.dot(T,np.transpose(T))

def PreconditionedConjgrad(A,b,x):
    r=b-np.dot(A,x)
    M=preconditioner(A)
    z=np.dot(np.linalg.inv(M),r)
    p=z
    for k in range(1,100001):
        alpha=(np.dot(np.transpose(r),z))/(np.dot(np.transpose(p),np.dot(A,p)))
        x=x+ alpha*p
        r2=r-alpha*(np.dot(A,p))
        rsNew=float(np.dot(np.transpose(r2),r2))
        if m.sqrt(rsNew) < 1e-10 :
            break
        z2=np.dot(np.linalg.inv(M),r2)
        b=(np.dot(np.transpose(z2),r2))/(np.dot(np.transpose(z),r))
        z=z2
        r=r2
    return x

##****************TEST: Conjugate gradient method

class Test_gradient(unittest.TestCase):
    def test_conjgrad(self):
        A= np.array([[4,1],[1,3]])
        b= np.array([1,2])
        x= np.array([2,1])
        expected= np.array([0.0909,0.6363])
        result= conjgrad(A,b,x)
        for i in range(len(A)):
                self.assertAlmostEqual(result[i],expected[i],3)

    def test_conjgrad_precond(self):
        A= np.array([[4,1],[1,3]])
        b= np.array([1,2])
        x= np.array([2,1])
        expected= np.array([0.0909,0.6363])
        result= PreconditionedConjgrad(A,b,x)
        print()
        for i in range(len(A)):
            self.assertAlmostEqual(result[i],expected[i],3)

def test_conjgrad_linalg(size):     #In this test we compare the relative error generated by np.linalg and conjgrad()
                                    #The size of the matrix A varies from 25 to 'size-1'
                                    ##relative error = the difference in magnitudes between the expected solution and
                                    ##the solution found by the algorithm
    R1=[]
    R2=[]
    iter=[]
    for i in range(25,size):
        iter+=[i]
        A=MdpGenerator(i)
        Xs = np.random.rand(i,1)
        x_conjgrad=np.zeros((i,1))
        b=np.dot(A,Xs)
        x_conjgrad=conjgrad(A,b,x_conjgrad)
        x_linalg=np.linalg.solve(A,b)
        R1+=[abs(np.linalg.norm(x_conjgrad)-np.linalg.norm(Xs))]
        R2+=[abs(np.linalg.norm(x_linalg)-np.linalg.norm(Xs))]
    plt.plot(iter, R1, label="Conjgrad Method")
    plt.plot(iter, R2, label="Linalg from numpy")
    plt.xlabel('Size of matrix')
    plt.ylabel('Relative error')
    plt.title('Relative error in the resolution of Ax=b')
    plt.legend()
    plt.show()
    #plt.savefig('relative_error.png')

test_conjgrad_linalg(50)                              #we run the test for size=50

###*************************************
###*************************************
###*************************************

if __name__ == '__main__':
    unittest.main(Test_gradient(), verbosity = 2)