import numpy as np

from utils_2nd_version import inc_index, dec_index, \
                              constraint_inequalities_funcs_generator

def gradient_approximation(f,x,h=1e-8):
    '''
    Numerical approximation of gradient for function f using forward differences.
    Args:
        f (lambda expression): definition of function f.
        x (array): numpy array that holds values where gradient will be computed.
        h (float): step size for forward differences, tipically h=1e-8
    Returns:
        gf (array): numerical approximation to gradient of f.
    '''
    n = x.size
    gf = np.zeros(n)
    f_x = f(x)
    for i in np.arange(n):
        inc_index(x,i,h)
        gf[i] = f(x) - f_x
        dec_index(x,i,h)
    return gf/h
def Hessian_approximation(f,x,h=1e-6):
    '''
    Numerical approximation of Hessian for function f using forward differences.
    Args:
        f (lambda expression): definition of function f.
        x (array): numpy array that holds values where Hessian will be computed.
        h (float): step size for forward differences, tipically h=1e-6
    Returns:
        Hf (array): numerical approximation to Hessian of f.
    '''
    n = x.size
    Hf = np.zeros((n,n))
    f_x = f(x)
    for i in np.arange(n):
        inc_index(x,i,h)
        f_x_inc_in_i = f(x)
        for j in np.arange(i,n):
            inc_index(x,j,h)
            f_x_inc_in_i_j = f(x)
            dec_index(x,i,h)
            f_x_inc_in_j = f(x)
            dif = f_x_inc_in_i_j-f_x_inc_in_i-f_x_inc_in_j+f_x
            Hf[i,j] = dif
            if j != i:
                Hf[j,i] = dif
            dec_index(x,j,h)
            inc_index(x,i,h)
        dec_index(x,i,h)
    return Hf/h**2
def numerical_differentiation_of_logarithmic_barrier(f, x, t_path, constraint_inequalities,
                                                     infeasible = None):
    '''
    First and second derivative of logarithmic barrier function approximation
    via finite differences
    '''
    sum_gf_const = 0
    sum_Hf_const = 0
    for const in constraint_inequalities_funcs_generator(constraint_inequalities):
        const_eval = const(x)
        gf_const_eval = gradient_approximation(const, x)
        Hf_const_eval = Hessian_approximation(const, x)
        sum_gf_const += gf_const_eval/(-const_eval) if not infeasible else \
                        gf_const_eval/(infeasible - const_eval)
        if not infeasible:
            sum_Hf_const += np.outer(gf_const_eval,gf_const_eval)/const_eval**2 - Hf_const_eval/const_eval
        else:
            sum_Hf_const += np.outer(gf_const_eval,gf_const_eval)/(infeasible - const_eval)**2 + \
                            Hf_const_eval/(infeasible - const_eval)
    gf_eval = gradient_approximation(f,x)
    Hf_eval = Hessian_approximation(f,x)
    return {'gradient': t_path*gf_eval + sum_gf_const,
            'Hessian': t_path*Hf_eval + sum_Hf_const
           }


import sympy
from typing import Tuple

def sym_skew(vec: sympy.Matrix) -> sympy.Matrix:
    vec = vec.reshape(1, 3)
    return sympy.Matrix([
        [0, -vec[2], vec[1]],
        [vec[2], 0, -vec[0]],
        [-vec[1], vec[0], 0]
    ])

def sym_vex(skewmat: sympy.Matrix) -> sympy.Matrix:
    return sympy.Matrix([skewmat[2, 1], skewmat[0, 2], skewmat[1, 0]])

def sym_no_rot() -> sympy.Matrix:
    return sympy.Matrix([
        [1, 0, 0],
        [0, 1, 0],
        [0, 0, 1]
    ])

def sym_hom_no_rot() -> sympy.Matrix:
    return sympy.Matrix([
        [1, 0, 0, 0],
        [0, 1, 0, 0],
        [0, 0, 1, 0],
        [0, 0, 0, 1]
    ])

def sym_rot(axis: sympy.Matrix, angle: sympy.NumberSymbol) -> sympy.Matrix:
    if axis.shape != (1, 3) and axis.shape != (3, 1):
        return sympy.Matrix([
        [1, 0, 0],
        [0, 1, 0],
        [0, 0, 1]
    ])
    axis = axis.reshape(3, 1)
    axis = sympy.simplify(axis.normalized())
    c = sympy.cos(angle)
    s = sympy.sin(angle)
    axis2 = sympy.simplify(axis * axis.T)
    axis = axis.reshape(1, 3)
    skewaxis = sympy.Matrix([
        [0, -axis[2], axis[1]],
        [axis[2], 0, -axis[0]],
        [-axis[1], axis[0], 0]
    ])
    return sympy.simplify(axis2 + (sympy.Matrix([
        [1, 0, 0],
        [0, 1, 0],
        [0, 0, 1]
    ]) - axis2) * c + skewaxis * s)

def sym_rotx(angle: sympy.NumberSymbol) -> sympy.Matrix:
    c = sympy.cos(angle)
    s = sympy.sin(angle)
    return sympy.Matrix([
        [1, 0, 0],
        [0, c, -s],
        [0, s, c]
    ])

def sym_roty(angle: sympy.NumberSymbol) -> sympy.Matrix:
    c = sympy.cos(angle)
    s = sympy.sin(angle)
    return sympy.Matrix([
        [c, 0, s],
        [0, 1, 0],
        [-s, 0, c]
    ])

def sym_rotz(angle: sympy.NumberSymbol) -> sympy.Matrix:
    c = sympy.cos(angle)
    s = sympy.sin(angle)
    return sympy.Matrix([
        [c, -s, 0],
        [s, c, 0],
        [0, 0, 1]
    ])

def sym_rot_eulzxz(anglez: sympy.NumberSymbol, anglex: sympy.NumberSymbol, anglez1: sympy.NumberSymbol) \
    -> sympy.Matrix:
    cz = sympy.cos(anglez)
    sz = sympy.sin(anglez)
    cx = sympy.cos(anglex)
    sx = sympy.sin(anglex)
    cz1 = sympy.cos(anglez1)
    sz1 = sympy.sin(anglez1)
    rotzmat = sympy.Matrix([
        [cz, -sz, 0],
        [sz, cz, 0],
        [0, 0, 1]
    ])
    rotxmat = sympy.Matrix([
        [1, 0, 0],
        [0, cx, -sx],
        [0, sx, cx]
    ])
    rotz1mat = sympy.Matrix([
        [cz1, -sz1, 0],
        [sz1, cz1, 0],
        [0, 0, 1]
    ])
    return sympy.simplify(rotzmat * rotxmat * rotz1mat)

def sym_rot_rpyxyz(anglex: sympy.NumberSymbol, angley: sympy.NumberSymbol, anglez: sympy.NumberSymbol) \
    -> sympy.Matrix:
    cx = sympy.cos(anglex)
    sx = sympy.sin(anglex)
    cy = sympy.cos(angley)
    sy = sympy.sin(angley)
    cz = sympy.cos(anglez)
    sz = sympy.sin(anglez)
    rotxmat = sympy.Matrix([
        [1, 0, 0],
        [0, cx, -sx],
        [0, sx, cx]
    ])
    rotymat = sympy.Matrix([
        [cy, 0, sy],
        [0, 1, 0],
        [-sy, 0, cy]
    ])
    rotzmat = sympy.Matrix([
        [cz, -sz, 0],
        [sz, cz, 0],
        [0, 0, 1]
    ])
    return sympy.simplify(rotzmat * rotymat * rotxmat)

def sym_rot_inv(rotmat: sympy.Matrix) -> sympy.Matrix:
    return sympy.transpose(rotmat)

def sym_hom_rotx(angle: sympy.NumberSymbol) -> sympy.Matrix:
    c = sympy.cos(angle)
    s = sympy.sin(angle)
    return sympy.Matrix([
        [1, 0, 0, 0],
        [0, c, -s, 0],
        [0, s, c, 0],
        [0, 0, 0, 1]
    ])

def sym_hom_roty(angle: sympy.NumberSymbol) -> sympy.Matrix:
    c = sympy.cos(angle)
    s = sympy.sin(angle)
    return sympy.Matrix([
        [c, 0, s, 0],
        [0, 1, 0, 0],
        [-s, 0, c, 0],
        [0, 0, 0, 1]
    ])

def sym_hom_rotz(angle: sympy.NumberSymbol) -> sympy.Matrix:
    c = sympy.cos(angle)
    s = sympy.sin(angle)
    return sympy.Matrix([
        [c, -s, 0, 0],
        [s, c, 0, 0],
        [0, 0, 1, 0],
        [0, 0, 0, 1]
    ])

def sym_hom_rot_inv(homrotmat: sympy.Matrix) -> sympy.Matrix:
    rot_inv_mat = sympy.transpose(homrotmat[0:3, 0:3])
    return sympy.Matrix([
        [rot_inv_mat[0, 0], rot_inv_mat[0, 1], rot_inv_mat[0, 2], 0],
        [rot_inv_mat[1, 0], rot_inv_mat[1, 1], rot_inv_mat[1, 2], 0],
        [rot_inv_mat[2, 0], rot_inv_mat[2, 1], rot_inv_mat[2, 2], 0],
        [0, 0, 0, 1]
    ])

def sym_no_transl() -> sympy.Matrix:
    return sympy.Matrix([[0, 0, 0]])

def sym_transl(tx: sympy.NumberSymbol, ty: sympy.NumberSymbol, tz: sympy.NumberSymbol) -> sympy.Matrix:
    return sympy.Matrix([[tx, ty, tz]])

def sym_no_coltransl() -> sympy.Matrix:
    return sympy.Matrix([0, 0, 0])

def sym_coltransl(tx: sympy.NumberSymbol, ty: sympy.NumberSymbol, tz: sympy.NumberSymbol) -> sympy.Matrix:
    return sympy.Matrix([tx, ty, tz])

def sym_transl_inv(translvec: sympy.Matrix) -> sympy.Matrix:
    return -translvec

def sym_hom_transl(tx: sympy.NumberSymbol, ty: sympy.NumberSymbol, tz: sympy.NumberSymbol) -> sympy.Matrix:
    return sympy.Matrix([
        [1, 0, 0, tx],
        [0, 1, 0, ty],
        [0, 0, 1, tz],
        [0, 0, 0, 1]
    ])

def sym_hom_transl_inv(homtranslmat: sympy.Matrix) -> sympy.Matrix:
    transl_inv_vec = -homtranslmat[0:3, 3:4]
    return sympy.Matrix([
        [1, 0, 0, transl_inv_vec[0, 0]],
        [0, 1, 0, transl_inv_vec[1, 0]],
        [0, 0, 1, transl_inv_vec[2, 0]],
        [0, 0, 0, 1]
    ])

def sym_DH_transformation(alpha: sympy.NumberSymbol, a: sympy.NumberSymbol,
                          d: sympy.NumberSymbol, theta: sympy.NumberSymbol) -> sympy.Matrix:
    calpha = sympy.cos(alpha)
    salpha = sympy.sin(alpha)
    ctheta = sympy.cos(theta)
    stheta = sympy.sin(theta)
    transf1 = sympy.Matrix([
        [ctheta, -stheta, 0, 0],
        [stheta, ctheta, 0, 0],
        [0, 0, 1, d],
        [0, 0, 0, 1]
    ])
    transf2 = sympy.Matrix([
        [1, 0, 0, a],
        [0, calpha, -salpha, 0],
        [0, salpha, calpha, 0],
        [0, 0, 0, 1]
    ])
    return sympy.simplify(transf1 * transf2)

def sym_hom(rotmat: sympy.Matrix, translvec: sympy.Matrix) -> sympy.Matrix:
    translvec = translvec.reshape(3, 1)
    return sympy.Matrix([
        [rotmat[0, 0], rotmat[0, 1], rotmat[0, 2], translvec[0, 0]],
        [rotmat[1, 0], rotmat[1, 1], rotmat[1, 2], translvec[1, 0]],
        [rotmat[2, 0], rotmat[2, 1], rotmat[2, 2], translvec[2, 0]],
        [0, 0, 0, 1]
    ])

def sym_hom_inv(hommat: sympy.Matrix) -> sympy.Matrix:
    rot_inv_mat = sympy.transpose(hommat[0:3, 0:3])
    transl_inv_vec = sympy.simplify(-rot_inv_mat * hommat[0:3, 3:4])
    return sympy.Matrix([
        [rot_inv_mat[0, 0], rot_inv_mat[0, 1], rot_inv_mat[0, 2], transl_inv_vec[0, 0]],
        [rot_inv_mat[1, 0], rot_inv_mat[1, 1], rot_inv_mat[1, 2], transl_inv_vec[1, 0]],
        [rot_inv_mat[2, 0], rot_inv_mat[2, 1], rot_inv_mat[2, 2], transl_inv_vec[2, 0]],
        [0, 0, 0, 1]
    ])

def dehom(hommat: sympy.Matrix) -> Tuple[sympy.Matrix, sympy.Matrix]:
    rotmat = sympy.Matrix([
        [hommat[0, 0], hommat[0, 1], hommat[0, 2]],
        [hommat[1, 0], hommat[1, 1], hommat[1, 2]],
        [hommat[2, 0], hommat[2, 1], hommat[2, 2]]
    ])
    translvec = sympy.Matrix([[hommat[0, 3], hommat[1, 3], hommat[2, 3]]])
    return rotmat, translvec


# Introdução à Computação Simbólica com _sympy_

## Motivação

Neste ponto do curso, você já aprendeu a realizar operações matemáticas elementares com Python. Por exemplo, se lhe for dado o valor do raio $R$, você consegue facilmente computar a área $\pi R^2$ de um círculo. Todavia, o valor de $\pi$ que você obtém é finito. As instruções abaixo verificam isto.

```python
from math import pi
print(pi)
3.141592653589793
```

Pense, no entanto, se você pudesse realizar o cálculo desta área de maneira "exata". Matematicamente falando, é impossível fazer isto pois $\pi$ é um número irracional – o imbróglio desta constante é longo na história da Matemática. Porém, a computação simbólica permite que operemos com $\pi$ como se fosse simplesmente um símbolo com precisão infinita. Embora 15 casas decimais, como o valor exemplificado acima, sejam suficientes para a maioria dos cálculos do mundo real, a computação simbólica permite que trabalhemos com modelos abstratos que servem a uma diversidade de propósitos.

Aliás, quando se diz que 3.141592653589793 é um valor razoavelmente aceitável, isto é verdade até mesmo para cálculos em escala astronômica. A equipe de engenharia da NASA explica que, usando este valor para calcular o perímetro de uma circunferência com diâmetro igual a 25 bilhões de milhas, o erro de cálculo é próximo de 1,5 polegada [[NASA]](https://www.jpl.nasa.gov/edu/news/2016/3/16/how-many-decimals-of-pi-do-we-really-need/). Até aí, nada mal para uma aproximação!

## O que é Computação Simbólica e para que serve?

*Computação Simbólica* (CS) é uma subárea de estudo da matemática e da ciência da computação que se preocupa em resolver problemas usando objetos simbólicos representáveis em um computador. Esses problemas surgem em muitas aplicações em ciências naturais, pesquisa básica, na indústria e principalmente no desenvolvimento de softwares para computação avançada denominados _sistemas de computação algébrica_ (SCAs).

A CS existente em um SCA é aplicada em álgebra computacional, projetos assistidos por computação (CAD), raciocínio automatizado, gestão do conhecimento, lógica computacional e sistemas formais de verificação. O desenvolvimento da CS depende da integração de basicamente três campos: *softwares matemáticos*, *álgebra computacional* e *lógica computacional* [[RISC/JKU]](https://risc.jku.at/studying-symbolic-computation/). Em casos mais avançados, a CS é útil para solucionar equações da macroeconomia, manipular números para a finalidade de criptografia e criar modelos probabilísticos [[Kotzé]](https://kevinkotze.github.io/mm-tut1-symbolic/), [[Cohen]](https://www.ukma.edu.ua/~yubod/teach/compalgebra/%5BJoel_S._Cohen%5D_Computer_algebra_and_symbolic_comp(BookFi.org).pdf).

## Principais SCAs

Alguns SCAs são populares de longa data, tais como Maple, Mathematica e MuPad. Entretanto, são comerciais e costumam ter licenças custosas, embora ofereçam versões com desconto para estudantes. Algumas alternativas robustas de uso livre são Scilab, Sagemath, Octave e o próprio módulo *sympy*. Uma lista completa de SCAs está disponível na [[Wikipedia]](https://en.wikipedia.org/wiki/Computer_algebra_system).

## Por que *sympy*?

O objetivo principal do *sympy* é ser uma biblioteca de manipulação simbólica para Python. Ele começou a ser desenvolvido em 2006 e atualmente está na versão 1.5.1, lançada em dezembro de 2019 na página oficial [[sympy.org]](https://www.sympy.org/pt/index.html). As principais características do módulo são as seguintes:

- é gratuito;

- é baseado inteiramente em Python;

- é leve e independente.

## Objetos numéricos x objetos simbólicos

Importaremos os módulos `math` e `sympy` para ver algumas  diferenças entre objetos numéricos e simbólicos.

import math as mt
import sympy as sy
sy.init_printing(pretty_print=True) # melhor impressão de símbolos

mt.pi # numérico

sy.pi # simbólico

Verifiquemos com `type`.

type(mt.pi)

type(sy.pi) # é um objeto simbólico

Vejamos mais um exemplo:

mt.sqrt(2)

sy.sqrt(2)

type(mt.sqrt(2))

type(sy.sqrt(2)) # é um objeto simbólico

### Função x método

Na aula anterior destacamos que `print` e `type` são "funções" similares àquelas do tipo $y = f(x)$ em Matemática. Em Python, essas "funções" recebem o nome de *função* mesmo.

Porém, há módulos que possuem *métodos*, que para seu entendimento, podem ser vistos como "funções" também. Porém, *função* e *método* são conceitos levemenete distintos.

Para aplicarmos funções usamos parênteses envolvendo um ou mais *parâmetros*.

No caso acima, `mt.sqrt(2)` mostra que `sqrt` age como uma função e o número 2 é seu único parâmetro. Note, além disso, que o sympy também possui a sua própria função `sqrt`, que é de uma natureza distinta. Ela é um objeto `sympy.core.power.Pow`. Não precisamos entender isso agora, mas basta saber que ela pertence a um submódulo do *sympy*.

Por outro lado, também aprendemos que o conjugado de um número complexo `z` (tipo `complex`) pode ser obtido como `z.conjugate()`. Esta forma "sem parâmetros" indica que `conjugate` é um método do objeto `z`.

A partir deste ponto, poderemos ver situações como as seguintes:

- `f(x)`: a função `f` é aplicada ao parâmetro `x`

- `a.f()`: `f` é um método sem parâmetro do objeto `a`

- `a.f(x)`: `f` é um método com parâmetro `x` do objeto `a`

A partir do último exemplo, podemos dizer que um método é, na verdade, uma função que pertence a um objeto.

### Atribuições com símbolos

Podemos atribuir símbolos a variáveis usando a função `symbols`.

x = sy.symbols('x')
y = sy.symbols('y')

`x` e `y` são símbolos sem valor definido.

x

y

Podemos operar aritmeticamente com símbolos e obter uma expressão simbólica como resultado.

z = sy.symbols('z')
x*y + z**2/3 + sy.sqrt(x*y - z)

**Exemplo**: escreva o produto notável $(x - y)^2$ como uma expressão simbólica.

x**2 - 2*x*y + y**2

Note que o nome da variável não tem a ver com o nome do símbolo. Poderíamos fazer o seguinte:

y = sy.symbols('x') # y é variável; x é símbolo
y

### Atribuição por desempacotamento

Também poderíamos realizar as atribuições anteriores da seguinte forma:

x, y, z = sy.symbols('x y z')

### Alfabeto de símbolos

O *sympy* dispõe de um submódulo chamado `abc` do qual podemos importar símbolos para letras latinas (maiúsculas e minúsculas) e gregas (minúsculas).

from sympy.abc import a,b,c,alpha,beta,gamma
(a + 2*b - 3*c)*(alpha/3 + beta/2 - gamma) # símbolico

from sympy.abc import D,G,psi,theta
D**a * G**b * psi**c * theta**2 # símbolico

**Nota**: algumas letras já são usadas como símbolos especiais, tais como `O`, que indica "ordem" e `I`, que é o complexo $i$. Neste caso, cuidado deve ser tomado com nomes de variáveis

sy.I # imaginário simbólico

type(sy.I)

### Símbolos com nomes genéricos

Para criar símbolos genéricos, temos de usar `symbols` ou `Symbol`.

sem_nocao = sy.symbols('nada')
sem_nocao

muito_louco = sy.Symbol('massa')
muito_louco

### Variáveis e símbolos

sem_medo = sem_nocao + 2
sem_medo

soma = muito_louco + 2
muito_louco = 3 # 'muito_louco' aqui não é o simbólico
soma

## Substituição

A operação de *substituição* permite que:

1. substituamos variáveis por valores numéricos para avaliar uma expressão ou calcular valores de uma função em um dado ponto.
2. substituamos uma subexpressão por outra.

Para tanto, procedemos da seguinte forma:

```python
expressao.subs(variavel,valor)
```

**Exemplo**: considere o polinômio $P(x) = 2x^3 - 4x -6$. Calcule o valor de $P(-1)$, $P(e/3)$, $P(\sqrt{3.2})$.

from sympy.abc import x
P = 2*x**3 - 4*x - 6
P1 = P.subs(x,-1)
Pe3 = P.subs(x,mt.e/3)
P32 = P.subs(x,mt.sqrt(3.2))
print(P1, Pe3, P32)

**Exemplo:** sejam $f(x) = 4^x$ e $g(x) = 2x - 1$. Compute o valor da função composta $f(g(x))$ em $x = 3$.

f = 4**x
fg = f.subs(x,2*x - 1)

fg.subs(x,3)

Poderíamos também fazer isso com um estilo "Pythônico":

fg = 4**x.subs(x,2*x - 1).subs(x,3)
fg

**Exemplo:** se $a(x) = 2^x$, $b(x) = 6^x$ e $c(x) = \cos(x)$, compute o valor de $a(x)b(c(x))$ em $x = 4$

a = 2**x
b = 6**x
c = sy.cos(x)
(a * b.subs(x,c)).subs(x,4)

Ou, de modo direto:

valor = ( 2**x * ( 6**x.subs(x,sy.cos(x))) ).subs(x,4)
valor

### Avaliação de expressão em ponto flutuante

Note que a expressão anterior não foi computada em valor numérico. Para obter seu valor numérico, podemos usar o método `evalf`.

valor.evalf()

#### Precisão arbitrária

`evalf` permite que escolhamos a precisão do cálculo impondo o número de dígitos de precisão. Por exemplo, a última expressão com 20 dígitos de precisão seria:

valor.evalf(20)

Com 55, seria:

valor.evalf(55)

E com 90 seria:

valor.evalf(90)

**Exemplo**: calcule o valor de $e$ com 200 dígitos de precisão.

sy.exp(1).evalf(200)

## Funções predefinidas x funções regulares

Vamos apresentar aqui três grupos de funções que podem ser criadas em Python para nos auxiliar ao longo do curso sem, no entanto, nos aprofundaremos nos detalhes de cada um.

Como dissemos em um momento anterior, a linguagem Python possui um *core* que contém um conjunto de funções já prontas que podemos usar, como é o caso de `print()`, `type()` e até mesmo `int()` e `float()` para operações de *casting*. Essas funções podem ser chamadas de **predefinidas** (*built-in functions*). Ou seja, são aquelas funções "já existentes". Exemplos adicionais seriam as funções do módulo `math`.

Suponhamos, porém, que você vasculhe módulos e mais módulos atrás de uma função que faça exatamente o que você quer, mas não a encontra. O que você faz? Você a cria! Podemos fazer isto de uma maneira usando uma "palavra-chave" (*keyword*) chamada `def` da seguinte forma:

```python
def f(x):
    (...)
    return y
```
A instrução acima permite que você crie uma *função* chamada `f` da qual `x` é um *argumento* e `y` é um *valor de retorno*, indicado por uma segunda "palavra-chave", *return*. Funções definidas por você dessa maneira são chamadas de **regulares**, *normais* – pelo fato de serem programadas de um modo regular, seguindo a "normalidade" da linguagem –, ou ainda *definidas pelo usuário* (do inglês *user-defined functions*, ou simplesmente *UDF*). Por conveniência, vamos nos referir a elas por este acrônimo elegante: UDF.

Uma UDF permite que você abstraia seu pensamento para criar basicamente o que quiser dentro dos limites da linguagem Python. Cabe, apesar disso, fazermos as seguintes ressalvas:

- uma UDF **pode ter zero ou mais argumentos**, tantos quantos se queira;
- uma UDF **pode ou não ter valor de retorno**;

Vamos entender as UDFs com exemplos.

**Exemplo:** Suponha que você é um(a) analista de dados do mercado imobiliário e está estudando o impacto do repasse de comissões pagas a corretores mediante vendas de imóveis. Você, então, começa a raciocinar e cria um modelo matemático bastante simples que, antes de tudo, precisa calcular o valor do repasse a partir do preço de venda.

Se $c$ for o percentual de comissão, $V$ o valor da venda do imóvel e $r$ o valor a ser repassado para o corretor, então, a função a ser definida é

$$r(V) = c\, V,$$

assumindo que $c$ seja um valor fixo.

Digamos que $c$ corresponda a 1.03% do valor da venda do imóvel. Neste caso podemos criar uma UDF para calcular $r$ para nós da seguinte forma:

def repasse(V):
    r = 0.0103*V
    return r

Para $V = \, R\$ \, 332.130,00$:

repasse(332130)

O que é necessário observar:

- `def` vem seguido pelo *nome* da função (`repasse`) após um espaço;
- o nome precede os argumentos, enclausurados por parênteses `(V)`. Neste caso, temos apenas um *argumento*, que é `V`;
- após o nome, os dois-pontos (`:`) são obrigatórios e significam mais ou menos "o que esta função faz será definido da seguinte maneira"
- a instrução `r = 0.0103*V` é o *escopo* da função, que deve ser escrito em uma ou mais linhas indentadas (pressione `TAB` para isso, ou use 4 espaços)
- o valor de retorno, se houver, é posto na última linha do escopo.

Podemos atribuir os valores do argumento e resultado a variáveis:

V = 332130
rep = repasse(V)
rep

Nomes iguais de variável e função são permissíveis.

repasse = repasse(V) # 'repasse' à esquerda é uma variável; à direita, função
print(repasse)

Todavia, isto pode ser confuso e é bom evitar.

O estilo "Pythônico" de escrever permite que o valor de retorno não seja explicitamente declarado. No escopo

```python
...
    r = 0.0103*V
    return r
```
 a variável `r` não é necessária.

Python é inteligente para permitir o seguinte:

def repasse(V):
    return 0.0103*V

# note que aqui não indentamos a linha.
# Logo esta instrução NÃO pertence ao escopo da função.
repasse(V)

Podemos criar uma função para diferentes valores de `c` e `V` usando *dois* argumentos:

def repasse_c(c,V): # esta função tem outro nome
    return c*V

c = 0.0234 # equivaleria a uma taxa de repasse de 2.34%
V = 197432 # o valor do imóvel agora é R$ 197.432,00
repasse_c(c,V)

A ordem dos argumentos importa:

V = 0.0234 # este deveria ser o valor de c
c = 197432 # este deveria ser o valor de V
repasse_c(c,V)

Por que o valor resultante é o mesmo? Porque a operação no escopo da função é uma multiplicação, `c*V`, que é comutativa independentemente do valor das variáveis. Porém, digamos que um segundo modelo tenha uma forma de cálculo distinta para a comissão dada por

$$r_2(V) = c^{3/5} \, V$$

Neste caso:

def repasse_2(c,V):
    return c**(3/5)*V

V = 197432
c = 0.0234

repasse_2(c,V)

Porém, se trocarmos o valor das variáveis, a função `repasse_2` calculará um valor distinto. Embora exista um produto também comutativo, o expoente `3/4` modifica apenas o valor de `c`.

# variáveis com valores trocados
c = 197432
V = 0.0234

repasse_2(c,V)

A ordem com que escrevemos os argumentos tem importância relativa aos valores que passamos e ao que definimos:

# variáveis com valores corretos
V = 197432
c = 0.0234

def repasse_2_trocada(V,c): # V vem antes de c
    return c**(3/5)*V

repasse_2_trocada(V,c)

Mas,

# os valores das variáveis estão corretos,
# mas foram passados para a função na ordem errada
repasse_2_trocada(c,V)

e

# a ordem dos argumentos está de acordo com a que foi definida
# mas os valores das variáveis foram trocados
V = 197432
c = 0.0234
repasse_2_trocada(c,V)

## Modelos matemáticos simbólicos

A partir do que aprendemos, podemos definir modelos matemáticos completamente simbólicos.

from sympy.abc import c,V

def repasse_2_simbolica(c,V):
    return c**(3/5)*V

Se chamarmos esta função, ela será um objeto simbólico.

repasse_2_simbolica(c,V)

Atribuindo em variável:

rep_simb = repasse_2_simbolica(c,V)

type(rep_simb) # é um objeto simbólico

**Exemplo:** Suponha, agora, que seu modelo matemático de repasse deva considerar não apenas um percentual $c$ pré-estabelecido, mas também um valor de "bônus" adicional concedido como prêmio pela venda do imóvel. Considere, então, que o valor deste bônus seja $b$. Diante disso, nosso novo modelo teria uma fórmula como a seguinte:

$$r_3(V) = c\,V + b$$

Simbolicamente:

# importaremos apenas o símbolo b,
# uma vez que c e V já foram importados
# como símbolos anteriormente
from sympy.abc import b

def r3(V):
    return c*V + b

rep_3 = r3(V)
rep_3

### Substituindo valores

Podemos usar a função `subs` para atribuir quaisquer valores para o modelo.

**Exemplo:** $c = 0.119$

rep_3.subs(c,0.119) # substituindo para c

**Exemplo:** $c = 0.222$

rep_3.subs(c,0.222) # substituindo para c

**Exemplo:** $c = 0.222$ e $b = 12.0$

rep_3.subs(c,0.222).subs(b,12.0) # substituindo para c, depois para b

### Substituição múltipla

O modo anterior de substituição não é "Pythônico". Para substituirmos mais de uma variável de uma vez, devemos usar *pares ordenados* separados por vírgula sequenciados entre colchetes como uma *lista*. Mais tarde, aprenderemos sobre pares ordenados e listas.

**Exemplo:** Modifique o modelo $r_3$ para que $c = 0.043$ e $b = 54.0$

# espaços foram adicionados para dar legibilidade
rep_3.subs( [ (c,0.043), (b,54.0) ] )

#### Pares ordenados

Em matemática, o conceito de par ordenado pode ser definido pelo conjunto:

$$ X \times Y = \{ (x,y) ; x \in X \text{ e } y \in Y \},$$

onde $X$ e $Y$ são conjuntos quaisquer e $x$ e $y$ são as *coordenadas*. Por exemplo, se $X = Y = \mathbb{R}$, o conjunto acima contém elementos do tipo $(3,2)$, $(-1,3)$, $(\pi,2.18)$ etc. Na verdade, eles formam o conjunto $\mathbb{R} \times \mathbb{R} = \mathbb{R}^2$, que é exatamente o *plano cartesiano*.

Logo, a substituição múltipla com `subs` ocorre da seguinte forma;

- a primeira coordenada é o *símbolo*;

- a segunda coordenada é o *valor* que você quer dar para o símbolo.

**Exemplo:** Calcule $r_3(V)$ considerando $c = 0.021$, $b = 34.0$ e $V = 432.000$.

# armazenaremos o valor na variável 'valor'
valor = r3(V)

# subsituição
valor.subs( [ (c,0.021), (b,54.0) ] )

Com o estilo "Pythônico":

valor = r3(V).subs( [ (c,0.021), (b,54.0) ] ) #
valor

Podemos seguir esta regra de pares para substituir todos os valores de um modelo simbólico genérico não necessariamente definido através de uma função. Veja o exemplo aplicado a seguir.

## Exemplo de aplicação: o índice de caminhabilidade

Estudos empíricos nos EUA mostraram que a *caminhabilidade* de uma vizinhança impacta substancialmente os preços das casas. A caminhabilidade está relacionada à distância da moradia a locais de amenidades, tais como restaurantes, bares, bibliotecas, mercearias etc.

O *índice de caminhabilidade* $W$ para uma vizinhança de casas é uma medida matemática que assume valores no intervalo $[0,1]$. A fórmula é definida por:

$$W(d) = e^{-5 \left( \dfrac{d}{M} \right)^5},$$

onde $d$ é a distância medida entre a vizinhança (0 metro) e um dado ponto de referência, e $M$ é a distância máxima de avaliação considerada a partir da qual a caminhabilidade é assumida como nula. Ou seja,

- quando estamos na vizinhança, $d = 0$, $W = 1$ e a caminhabilidade é considerada ótima.

- à medida que nos afastamos da vizinhança em direção ao local da amenidade, $d$ aumenta e o valor $W$ decai vertiginosamente até atingir o valor limite $M$ a partir do qual $W = 0$ e a caminhabilidade é considerada "péssima".

O índice de caminhabilidade é, portanto, calculado com relação a um ponto de destino definido e a distância deve levar em consideração as vias de circulação (ruas, rodovias etc) e não a distância mais curta (raio do perímetro). Por exemplo, se a distância máxima a ser considerada para a caminhabilidade for $M = 500 \, m$ , um bar localizado a 100 metros da vizinhança teria um índice de caminhabilidade maior do que o de uma farmácia localizada a 300 m e muito maior do que o de um shopping localizado a 800 m, ainda que muito famoso. Aliás, neste caso, o valor de $W$ para o shopping seria zero, já que 800 m está além do limite $M$ estabelecido.

Fonte: *De Nadai, M. and Lepri, B. [[The economic value of neighborhoods: Predicting real estate prices from the urban environment]](https://arxiv.org/pdf/1808.02547.pdf)*.

### Modelo simbólico

Podemos modelar $W$ simbolicamente e calcular seu valor para diferentes valores de $d$ e $M$ usando a substituição múltipla.

from sympy.abc import d,M,W

W = sy.exp(-5*(d/M)**5) # função exponencial simbólica
W

**Exemplo:** A nossa corretora de imóveis gostaria de entender a relação de preços de imóveis para o Condomínio Pedras de Marfim. Considerando $M = 1 km$, calcule:

- o índice de caminhabilidade $W_1$ em relação à farmácia Dose Certa, localizada a 222 m do condomínio.

- o índice de caminhabilidade $W_2$ em relação ao restaurante Sabor da Arte, localizada a 628 m do condomínio.

- o índice de caminhabilidade $W_3$ em relação ao Centro Esportivo Physicalidade, localizada a 998 m do condomínio.

- o índice de caminhabilidade $W_4$ em relação à Padaria Dolce Panini, localizada a 1,5 km do condomínio.

# note que 1 km = 1000 m
W1 = W.subs([ (d,222), (M,1000) ])
W2 = W.subs([ (d,628), (M,1000) ])
W3 = W.subs([ (d,998), (M,1000) ])
W4 = W.subs([ (d,1500), (M,1000) ])

Perceba, entretanto, que os valores calculados ainda não são numéricos, como esperado.

W1

W2

W3

W4

Lembre-se que podemos usar `evalf` para calcular esses valores. Faremos isso considerando 3 casas decimais.

# reatribuindo todos os valores
W1n = W1.evalf(3)
W2n = W2.evalf(3)
W3n = W3.evalf(3)
W4n = W4.evalf(3)

print('W1 =', W1n, '; ' \
      'W2 =', W2n, '; ' \
      'W3 =', W3n, '; ' \
      'W4 =', W4n)

Como era de se esperar, os valores decaem de 0.997 a 3.24e-17, que é um valor considerado nulo em termos de aproximação numérica.

#### Quebrando instruções com `\`

A contra-barra `\` pode ser usada para quebrar instruções e continuá-las nas próximas linhas, porém não poderá haver nenhum caracter após ela, nem mesmo espaços. Caso contrário, um erro será lançado.

print('Continuando' \
      'na linha abaixo')

# neste exemplo, há um caracter de espaço após \
print('Continuando' \
      'na linha abaixo')

### O tipo `bool`

Em Python, temos mais um tipo de dado bastante útil, o `bool`, que é uma redução de "booleano". Objetos `bool`, que têm sua raiz na chamada Álgebra de Boole, são baseados nos conceitos *true* (verdadeiro) e *false*, ou *0* e *1* e são estudados em algumas disciplinas, tais como Circuitos Lógicos, Matemática Discreta, Lógica Aplicada, entre outras.

Aprenderemos sobre operadores lógicos mais à frente. Por enquanto, cabe mencionar as entidades fundamentais `True` e `False`.

True

False

type(True)

type(False)

Podemos realizar testes lógicos para concluir verdades ou falsidades quando temos dúvidas sobre objetos e relações entre eles. Por exemplo, retomemos os seguintes valores:

W1

W2

A princípio, é difícil determinar qual dos dois é o maior. Porém, podemos realizar "perguntas" lógicas para o interpretador Python com operadores lógicos. Mostraremos apenas dois exemplos com `>` e `<`.

W1 > W2 # isto quer dizer: "W1 é maior do que W2?"

O valor `True` confirma que o valor de `W1` é maior do que `W2`.

W4 < 0

Note que, de acordo com nosso modelo de caminhabilidade, este valor deveria ser zero. Porém, numericamente, ele é uma aproximação para zero. Embora muito pequeno, não é exatamente zero! Por que isso ocorre? Porque o computador lida com uma matemática inexata e aproximada, mas com precisão satisfatória.

## Operadores lógicos

Vimos que `True` e `False` são os dois valores atribuíves a um objeto de tipo `bool`. Eles são úteis para testar condições, realizar verificações e comparar quantidades. Vamos estudar *operadores de comparação*, *operadores de pertencimento* e *operadores de identidade*.

### Operadores de comparação

A tabela abaixo resume os operadores de comparação utilizados em Python.

| operador | significado | símbolo matemático |
|---|---|---|
| `<` | menor do que | $<$ |
| `<=` | menor ou igual a | $\leq$ |
| `>` | maior do que | $>$ |
| `>=` | maior ou igual a | $\geq$ |
| `==` | igual a | $=$ |
| `!=` | diferente de | $\neq$ |

Podemos usá-los para comparar objetos.

**Nota:** `==` está relacionado à igualdade, ao passo que `=` é uma atribuição. São conceitos operadores com finalidade distinta.

2 < 3 # o resultado é um 'bool'

5 < 2 # isto é falso

2 <= 2 # isto é verdadeiro

4 >= 3 # isto é verdadeiro

6 != -2

4 == 4 # isto não é uma atribuição!

Podemos realizar comparações aninhadas:

x = 2
1 < x < 3

3 > x > 4

2 == x > 3

As comparações aninhadas acima são resolvidas da esquerda para a direita e em partes. Isso nos leva a introduzir os seguintes operadores.

| operador | símbolo matemático | significado | uso relacionado a |
|---|---|---|---|
| `or` | $\vee$ | "ou" booleano | união, disjunção |
| `and` | $\wedge$ | "e" booleano | interseção, conjunção |
| `not` | $\neg$ | "não" booleano | exclusão, negação |

# parênteses não são necessários aqui
(2 == x) and (x > 3) # 1a. comparação: 'True'; 2a.: 'False'. Portanto, ambas: 'False'

# parênteses não são necessários aqui
(x < 1) or (x < 2) # nenhuma das duas é True. Portanto,

not (x == 2) # nega o "valor-verdade" que é 'True'

not x + 1 > 3 # estude a precedência deste exemplo. Por que é 'True'?

not (x + 1 > 3) # estude a precedência deste exemplo. Por que também é 'True'?

### Operadores de pertencimento

A tabela abaixo resume os operadores de pertencimento.

| operador | significado | símbolo matemático
|---|---|---|
| `in` | pertence a | $\in$ |
| `not in` | não pertence a | $\notin$ |

Eles terão mais utilidade quando falarmos sobre sequências, listas. Neste momento, vejamos exemplos com objetos `str`.

'2' in '2 4 6 8 10' # o caracter '2' pertence à string

frase_teste = 'maior do que'
'maior' in frase_teste

'menor' in frase_teste # a palavra 'menor' está na frase

1 in 2 # 'in' e 'not in' não são aplicáveis aqui

### Operadores de identidade

A tabela abaixo resume os operadores de identidade.

| operador | significado
|---|---|
| `is` | "aponta para o mesmo objeto"
| `is not` | "não aponta para o mesmo objeto" |

Esses operadores são úteis para verificar se duas variáveis se referem ao mesmo objeto. Exemplo:

```python
a is b
a is not b
```

- `is` é `True` se `a` e `b` se referem ao mesmo objeto; `False`, caso contrário.
- `is not` é `False` se `a` e `b` se referem ao mesmo objeto; `True`, caso contrário.

a = 2
b = 3
a is b # valores distintos

a = 2
b = a
a is b # mesmos valores

a = 2
b = 3
a is not b # de fato, valores não são distintos

a = 2
b = a
a is not b # de fato, valores são distintos

## Equações simbólicas

Equações simbólicas podem ser formadas por meio de `Eq` e não com `=` ou `==`.

# importação
from sympy.abc import a,b
import sympy as sy
sy.init_printing(pretty_print=True)

sy.Eq(a,b) # equação simbólica

sy.Eq(sy.cos(a), b**3) # os objetos da equação são simbólicos

### Resolução de equações algébricas simbólicas

Podemos resolver equações algébricas da seguinte forma:

```python
solveset(equação,variável,domínio)
```

**Exemplo:** resolva $x^2 = 1$ no conjunto $\mathbb{R}$.

from sympy.abc import x
sy.solveset( sy.Eq( x**2, 1), x,domain=sy.Reals)

Podemos reescrever a equação como: $x^2 - 1 = 0$.

sy.solveset( sy.Eq( x**2 - 1, 0), x,domain=sy.Reals)

Com `solveset`, não precisamos de `Eq`. Logo, a equação é passada diretamente.

sy.solveset( x**2 - 1, x,domain=sy.Reals)

**Exemplo:** resolva $x^2 + 1 = 0$ no conjunto $\mathbb{R}$.

sy.solveset( x**2 + 1, x,domain=sy.Reals) # não possui solução real

**Exemplo:** resolva $x^2 + 1 = 0$ no conjunto $\mathbb{C}$.

sy.solveset( x**2 + 1, x,domain=sy.Complexes) # possui soluções complexas

**Exemplo:** resolva $\textrm{sen}(2x) = 3 + x$ no conjunto $\mathbb{R}$.

sy.solveset( sy.sin(2*x) - x - 3,x,sy.Reals) # a palavra 'domain' também pode ser omitida.

O conjunto acima indica que nenhuma solução foi encontrada.

**Exemplo:** resolva $\textrm{sen}(2x) = 1$ no conjunto $\mathbb{R}$.

sy.solveset( sy.sin(2*x) - 1,x,sy.Reals)

## Expansão, simplificação e fatoração de polinômios

Vejamos exemplos de polinômios em uma variável.

a0, a1, a2, a3 = sy.symbols('a0 a1 a2 a3') # coeficientes
P3x = a0 + a1*x + a2*x**2 + a3*x**3 # polinômio de 3o. grau em x
P3x

b0, b1, b2, b3 = sy.symbols('b0 b1 b2 b3') # coeficientes
Q3x = b0 + b1*x + b2*x**2 + b3*x**3 # polinômio de 3o. grau em x
Q3x

R3x = P3x*Q3x # produto polinomial
R3x

R3x_e = sy.expand(R3x) # expande o produto
R3x_e

sy.simplify(R3x_e) # simplify às vezes não funciona como esperado

sy.factor(R3x_e) # 'factor' pode funcionar melhor

# simplify funciona para casos mais gerais
ident_trig = sy.sin(x)**2 + sy.cos(x)**2
ident_trig

sy.simplify(ident_trig)

## Identidades trigonométricas

Podemos usar `expand_trig` para expandir funções trigonométricas.

sy.expand_trig( sy.sin(a + b) ) # sin(a+b)

sy.expand_trig( sy.cos(a + b) ) # cos(a+b)

sy.expand_trig( sy.sec(a - b) ) # sec(a-b)

## Propriedades de logaritmo

Com `expand_log`, podemos aplicar propriedades válidas de logaritmo.

sy.expand_log( sy.log(a*b) )

A identidade não foi validada pois `a` e `b` são símbolos irrestritos.

a,b = sy.symbols('a b',positive=True) # impomos que a,b > 0

sy.expand_log( sy.log(a*b) ) # identidade validada

sy.expand_log( sy.log(a/b) )

m = sy.symbols('m', real = True) # impomos que m seja um no. real
sy.expand_log( sy.log(a**m) )

Com `logcombine`, compactamos as propriedades.

sy.logcombine( sy.log(a) + sy.log(b) ) # identidade recombinada

## Fatorial

A função `factorial(n)` pode ser usada para calcular o fatorial de um número.

sy.factorial(m)

sy.factorial(m).subs(m,10) # 10!

sy.factorial(10) # diretamente

**Exemplo:** Sejam $m,n,x$ inteiros positivos. Se $f(m) = 2m!$, $g(n) = \frac{(n + 1)!}{n^2!}$ e $h(x) = f(x)g(x)$, qual é o valor de $h(2)$?

from sympy.abc import m,n,x

f = 2*sy.factorial(m)
g = sy.factorial(n + 1)/sy.factorial(n**2)

h = (f.subs(m,x)*g.subs(n,x)).subs(x,4)
h

## Funções anônimas

A terceira classe de funções que iremos aprender é a de *funções anônimas*. Uma **função anônima** em Python consiste em uma função cujo nome não é explicitamente definido e que pode ser criada em apenas uma linha de código para executar uma tarefa específica.

Funções anônimas são baseadas na palavra-chave `lambda`. Este nome tem inspiração em uma área da ciência da computação chamada de cálculo-$\lambda$.

Uma função anônima tem a seguinte forma:

```python
lambda lista_de_parâmetros: expressão
```

Funções anônimas podem são bastante úteis para tornar um código mais conciso.

Por exemplo, na aula anterior, definimos a função

```python
def repasse(V):
    return 0.0103*V
```

para calcular o repasse financeiro ao corretor imobiliário.

Com uma função anônima, a mesma função seria escrita como:

repasse = lambda V: 0.0103*V

Não necessariamente temos que atribui-la a uma variável. Neste caso, teríamos:

lambda V: 0.0103*V

Para usar a função, passamos um valor:

repasse(100000) # repasse sobre R$ 100.000,00

O modelo completo com "bonificação" seria escrito como:

r3 = lambda c,V,b: c*V + b # aqui há 3 parâmetros necessários

Redefinamos objetos simbólicos:

from sympy.abc import b,c,V
r3(b,c,V)

O resultado anterior continua sendo um objeto simbólico, mas obtido de uma maneira mais direta. Podemos usar funções anônimas para tarefas de menor complexidade.

## "Lambdificação" simbólica

Usando `lambdify`, podemos converter uma expressão simbólica do *sympy* para uma expressão que pode ser numericamente avaliada em outra biblioteca. Essa função desempenha papel similar a uma função *lambda* (anônima).

expressao = sy.sin(x) + sy.sqrt(x) # expressão simbólica
f = sy.lambdify(x,expressao,"math") # lambdificação para o módulo math
f(0.2) # avalia

Para avaliações simples como a anterior, podemos usar `evalf` e `subs`. A lambdificação será útil quando quisermos avaliar uma função em vários pontos, por exemplo. Na próxima aula, introduziremos sequencias e listas. Para mostrar um exemplo de lambdificação melhor veja o seguinte exemplo.

from numpy import arange # importação de função do módulo numpy

X = arange(40) # gera 40 valores de 0 a 39

X

f = sy.lambdify(x,expressao,"numpy")(X) # avalia 'expressao' em X
f


# importing Python library
import numpy as np

# define Unit Step Function
def unitStep(v):
    if v >= 0:
        return 1
    else:
        return 0

# design Perceptron Model
def perceptronModel(x, w, b):
    v = np.dot(w, x) + b
    y = unitStep(v)
    return y

# NOT Logic Function
# wNOT = -1, bNOT = 0.5
def NOT_logicFunction(x):
    wNOT = -1
    bNOT = 0.5
    return perceptronModel(x, wNOT, bNOT)

# AND Logic Function
# w1 = 1, w2 = 1, bAND = -1.5
def AND_logicFunction(x):
    w = np.array([1, 1])
    bAND = -1.5
    return perceptronModel(x, w, bAND)

# NAND Logic Function
# with AND and NOT
# function calls in sequence
def NAND_logicFunction(x):
    output_AND = AND_logicFunction(x)
    output_NOT = NOT_logicFunction(output_AND)
    return output_NOT

# testing the Perceptron Model
test1 = np.array([0, 1])
test2 = np.array([1, 1])
test3 = np.array([0, 0])
test4 = np.array([1, 0])

print("NAND({}, {}) = {}".format(0, 1, NAND_logicFunction(test1)))
print("NAND({}, {}) = {}".format(1, 1, NAND_logicFunction(test2)))
print("NAND({}, {}) = {}".format(0, 0, NAND_logicFunction(test3)))
print("NAND({}, {}) = {}".format(1, 0, NAND_logicFunction(test4)))

'''
OUTPUT
NAND(0, 1) = 1
NAND(1, 1) = 0
NAND(0, 0) = 1
NAND(1, 0) = 1
'''

ml/Perceptrons/Perceptron as NAND operator.py


import numpy as np

def detA_2x2(a, b, c, d):
    return a*d-b*c

def inv(A):

    if len(A.shape) == 1:
        return ValueError('Matix must have at least 2 dimensions!')
    if A.shape[1] not in [2, 3]:
        return ValueError('Matrix must be either 2x2 or 3x3!')

    elif len(A.shape) == 2:
        if A.shape[0] == 2:       # 2x2
            detA = detA_2x2(A[0, 0], A[0,1], A[1,0], A[1,1])
            cofactor = np.array([[A[1,1], -A[0,1]],
                                [-A[1,0], A[0,0]]])
            Ainv = cofactor/detA
        else:                     # 3x3
            detA = A[0,0]*detA_2x2(A[1,1],A[1,2],A[2,1],A[2,2]) + \
                    -A[0,1]*detA_2x2(A[1,0],A[1,2],A[2,0],A[2,2]) + \
                    +A[0,2]*detA_2x2(A[1,0],A[1,1],A[2,0],A[2,1])

            cf00 = detA_2x2(A[1,1],A[1,2],A[2,1],A[2,2])
            cf01 = detA_2x2(A[0,2],A[0,1],A[2,2],A[2,1])
            cf02 = detA_2x2(A[0,1],A[0,2],A[1,1],A[1,2])
            cf10 = detA_2x2(A[1,2],A[1,0],A[2,2],A[2,0])
            cf11 = detA_2x2(A[0,0],A[0,2],A[2,0],A[2,2])
            cf12 = detA_2x2(A[0,2],A[0,0],A[1,2],A[1,0])
            cf20 = detA_2x2(A[1,0],A[1,1],A[2,0],A[2,1])
            cf21 = detA_2x2(A[0,1],A[0,0],A[2,1],A[2,0])
            cf22 = detA_2x2(A[0,0],A[0,1],A[1,0],A[1,1])

            Ainv = np.array([[cf00, cf01, cf02],
                            [cf10, cf11, cf12],
                            [cf20, cf21, cf22]])/detA

    elif len(A.shape) == 3:
        Ainv = np.zeros_like(A)

        if A.shape[1] == 2:       # 2x2
            detA = detA_2x2(A[:,0, 0], A[:,0,1], A[:,1,0], A[:,1,1])

            cf00 = A[:,1,1]
            cf01 = -A[:,0,1]
            cf10 = -A[:,1,0]
            cf11 = A[:,0,0]

            Ainv[:,0,0] = cf00
            Ainv[:,0,1] = cf01
            Ainv[:,1,0] = cf10
            Ainv[:,1,1] = cf11

            Ainv = Ainv/detA[:,None,None]
        else:                     # 3x3
            detA = A[:,0,0]*detA_2x2(A[:,1,1],A[:,1,2],A[:,2,1],A[:,2,2]) + \
                    -A[:,0,1]*detA_2x2(A[:,1,0],A[:,1,2],A[:,2,0],A[:,2,2]) + \
                    +A[:,0,2]*detA_2x2(A[:,1,0],A[:,1,1],A[:,2,0],A[:,2,1])

            cf00 = detA_2x2(A[:,1,1],A[:,1,2],A[:,2,1],A[:,2,2])
            cf01 = detA_2x2(A[:,0,2],A[:,0,1],A[:,2,2],A[:,2,1])
            cf02 = detA_2x2(A[:,0,1],A[:,0,2],A[:,1,1],A[:,1,2])
            cf10 = detA_2x2(A[:,1,2],A[:,1,0],A[:,2,2],A[:,2,0])
            cf11 = detA_2x2(A[:,0,0],A[:,0,2],A[:,2,0],A[:,2,2])
            cf12 = detA_2x2(A[:,0,2],A[:,0,0],A[:,1,2],A[:,1,0])
            cf20 = detA_2x2(A[:,1,0],A[:,1,1],A[:,2,0],A[:,2,1])
            cf21 = detA_2x2(A[:,0,1],A[:,0,0],A[:,2,1],A[:,2,0])
            cf22 = detA_2x2(A[:,0,0],A[:,0,1],A[:,1,0],A[:,1,1])

            Ainv[:,0,0] = cf00
            Ainv[:,0,1] = cf01
            Ainv[:,0,2] = cf02
            Ainv[:,1,0] = cf10
            Ainv[:,1,1] = cf11
            Ainv[:,1,2] = cf12
            Ainv[:,2,0] = cf20
            Ainv[:,2,1] = cf21
            Ainv[:,2,2] = cf22

            Ainv = Ainv/detA[:,None,None]

    return Ainv, detA

if __name__ == '__main__':
    A_2x2 = np.random.rand(2,2)
    A_3x3 = np.random.rand(3,3)

    print(np.linalg.norm((np.linalg.inv(A_2x2) - inv(A_2x2))))
    print(np.linalg.norm((np.linalg.inv(A_3x3) - inv(A_3x3))))

    a = np.array([[[4, 8, 2],
                    [5, 0, 2],
                    [1, 1, 3]],

                    [[7, 0, 8],
                    [9, 5, 4],
                    [3, 4, 2]]])

    b = np.array([[[4, 8],
                    [5, 0]],

                    [[7, 0],
                    [9, 5]]])

    #2x2
    i = np.zeros_like(a, dtype=float)
    i[0,:,:] = np.linalg.inv(a[0,:,:])
    i[1,:,:] = np.linalg.inv(a[1,:,:])

    i_custom = inv(a)

    print(np.linalg.norm(i-i_custom))

    #3x3
    i = np.zeros_like(b, dtype=float)
    i[0,:,:] = np.linalg.inv(b[0,:,:])
    i[1,:,:] = np.linalg.inv(b[1,:,:])

    i_custom = inv(b)

    print(np.linalg.norm(i-i_custom))


"""
test_numpy.fft_example_01.py

This is an example code for numpy.fft.fft
https://docs.scipy.org/doc/numpy/reference/generated/numpy.fft.fft.html#numpy.fft.fft

More in general, Discrete Fourier Transform (numpy.fft)
https://docs.scipy.org/doc/numpy/reference/routines.fft.html

>>> np.arange(8)
array([0, 1, 2, 3, 4, 5, 6, 7])

when x=0, a=1,
        ...
     x=8, e^2(pi)j
"""

import numpy as np
import matplotlib.pyplot as plt

x = np.arange(8)
a = np.exp( 2j*np.pi * x/8 )
plt.plot( a )
np.fft.fft( a )

t = np.arange(256)
#array([0, 1, 2, ... , 254, 255])
sp = np.fft.fft( np.sin(t) )
freq = np.fft.fftfreq( t.shape[-1] )
plt.plot( freq, sp.real, freq, sp.imag )
plt.show()

"""
In this example, real input has an FFT which is Hermitian,
i.e., symmetric in the real part and anti-symmetric in the imaginary part,
as described in the numpy.fft documentation:
"""

plt.figure()

plt.subplot(211)
plt.title("Real part")
plt.plot( freq, sp.real )
plt.grid(True)
plt.axis([-0.5,0.5,-100,100])
plt.ylabel("?")

plt.subplot(212)
plt.title("Imagenary part")
plt.plot( freq, sp.imag )
plt.grid(True)

plt.axis([-0.5,0.5,-100,100])
plt.xlabel("?")
plt.ylabel("?")

#plt.title("Spectrum of r'$e^(2\pij*x/8)$'")


#!/usr/bin/env python

#	Sigular Value Decomposition (SVD)
#		Trucco, Appendix A.6
#
#	definition
#
#			A = U*D*transpose(V)
#
#		A any real m x n matrix
#		U is m x n matrix that columns are eigenvectors of A*transpose(A)
#			A*transpose(A) = U * D * transpose(V) * V * D * transpose(U) = U * D^2 * transpose(U)
#		V is n x n matrix that columns are eivenvectors of transpose(A)*A
#			transpose(A)*A = V * D * transpose(U) * U * D * transpose(V) = V * D^2 * transpose(V)
#		D is n x n diagonal matrix (non-negative real values called sigular values)
#
#	computing the inverse of a matrix using SVD
#
#		inverse(A) = V*inverse(D)*transpose(U)
#			where inverse(D) are 1/singular values
#

#
#		|-     -|
#		| 1 2 1 |
#	A = | 2 3 2 |
#		| 1 2 1 |
#		|_	   _|
#
#											   |-	     -|
#											   | 6  10 6  |
#	A * transpose( A ) = transpose( A ) * A =  | 10 17 10 |
#											   | 6  10 6  |
#											   |_	     _|

import numpy as np

Epsilon = 1e-12

a = np.array( [ [ 1, 2, 1 ],
				[ 2, 3, 2 ],
				[ 1, 2, 1 ],
			] )
print( '  matrix A = {}'.format( a ) )

#	basic inverse matrix
print( '###################################################' )
print( 'basic method inverse matrix...............' )

#	try to inverse using basic method 1/det(a) * adj(a)
try:
	inverseA = np.linalg.inv( a )
	print( '  matrix inverse( A ) = {}'.format( inverseA ) )
except np.linalg.linalg.LinAlgError as e:
	print( 'ERROR!!! Cannot inverse matrix : {}'.format( e ) )

print( '###################################################' )
print( 'SVD method inverse matrix...............' )

#	construct A * transpose( A )
aMultiplyTransposeA = np.array( [ [ 6, 10, 6 ],
									[ 10, 17, 10 ],
									[ 6, 10, 6 ],
								] )
print( '  matrix A * trasnpose( A ) = {}'.format( aMultiplyTransposeA ) )

#	calculate eigenvalues and eigenvectors
print( '      calculating eigen values/vectors' )
eigenvalues, eigenvectors = np.linalg.eig( aMultiplyTransposeA )
print( '      eigenvalues of matrix A * trasnpose( A ) = {}'.format( eigenvalues ) )
print( '      eigenvectors of matrix A * trasnpose( A ) = {}'.format( eigenvectors ) )

#	construct the D matrix from eigenvalues
d = np.array( [ [ eigenvalues[0], 0, 0 ],
				[ 0, eigenvalues[1], 0 ],
				[ 0, 0, eigenvalues[2] ]
			] )
print( '\n    d matrix = {}'.format( d ) )

#	construct the U and V matrix from eigenvectors
u = np.array( eigenvectors )
print( '    u matrix = {}'.format( u ) )

#	construct transpose v
vt = np.array( [ [ eigenvectors[0][0], eigenvectors[1][0], eigenvectors[2][0] ],
					[ eigenvectors[0][1], eigenvectors[1][1], eigenvectors[2][1] ],
					[ eigenvectors[0][2], eigenvectors[1][2], eigenvectors[2][2] ],
				 ] )
print( '    vt matrix = {}'.format( vt ) )

print( '    testing by calculate A from U*D*transpose(V)..............' )
result = np.matmul( u, d )
result = np.matmul( result, vt )
print( '         result from reconstruct A from SVD = {}'.format( result ) )
assert( ( a[0][0] - result[0][0] < Epsilon ) and ( a[0][1] - result[0][1] < Epsilon ) and ( a[0][2] - result[0][2] < Epsilon )
		and ( a[1][0] - result[1][0] < Epsilon ) and ( a[1][1] - result[1][1] < Epsilon ) and ( a[1][2] - result[1][2] < Epsilon )
		and ( a[2][0] - result[2][0] < Epsilon ) and ( a[2][1] - result[2][1] < Epsilon ) and ( a[2][2] - result[2][2] < Epsilon ) )

print( '..........................................' )
print( '\n\n    preparing data to do a inverse matrix from V*inverse(D)*transpose(U)..............' )

#	calculate inverse of D
inverseD = np.array( [ [ 1./eigenvalues[0] if eigenvalues[0] > Epsilon else 0, 0, 0 ],
						[ 0, 1./eigenvalues[1] if eigenvalues[1] > Epsilon else 0, 0 ],
						[ 0, 0, 1./eigenvalues[2] if eigenvalues[2] > Epsilon else 0 ]
					] )
print( '\n    inverse(D) matrix = {}'.format( inverseD ) )

#	construct v from eigenvectors
v = np.array( eigenvectors )
print( '    v matrix = {}'.format( v ) )

#	construct transpose u
ut = np.array( [ [ eigenvectors[0][0], eigenvectors[1][0], eigenvectors[2][0] ],
					[ eigenvectors[0][1], eigenvectors[1][1], eigenvectors[2][1] ],
					[ eigenvectors[0][2], eigenvectors[1][2], eigenvectors[2][2] ],
				 ] )
print( '    ut matrix = {}'.format( ut ) )

print( '\n\n    testing by calculate inverse( A ) from V*inverse(D)*transpose(U)..............' )
inverseA = np.matmul( v, inverseD )
inverseA = np.matmul( result, ut )
print( '         result from reconstruct inverse of A from SVD = {}'.format( inverseA ) )

#	checking the inverse matrix
aMultiplyInverseA = np.matmul( a, inverseA )
print( '         checking the inverse of A by A * inverse(A) = {}'.format( aMultiplyInverseA ) )


'''
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Author:B.Ajay
Licence:Apache 2.0
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
'''

import random as r
import math
import numpy as np
import matplotlib.pyplot as plt
lx,ly=[],[]
def compute_pi(sample_points):
    inside_point_ctr=0
    for i in range(sample_points):
        x=r.random()
        y=r.random()
        lx.append(x)
        ly.append(y)
        if(math.sqrt(x**2+y**2)<=1):inside_point_ctr+=1
    return 4*inside_point_ctr/sample_points
def draw():
    x_axis=np.linspace(0,1.0,150)
    y_axis=np.linspace(0,1.0,150)
    a,b=np.meshgrid(x_axis,y_axis)
    circle_equation=a**2+b**2-1.0
    _,axis=plt.subplots()
    axis.contour(a,b,circle_equation,[0])
    plt.scatter(lx,ly,s=0.001)
    axis.set_aspect(1)
    plt.title("Scatter Plot of Points sampled")
    plt.show()
sample_point=int(input("Enter the number of sample points to compute pi->Enter a large value 10000<x<10000000"))
flag='y'
if sample_point>10000000:
    flag=input("This value is much above the limit .Allowing this process may hog your memory .Do you still want to proceed?[y/n]")
if flag=='y':
    print("The Computation may take seconds to compute Please wait")
    print("Value of Pi (approximated): ",compute_pi(sample_point))
    draw()
else:
    exit()


import csv
import math
import random
import os.path
import operator
import numpy as np
import matplotlib.pyplot as plt
from copy import deepcopy
from functools import reduce

"""
    FURB - Bacharelado em Ciências da Computação
    Inteligência Artificial
    Trabalho 03 - Fase 03 - Regressão Polinomial - Overfitting
    Equipe: Adriner Maranho de Andrade, Fábio Luiz Fischer, Felipe Anselmo dos Santos, Jorge Guilherme Kohn

    Faça um script demo_regressaop que faz o seguinte:
        a) Baixe o arquivo data_preg.mat ou data_preg.svg. A primeira coluna representa os valores de x e a segunda coluna representa os valores de y.
        b) Faça o Gráfico de dispersão dos dados.
        c) Use a função polyfit para gerar a linha de regressão para N = 1 e trace-o no gráfico de dispersão na cor vermelha (plot (x, y, 'r')).
        (observe que nesta função a numeração coeficiente é invertida! β0=βN, β1=βN−1, β2=βN−2 , ...βN=β0)
        d) Trace a linha de regressão para N = 2 no gráfico na cor verde.
        e) Trace a linha de regressão para N = 3 no gráfico na cor preta.
        f) Trace a linha de regressão para N = 8 no gráfico na cor amarela.
        g) Calcule o Erro Quadrático Médio (EQM) para cada linha de regressão. Qual é o mais preciso?
            Resposta: A linha de regressão para N=8 é mais precisa para esse cenário pois obteve o menor erro quadrático médio: 0.05870934697363511
        h) Para evitar o overfitting, divida os dados aleatoriamente em Dados de Treinamento e Dados de Teste. Use os primeiros 10% dos dados como conjunto de teste, e o resto como de treinamento.
        i) Repita os passos de c - f, mas agora use apenas os dados de treinamento para ajustar a linha de regressão.
        J) Repita o passo g, mas agora utilize somente os dados de Teste para calcular o erro.
        k) Que método é o mais preciso neste caso?
            Resposta: Apesar de cada regressão poder se sobressair de acordo com os dados selecionados para o treinamento,
                nas execuções realizadas a regressão N=2 teve o melhor resultado em 5 execuções consecutivas de 5000 iterações.
                Em média, a regressão N=2 se saiu melhor em 50% das iterações da execução.

                Execução 1 - 5000 iterações
                    N1 teve o melhor EQM 14 vezes
                    N2 teve o melhor EQM 2779 vezes
                    N3 teve o melhor EQM 1104 vezes
                    N8 teve o melhor EQM 1103 vezes

                Execução 2 - 5000 iterações
                    N1 teve o melhor EQM 27 vezes
                    N2 teve o melhor EQM 2721 vezes
                    N3 teve o melhor EQM 1112 vezes
                    N8 teve o melhor EQM 1140 vezes

                Execução 3 - 5000 iterações
                    N1 teve o melhor EQM 25 vezes
                    N2 teve o melhor EQM 2718 vezes
                    N3 teve o melhor EQM 1137 vezes
                    N8 teve o melhor EQM 1120 vezes

                Execução 4 - 5000 iterações
                    N1 teve o melhor EQM 23 vezes
                    N2 teve o melhor EQM 2708 vezes
                    N3 teve o melhor EQM 1133 vezes
                    N8 teve o melhor EQM 1136 vezes

                Execução 5 - 5000 iterações
                    N1 teve o melhor EQM 14 vezes
                    N2 teve o melhor EQM 2725 vezes
                    N3 teve o melhor EQM 1108 vezes
                    N8 teve o melhor v 1153 vezes
"""

def strnum(x):
    return round(x, 2)

class DataSet:
    def __init__(self, x, y):
        self.x = np.array(x)
        self.y = np.array(y)
        self.lenght = len(self.x)

class RegressaoPolinomial:
    def __init__(self, dataset):
        self.dataset = dataset

    def regressaop(self, n, outro=None):
        # Chama a função polyfit e inverte seu resultado, pois sua numeração coeficiente é invertida
        b = np.polyfit(self.dataset.x, self.dataset.y, n)[::-1]

        def somatoriap(b, x):
            resultado = 0
            for i in range(n+1):
                resultado += b[i] * x ** i
            return np.array(resultado)
        # Quando outro objeto for passado como parâmetro o valor do polyfit será compartilhado entre o calculo de ambos
        if outro is None:
            return somatoriap(b, self.dataset.x)
        return somatoriap(b, self.dataset.x), somatoriap(b, outro.dataset.x)

    @staticmethod
    def eqm(y1, y2):
        # Se o tamanho dos vetores de entrada for diferente, deve ser feito um broadcasting do maior para o menor,
        # de forma que os elementos faltantes no array menor sejam iguais aos do array maior
        if len(y1) != len(y2):
            y1y = y1 if len(y1) > len(y2) else np.array([y1[i] if i < len(y1) else y2[i] for i in range(len(y2))])
            y2y = y2 if len(y2) > len(y1) else np.array([y2[i] if i < len(y2) else y1[i] for i in range(len(y1))])
            return reduce(operator.add, (y1y - y2y) ** 2) / len(y2y)
        return reduce(operator.add, (y1 - y2) ** 2) / len(y2)

# Realiza leitura do arquivo e parsing dos valores para float
with open(os.path.join(os.path.abspath(os.path.dirname(__file__)), "data_preg.csv")) as file:
    raw_data = csv.reader(file, delimiter=',')
    data = [[float(elem) for elem in row] for row in raw_data]

if data is not None:
    ds = DataSet([elem[0] for elem in data], [elem[1] for elem in data])
    rp = RegressaoPolinomial(ds)

    # Cria diagrama de disperção dos pontos do dataset
    fig = plt.figure('Gráfico de disperção')
    plt.title('Vermelho: N1  Verde: N2  Preto: N3  Amarelo: N8')
    plt.grid(True)
    plt.scatter(ds.x, ds.y)

    # Gera a linha de regressão polinomial para N1 em vermelho
    y1 = rp.regressaop(1)
    plt.plot(ds.x, y1, 'r')
    # Gera a linha de regressão polinomial para N2 em verde
    y2 = rp.regressaop(2)
    plt.plot(ds.x, y2, 'g')
    # Gera a linha de regressão polinomial para N3 em preto
    y3 = rp.regressaop(3)
    plt.plot(ds.x, y3, 'k')
    # Gera a linha de regressão polinomial para N8 em amarelo
    y4 = rp.regressaop(8)
    plt.plot(ds.x, y4, 'y')

    # Calcula o erro quadratico médio EQM para cada uma das regressões
    eqm = [[y, RegressaoPolinomial.eqm(y[1], ds.y)] for y in [('N1', y1), ('N2', y2), ('N3', y3), ('N8', y4)]]
    for i in range(len(eqm)):
        print('EQM da regressão de %s' % eqm[i][0][0], eqm[i][1])
    plt.show()

    resultados = []
    # Treinamento para evitar underfitting e overfitting
    # Número de iterações para o treinamento
    iteracoes = 5000
    # 'True' para exibir as regressões, 'False' para exeibir apenas o resultado final
    exibeDiagramaTreinamento = False

    print('\nIniciando treinamento para evitar underfitting e overfitting...')
    print('Utilizando', iteracoes, 'iterações\n')

    for i in range(iteracoes):
        # Cria uma cópia dos dados do dataset e randomiza o endereçamento deles na matriz
        datai = deepcopy(data)
        random.shuffle(datai)

        # Seleciona 10% dos dados aleatórios como dados para teste e os outros 90% para dados de treinamento
        # Ordena os valores para melhor qualidade na exibição do gráfico
        index = round((ds.lenght * 10) / 100)
        data_tes = datai[0:index]
        data_tre = datai[index:ds.lenght]
        data_tre.sort()

        ds_tes = DataSet([elem[0] for elem in data_tes], [elem[1] for elem in data_tes])
        ds_tre = DataSet([elem[0] for elem in data_tre], [elem[1] for elem in data_tre])
        rp_tes = RegressaoPolinomial(ds_tes)
        rp_tre = RegressaoPolinomial(ds_tre)

        if exibeDiagramaTreinamento:
            fig = plt.figure('Treinamento de Regressão Polinominial')
            plt.grid(True)
            plt.scatter(ds_tes.x, ds_tes.y)
            plt.scatter(ds_tre.x, ds_tre.y, c='b')

        # Calcula a regressão polinomial dos datasets de treino e de teste com o mesmo polyfit
        y1_tre, y1_tes = rp_tre.regressaop(1, rp_tes)
        y2_tre, y2_tes = rp_tre.regressaop(2, rp_tes)
        y3_tre, y3_tes = rp_tre.regressaop(3, rp_tes)
        y4_tre, y4_tes = rp_tre.regressaop(8, rp_tes)

        if exibeDiagramaTreinamento:
            plt.plot(ds_tre.x, y1_tre, 'r')
            plt.plot(ds_tre.x, y2_tre, 'g')
            plt.plot(ds_tre.x, y3_tre, 'k')
            plt.plot(ds_tre.x, y4_tre, 'y')

        # Calcula o erro quadratico médio EQM para cada uma das regressões
        eqm_tre = [('N1', y1_tre, y1_tes), ('N2', y2_tre, y2_tes), ('N3', y3_tre, y3_tes), ('N8', y4_tre, y4_tes)]
        eqm = [[y, RegressaoPolinomial.eqm(y[1], y[2])] for y in eqm_tre]

        # Salva o melhor resultado  na lista de resultados
        melhor = None
        for res in eqm:
            melhor = res if melhor is None or res[1] < melhor[1] else melhor
        resultados.append(melhor)

    if exibeDiagramaTreinamento:
        plt.show()
    # Sumariza o resultado do treinamento atravéz da iterações
    # Informa a qunatidade vezes que cada valor de N teve o melhor resultado
    for tre in ['N1', 'N2', 'N3', 'N8']:
        aux = list(filter(lambda res: res[0][0] == tre, resultados))
        print('%s teve o melhor EQM %s vezes' % (tre, len(aux)))

else:
    print('Erro ao ler arquivo')


# NumPy is often used along with packages like SciPy (Scientific Python) and Mat−plotlib (plotting library). This combination is widely used as a replacement for MatLab
# ndarray - an N-dimensional array; collection of items of the same type; items can be accessed using a zero-based index
# Each element in ndarray is an object of data-type object (called dtype)

import numpy as np
a = np.array([1,2,3])  # [1, 2, 3]
a = np.array([[1, 2], [3, 4]])
'''
[[1, 2]
 [3, 4]]
'''

# ndarray.shape - this array attribute returns a tuple consisting of array dimensions
a = np.array([[1,2,3],[4,5,6]])
print(a.shape) # (2, 3)

# It can also be used to resize the array
a.shape=(3,2)
'''
[[1, 2]
 [3, 4]
 [5, 6]]
'''

# NumPy also provides a reshape function to resize an array
a.reshape(3,2)

a.ndim # 2 dimentional array

# numpy.linspace - this function is similar to arange() function.
# In this function, instead of step size, the number of evenly spaced values between the interval is specified
# numpy.linspace(start, stop, num, endpoint, retstep, dtype)
x = np.linspace(10,20,5)  # [10.   12.5   15.   17.5  20.]

# numpy.logspace - this function returns an ndarray object that contains the numbers that are evenly spaced on a log scale.
# Start and stop endpoints of the scale are indices of the base, usually 10.
# numpy.logspace(start, stop, num, endpoint, base, dtype)
# default base is 10
a = np.logspace(1.0, 2.0, num = 10)
'''
[ 10.           12.91549665     16.68100537      21.5443469  27.82559402
  35.93813664   46.41588834     59.94842503      77.42636827    100.    ]
'''
a = np.logspace(1,10,num = 10, base = 2)
# [ 2.     4.     8.    16.    32.    64.   128.   256.    512.   1024.]

# arange funciton produces a list of numbers based on specifications
# numpy.arange(start, stop, step, dtype)
a=np.arange(8) # array([0, 1, 2, 3, 4, 5, 6, 7])
# now reshape it
b = a.reshape(2,2,2)
print(b) # b has three dimensions

b.dtype # its int32
b.itemsize

# specify data type
x = np.array([1,2,3,4,5], dtype = np.float32)

'''
different data types
bool_ Boolean (True or False) stored as a byte
int_ Default integer type (same as C long; normally either int64 or int32)
intc Identical to C int (normally int32 or int64)
intp Integer used for indexing (same as C ssize_t; normally either int32 or int64)
int8 Byte (-128 to 127)
int16 Integer (-32768 to 32767)
int32 Integer (-2147483648 to 2147483647)
int64 Integer (-9223372036854775808 to 9223372036854775807)
uint8 Unsigned integer (0 to 255)
uint16 Unsigned integer (0 to 65535)
uint32 Unsigned integer (0 to 4294967295)
uint64 Unsigned integer (0 to 18446744073709551615)
float_ Shorthand for float64
float16 Half precision float: sign bit, 5 bits exponent, 10 bits mantissa
float32 Single precision float: sign bit, 8 bits exponent, 23 bits mantissa
float64 Double precision float: sign bit, 11 bits exponent, 52 bits mantissa
complex_ Shorthand for complex128
complex64 Complex number, represented by two 32-bit floats (real and imaginary components)
complex128 Complex number, represented by two 64-bit floats (real and imaginary components)
'''

# numpy.empty creates an uninitialized array of specified shape and dtype
x = np.empty([3,2], dtype = int)
print(x)
'''
[[ -572458896         495]
 [          0           0]
 [          1 -2147483648]]
Note − The elements in an array show random values as they are not initialized
'''

# numpy.zeros returns a new array of specified size, filled with zeros
x = np.zeros(5)  # array([0., 0., 0., 0., 0.])
x = np.zeros((5,), dtype = np.int)  # array([0, 0, 0, 0, 0])
x = np.zeros((2,2), dtype = [('x', 'i4'), ('y', 'i4')])
'''
array([[(0, 0), (0, 0)],
       [(0, 0), (0, 0)]], dtype=[('x', '<i4'), ('y', '<i4')])
'''

# numpy.ones returns a new array of specified size and type, filled with ones
x = np.ones(5)  # array([1., 1., 1., 1., 1.])
x = np.ones([2,2], dtype = int)
'''
array([[1, 1],
       [1, 1]])
'''

# numpy.asarray - this function is similar to numpy.array
# # except for the fact that it has fewer parameters.
# # This routine is useful for converting Python sequence into ndarray.

# convert list to ndarray
x = [1,2,3]
a = np.asarray(x)

# dtype is set
x = [1,2,3]
a = np.asarray(x, dtype = float)

# ndarray from tuple
x = (1,2,3)
a = np.asarray(x)

# ndarray from list of tuples
x = [(1,2,3),(4,5)]
a = np.asarray(x)

# numpy.frombuffer - this function interprets a buffer as one-dimensional array.
# Any object that exposes the buffer interface is used as parameter to return an ndarray.
s = 'Hello World'
a = np.frombuffer(s, dtype = 'S1')

# numpy.fromiter
# This function builds an ndarray object from any iterable object.
# A new one-dimensional array is returned by this function.
list = range(5)
it = iter(list)   # obtain iterator object from list
x = np.fromiter(it, dtype = float)   # use iterator to create ndarray

# Slicing one dim array [start:stop:step<one-by-default>]
a = np.arange(10)
slice(2,7,2)  # [2  4  6]
a[2:7:2]  # array([2, 4, 6])
a[2:7] # array([2, 3, 4, 5, 6])
a[5]  # 5 - this is just an individual int not an array
a[7:]  # array([7, 8, 9])

# Slicing two dim array
a = np.arange(9)
a.shape=(3,3)
'''
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])
'''
a[2,2] # 8
a[1,:]  # array([3, 4, 5]) - we can skip specifying :
a[:,1] # array([1, 4, 7]) - we cant skip specifying :
a[1:,:] # all columns of row 2 and 3
'''
array([[3, 4, 5],
       [6, 7, 8]])
'''

# NaN (Not a Number) elements can be omitted by using ~ (complement operator)
a = np.array([np.nan, 1,2,np.nan,3,4,5])
print(a[~np.isnan(a)]) # [1. 2. 3. 4. 5.]

# filter out the non-complex elements from an array
a = np.array([1, 2+6j, 5, 3.5+5j])
print(a[np.iscomplex(a)]) # [2. +6.j 3.5+5.j]

'''
Broadcasting
-Ability of NumPy to treat arrays of different shapes during arithmetic operations.
-Arithmetic operations on arrays are usually done on corresponding elements.
-If two arrays are of exactly the same shape, then these operations are smoothly performed.

Broadcasting is possible if the following rules are satisfied −
-Array with smaller ndim than the other is prepended with '1' in its shape.
-Size in each dimension of the output shape is maximum of the input sizes in that dimension.
-An input can be used in calculation, if its size in a particular dimension matches the output size or its value is exactly 1.
-If an input has a dimension size of 1, the first data entry in that dimension is used for all calculations along that dimension.

A set of arrays is said to be broadcastable if the above rules produce a valid result and one of the following is true −
-Arrays have exactly the same shape.
-Arrays have the same number of dimensions and the length of each dimension is either a common length or 1.
-Array having too few dimensions can have its shape prepended with a dimension of length 1, so that the above stated property is true.
'''
a = np.array([[0.0,0.0,0.0],[10.0,10.0,10.0],[20.0,20.0,20.0],[30.0,30.0,30.0]])
b = np.array([1.0,2.0,3.0])
'''
First array:
[[ 0. 0. 0.]
 [ 10. 10. 10.]
 [ 20. 20. 20.]
 [ 30. 30. 30.]]

Second array:
[ 1. 2. 3.]

First Array + Second Array
[[ 1. 2. 3.]
 [ 11. 12. 13.]
 [ 21. 22. 23.]
 [ 31. 32. 33.]]
'''

# Iterating Over Array using numpy.nditer
a = np.arange(0,60,5)
a = a.reshape(3,4)
for x in np.nditer(a): # order of iteration is chosen to match the memory layout of the array (so transpose the array wont help, instead we can change the memory layout)
   print (x) # output 1

b = a.copy(order = 'F')  # we can change the memory layout of the array using order
for x in np.nditer(b):
   print (x) # output 2

for x in np.nditer(a, order = 'F'):  # or we can force nditer object to use a specific order by explicitly mentioning it
   print (x) # output 3
'''
Original array is:
[[ 0 5 10 15]
 [20 25 30 35]
 [40 45 50 55]]

Output 1:
0 5 10 15 20 25 30 35 40 45 50 55

Output 2:
0 20 40 5 25 45 10 30 50 15 35 55

Output 3:
0 20 40 5 25 45 10 30 50 15 35 55
'''

# Transposing an array
a=np.arange(9)
a.shape=(3,3)
a.T

# Modifying Array Values usinf op_flags parameter
a = np.arange(0,60,5)
a = a.reshape(3,4)
print (a) # output 1
for x in np.nditer(a, op_flags = ['readwrite']):
   x[...] = 2*x
print(a) # output 2
'''
Output 1:
[[ 0  5 10 15]
 [20 25 30 35]
 [40 45 50 55]]

Output 2:
[[  0  10  20  30]
 [ 40  50  60  70]
 [ 80  90 100 110]]
'''

# extracting one-dimensional arrays corresponding to each column
a = np.arange(0,60,5)
a = a.reshape(3,4)
print(a)
for x in np.nditer(a, flags = ['external_loop'], order = 'F'):
   print(x)
'''
output 1:
[[ 0  5 10 15]
 [20 25 30 35]
 [40 45 50 55]]

output 2:
[ 0 20 40]
[ 5 25 45]
[10 30 50]
[15 35 55]
'''

# Broadcasting Iteration
a = np.arange(0,60,5)
a = a.reshape(3,4)
print(a) # output 1
b = np.array([1, 2, 3, 4], dtype = int)
print(b) # output  2
for x,y in np.nditer([a,b]):
   print(x+y) # output 3
'''
output 1:
[[ 0  5 10 15]
 [20 25 30 35]
 [40 45 50 55]]

output 2:
[1 2 3 4]

output 3:
1 7 13 19 21 27 33 39 41 47 53 59
'''

# Array Manipulation
# Changing Shape
a = np.arange(8).reshape(2,4)  # reshape a 1x8 (1 row, 8 cols) array into a 2x4 array
a.flat[5] # 5 ; 1-D iterator over the array
a.flatten(order = 'F') # returns a copy of an array collapsed into one dimension
a.ravel(order = 'F') # Returns a contiguous flattened array / copy is made only if needed

# Transpose Operations
np.transpose(a) # same as a.T
a.T

# Changing Dimensions
a = np.arange(4).reshape(1,4)  # [0  1  2  3]
np.broadcast_to(a,(4,4))
'''
[[0  1  2  3]
 [0  1  2  3]
 [0  1  2  3]
 [0  1  2  3]]
'''

# Joining Arrays
a = np.array([[1,2],[3,4]])
'''First array:
[[1 2]
 [3 4]]'''
b = np.array([[5,6],[7,8]])
'''Second array:
[[5 6]
 [7 8]]'''

# concatenate joins a sequence of arrays along an existing axis
np.concatenate((a,b)) # by default it joins on axis=0, stacks horizontally - this is same as np.vstack((a,b))
'''Joining the two arrays along axis 0:
[[1 2]
 [3 4]
 [5 6]
 [7 8]]'''
np.concatenate((a,b),axis = 1) # this is same as np.hstack((a,b))
'''Joining the two arrays along axis 1:
[[1 2 5 6]
 [3 4 7 8]]'''

# stack joins a sequence of arrays along a new axis
np.stack((a,b),0)
'''Stack the two arrays along axis 0:
[[[1 2]
 [3 4]]
 [[5 6]
 [7 8]]]'''
np.stack((a,b),1)
'''Stack the two arrays along axis 1:
[[[1 2]
 [5 6]]
 [[3 4]
 [7 8]]]'''

# Splitting Arrays
a = np.arange(9)
# [0 1 2 3 4 5 6 7 8]
np.split(a,3) # Split the array in 3 equal-sized subarrays:
# [array([0, 1, 2]), array([3, 4, 5]), array([6, 7, 8])]
np.split(a,[4,7]) # Split the array at positions indicated in 1-D array:
# [array([0, 1, 2, 3]), array([4, 5, 6]), array([7, 8])]

a = np.arange(16).reshape(4,4)
'''
[[ 0 1 2 3]
 [ 4 5 6 7]
 [ 8 9 10 11]
 [12 13 14 15]]'''
np.hsplit(a,2) # Horizontal splitting
'''[array([[ 0,  1],
       [ 4,  5],
       [ 8,  9],
       [12, 13]]), array([[ 2,  3],
       [ 6,  7],
       [10, 11],
       [14, 15]])]'''
np.vsplit(a,2) # Vertical splitting
'''[array([[0, 1, 2, 3],
       [4, 5, 6, 7]]), array([[ 8,  9, 10, 11],
       [12, 13, 14, 15]])]'''

# Adding / Removing Elements
a = np.array([[1,2,3],[4,5,6]])
'''[[1 2 3]
 [4 5 6]]'''
np.resize(a,(3,3))
'''[[1 2 3]
 [4 5 6]
 [1 2 3]]''' # Observe that first row of a is repeated in b since size is bigger

a = np.array([[1,2,3],[4,5,6]])
'''[[1 2 3]
 [4 5 6]]'''
np.append(a, [7,8,9])  # Append elements to array - Axis parameter not passed. The input array is flattened before append
# [1 2 3 4 5 6 7 8 9]
np.append(a, [[7,8,9]],axis = 0)  # Append elements along axis 0
'''[[1 2 3]
 [4 5 6]
 [7 8 9]]'''
np.append(a, [[5,5,5],[7,8,9]],axis = 1) # Append elements along axis 1
'''[[1 2 3 5 5 5]
 [4 5 6 7 8 9]]'''

a = np.array([[1,2],[3,4],[5,6]])
'''[[1 2]
 [3 4]
 [5 6]]'''
np.insert(a,3,[11,12])  # Axis parameter not passed. The input array is flattened before insertion.
# [ 1 2 3 11 12 4 5 6]
np.insert(a,1,[11],axis = 0)  # Broadcast along axis 0:
'''[[ 1 2]
 [11 11]
 [ 3 4]
 [ 5 6]]'''
np.insert(a,1,11,axis = 1) # Broadcast along axis 1:
'''[[ 1 11 2]
 [ 3 11 4]
 [ 5 11 6]]'''

a = np.arange(12).reshape(3,4)
'''[[ 0 1 2 3]
 [ 4 5 6 7]
 [ 8 9 10 11]]'''
np.delete(a,5)  # no axis specified, so array flattened before delete operation as axis not used
[ 0 1 2 3 4 6 7 8 9 10 11]
np.delete(a,1,axis = 1) # Column 2 deleted
'''[[ 0 2 3]
 [ 4 6 7]
 [ 8 10 11]]'''
np.delete(a, np.s_[::2]) # A slice containing alternate values from array deleted:
# [ 2 4 6 8 10]

# unique function returns an array of unique elements in the input array
a = np.array([5,2,6,2,7,5,6,8,2,9])
np.unique(a)  # Unique values of first array
u,indices = np.unique(a, return_index = True) # Unique array and Indices array
u[indices]  # Reconstruct the original array using indices
u,cnt = np.unique(a,return_counts = True) # Return the count of repetitions of unique elements

# Mathematical/statistical funcitons
a = np.array([1.0,5.55, 123, 0.567, 25.532])
np.around(a)  # Default is 0. If negative, the integer is rounded to position to the left of the decimal point
np.around(a, decimals = 1)  # [   1.    5.6  123.    0.6  25.5]
np.around(a, decimals = -1) # [   0.    10.  120.    0.   30. ]

a = np.array([-1.7, 1.5, -0.2, 0.6, 10])
np.floor(a) # [ -2.   1.  -1.   0.  10.]
np.ceil(a)  # [ -1.   2.  -0.   1.  10.]

# Arithmetic functions
a = np.arange(9, dtype = np.float_).reshape(3,3)
'''[[ 0. 1. 2.]
 [ 3. 4. 5.]
 [ 6. 7. 8.]]'''
b = np.array([10,10,10])
# [10 10 10]
np.add(a,b) # Add the two arrays
[[ 10. 11. 12.]
 [ 13. 14. 15.]
 [ 16. 17. 18.]]
np.subtract(a,b) # Subtract the two arrays
[[-10. -9. -8.]
 [ -7. -6. -5.]
 [ -4. -3. -2.]]
np.multiply(a,b)  # Multiply the two arrays
[[ 0. 10. 20.]
 [ 30. 40. 50.]
 [ 60. 70. 80.]]
np.divide(a,b) # Divide the two arrays
[[ 0. 0.1 0.2]
 [ 0.3 0.4 0.5]
 [ 0.6 0.7 0.8]]

# Dot product
# For 2-D vectors, it is the equivalent to matrix multiplication.
# For 1-D arrays, it is the inner product of the vectors.
# For N-dimensional arrays, it is a sum product over the last axis of a and the second-last axis of b.
np.dot(a,b)
np.dot(b,a) == np.dot(a.T,b)

a=[1,2,3]
b=[2,3,1]
np.power(a,2) # array([1, 4, 9], dtype=int32)
np.power(a,b) # array([1, 8, 3], dtype=int32)

a = np.array([10,20,30])
b = np.array([3,5,7])
np.mod(a,b)  # same as np.remainder(a,b)
# array([1, 0, 2], dtype=int32)

a = np.array([[3,7,5],[8,4,3],[2,4,9]])
np.amin(a,1)  # array of min in each row
np.amin(a,0)  # array of min in each col
np.amin(a)    # overall min element

# ptp function returns the range (maximum-minimum) of values along an axis
a = np.array([[3,7,5],[8,4,3],[2,4,9]])
np.ptp(a)
np.ptp(a, axis = 1)
np.ptp(a, axis = 0)

# Percentile (or a centile) is a measure used in statistics indicating the value below which a given percentage of observations in a group of observations fall
a = np.array([[30,40,70],[80,20,10],[50,90,60]])
np.percentile(a,50)
# 50.0
np.percentile(a,50, axis = 1)  # 1 is rows
# [ 40. 20. 60.]
np.percentile(a,50, axis = 0)  # 0 is columns
# [ 50. 40. 60.]

# Median is defined as the value separating the higher half of a data sample from the lower half
a = np.array([[30,65,70],[80,95,10],[50,90,60]])
np.median(a) # 65.0
np.median(a, axis = 0)  # [ 50. 90. 60.]
np.median(a, axis = 1) # [ 65. 80. 60.]

# np.mean - syntax similar to np.median
# np.average function can be used to calculate weighted average if weights are specified, if they are not specified then this function is same as mean function
# In a multi-dimensional array, the axis for computation can be specified.
a = np.array([1,2,3,4])
np.average(a)
wts = np.array([4,3,2,1])
np.average(a,weights = wts)

# Standard deviation is the square root of the average of squared deviations from mean
# std = sqrt(mean(abs(x - x.mean())**2))
np.std([1,2,3,4])

# Variance is the average of squared deviations - mean(abs(x - x.mean())**2)
# In other words, the standard deviation is the square root of variance
np.var([1,2,3,4])

# sorting
# sort() function returns a sorted copy of the input array
a = np.array([[3,7],[9,1]])
'''[[3 7]
 [9 1]]'''
np.sort(a)
'''[[3 7]
 [1 9]]'''

# # Order parameter in sort function
dt = np.dtype([('name', 'S10'),('age', int)])
a = np.array([("raju",21),("anil",25),("ravi", 17), ("amar",27)], dtype = dt)
# [('raju', 21) ('anil', 25) ('ravi', 17) ('amar', 27)]
np.sort(a, order = 'name')
# [('amar', 27) ('anil', 25) ('raju', 21) ('ravi', 17)]

# numpy.argsort returns the array of indices of sorted array
# this indices array is used to construct the sorted array
x = np.array([3, 1, 2])
y = np.argsort(x)
x[y]  # Reconstruct original array in sorted order

# numpy.argmax() and numpy.argmin()
# return the indices of maximum and minimum elements along the given axis

# numpy.nonzero() function returns the indices of non-zero elements in the input array
# numpy.where(condition) function returns the indices of elements in an input array where the given condition is satisfied
# numpy.extract(condition) function returns the elements satisfying any condition

# id() returns a universal identifier of Python object, similar to the pointer in C
# While executing the functions, some of them return a copy of the input array, while some return the view
a
b=a # b.shape will also change the shape of a
# view - a different view of the same memory content
b=a.view() # b.shape will not change the shape of a, even though both a and b have same id()
# copy - When the contents are physically stored in another location
b=a.copy() # b.shape will not change the shape of a, as a and b have different id()

# MATRIX
# matrix is always two-dimensional, whereas ndarray is an n-dimensional array. Both the objects are inter-convertible.
i = np.matrix('1,2;3,4')
'''[[1  2]
 [3  4]]'''
j = np.asarray(i)
'''[[1  2]
 [3  4]]'''
k = np.asmatrix (j)
'''[[1  2]
 [3  4]]'''

# Matrix functions
np.matlib.empty((2,2)) # returns a new matrix without initializing the entries (it will be filled with random data)
np.matlib.zeros((2,2)) # returns the matrix filled with zeros
np.matlib.ones((2,2))
np.matlib.eye(n = 3, M = 4, k = 0, dtype = float) # returns a matrix with 1 along the diagonal elements and the zeros elsewhere
np.matlib.identity(5, dtype = float) # returns the Identity matrix of the given size. An identity matrix is a square matrix with all diagonal elements as 1
np.matlib.rand(3,3) # returns a matrix of the given size filled with random values

Python-Programming/04-numpy arrays and matrices.py


"""
Given the coordinates of four points in 2D space, return whether the four points could construct a square.

The coordinate (x,y) of a point is represented by an integer array with two integers.

Example:
Input: p1 = [0,0], p2 = [1,1], p3 = [1,0], p4 = [0,1]
Output: True
Note:

All the input integers are in the range [-10000, 10000].
A valid square has four equal sides with positive length and four equal angles (90-degree angles).
Input points have no order.
"""
import numpy as np
import math
class Solution:
    def euclid(self,t1,t2):
        return math.sqrt(math.pow(t1[0]-t2[0],2)+math.pow(t1[1]-t2[1],2))
    def validSquare(self, p1, p2, p3, p4):
        """
        :type p1: List[int]
        :type p2: List[int]
        :type p3: List[int]
        :type p4: List[int]
        :rtype: bool
        """
        dist = list()
        dist.append(self.euclid(p1,p2))
        dist.append(self.euclid(p1,p3))
        dist.append(self.euclid(p1,p4))
        dist.append(self.euclid(p2,p3))
        dist.append(self.euclid(p2,p4))
        dist.append(self.euclid(p3,p4))
        set_temp = set(dist)
        if(len(set_temp)!=2):
            return False
        else:
            temp = list()
            for nums in set_temp:
                temp.append(nums)
            temp.sort()
            if round(math.sqrt(2)*temp[0],2) == round(temp[1],2):
                return True
            else:
                return False


from sympy import *

#define sympy symbols for
x , t , z , nu = symbols ('x t z nu')

#for pretty printing:
init_printing ( use_unicode=True)

#take a derivative of  [ sin (x) e ^ x ]
print (" Diffrentiating sin (x) e ^ x ")
print ( diff ( sin ( x ) * exp (x), x))

#integration:
print ( "\n Integrating the result:")
print (  integrate ( exp(x) * sin (x) + exp(x)*cos(x) , x) )

print("\n Integrating with limits   -oo to + oo ")
print ( integrate( sin(x**2) , ( x, -oo , oo ) ) )

print ("\n Finding the limit as x -> 0 " )
print ( limit (sin(x)/x, x, 0 ) )

print ("\n Solving an equation:")
print ( solve ( x ** 2 - 2, x) )

print ("\n Solving a diffrentaial equation:")
y = Function ( 'y' )
print ( dsolve( Eq( y(t).diff(t,t) - y(t), exp(t) ), y(t) ) )

print("\n Finding EigenValues:")
print ( Matrix( [ [1,2],[2,2] ] ).eigenvals() )

#to check mathematical equality, between two expressions say a and b
# if simplify(a-b) evaluates to 0 => a = b
x = symbols('x')
a = (x+1)**2
b = x**2 + 2*x + 1
print ( simplify (a-b) )


# GenerateCurve.py

import numpy as np
import sys

def getCurveNames() :
    return ("line","circle","ellipse","clover","heart","lissajous","figureight")

def generateCurve(argv) :
    nargs = len(argv)
    if nargs==1 :
        print('argument must be one of: ',getCurveNames())
        exit()
    if nargs<2 : raise Exception('must specify curve name')
    name = sys.argv[1]
    if name==str("line") :
         npts = 1000
         x = np.arange(npts,dtype="float64")
         y = np.arange(npts,dtype="float64")
         return {"x":x,"y":y,"name":name}
    if name==str("circle") :
         min = 0.0
         max = 1.0
         npts = 2000
         inc = (max-min)/npts
         t = np.arange(min, max, inc)
         x = np.cos(2*np.pi*t)
         y = np.sin(2*np.pi*t)
         return {"x":x,"y":y,"name":name}
    if name==str("ellipse") :
         min = 0.0
         max = 1.0
         npts = 2000
         inc = (max-min)/npts
         t = np.arange(min, max, inc)
         a = 3.0
         if nargs>=3 : a = float(sys.argv[2])
         b = 2.0
         if nargs>=4 : b = float(sys.argv[3])
         x = a*np.cos(2*np.pi*t)
         y = b*np.sin(2*np.pi*t)
         return {"x":x,"y":y,"name":name}
    if name==str("clover") :
         min = 0.0
         max = 1.0
         npts = 2000
         inc = (max-min)/npts
         nloops = 3
         if nargs>=3 : nloops = float(sys.argv[2])
         print('nloops=',nloops)
         t = np.arange(min, max, inc)
         x = np.sin(nloops*2*np.pi*t)*np.cos(2*np.pi*t)
         y = np.sin(nloops*2*np.pi*t)*np.sin(2*np.pi*t)
         return {"x":x,"y":y,"name":name}
    if name==str("heart") :
         min = 0.0
         max = 1.0
         npts = 2000
         inc = (max-min)/npts
         t = np.arange(min, max, inc)
         x = (1.0 - np.cos(2*np.pi*t)*np.cos(2*np.pi*t))*np.sin(2*np.pi*t)
         y = (1.0 - np.cos(2*np.pi*t)*np.cos(2*np.pi*t)*np.cos(2*np.pi*t))*np.cos(2*np.pi*t)
         return {"x":x,"y":y,"name":name}
    if name==str("lissajous") :
         min = 0.0
         max = 1.0
         npts = 4000
         inc = (max-min)/npts
         t = np.arange(min, max, inc)
         m = 3
         if nargs>=3 : m = float(sys.argv[2])
         n = 1
         if nargs>=4 : n = float(sys.argv[3])
         x = np.sin(n*2*np.pi*t)
         y = np.cos(m*2*np.pi*t)
         return {"x":x,"y":y,"name":name}
    if name==str("figureight") :
         min = 0.0
         max = 1.0
         npts = 2000
         inc = (max-min)/npts
         a = 1
         if nargs>=3 : a = float(sys.argv[2])
         print('a=',a)
         t = np.arange(min, max, inc)
         x = a*np.sin(2*np.pi*t)*np.cos(2*np.pi*t)
         y = a*np.sin(2*np.pi*t)
         return {"x":x,"y":y,"name":name}
    raise Exception(name + ' not implemented')

if __name__ == '__main__':
    curveData = generateCurve(sys.argv)
    print('name=',curveData["name"],' len(x)=',len(curveData["x"]),' len(y)=',len(curveData["y"]))


import numpy as np
from numpy.core.records import array

# Request the matrix from the user
A = [float(a) for a in input("Please, Enter the matrix row by row: ").split(" ")]
A_shape = [int(i) for i in input("Please, Enter the dimension of the previous matrix: ").split(" ")]
A = np.array(A).reshape(A_shape)

# Request the starting vector from the user
X = [float(a) for a in input("Please, Enter the starting vector: ").split(" ")]
X_shape = [int(i) for i in input("Please, Enter the dimension of the previous vector: ").split(" ")]
X = np.array(X).reshape(X_shape)

def PowerMethod(A, X, tolerance, ndigits, counter = np.inf):
    print('#' * 50, "The Power Method", '#' * 50)
    i = 1
    B = list()
    print("Iteration No.", 0)
    print(X.T, "---")
    print("Iteraion No.", 1)
    B.append(np.dot(A, X))
    print(B[0].T, "---")
    print("Iteration No.", 2)
    # min = np.amax([abs(b) for b in B[0]])
    # B[0] = np.array([b / min for b in B[0]])
    B.append(np.dot(A, B[0]))
    dominant_eigenvalue = round(np.amax([abs(b) for b in abs(B[1])]) / np.amax([abs(b) for b in abs(B[0])]), ndigits)
    print(B[1].T, dominant_eigenvalue)
    # min = np.amax([abs(b) for b in B[1]])
    # B[1] = np.array([b / min for b in B[1]])
    previous = -1
    while((abs(dominant_eigenvalue - previous) > tolerance or (dominant_eigenvalue == previous)) and i <= counter-2):
        B.append(np.dot(A, B[i]))
        previous = dominant_eigenvalue
        i += 1
        # min = np.amax([abs(b) for b in B[i]])
        # B[i] = np.array([b / min for b in B[i]])
        dominant_eigenvalue = round(np.amax([abs(b) for b in abs(B[i])]) / np.amax([abs(b) for b in abs(B[i - 1])]),ndigits)
        print("Iteration No.", i+1)
        print(B[i].T, dominant_eigenvalue)

    print('#' * 120)
    dominant_eignevector = np.round([(b / np.amin([abs(b) for b in abs(B[i])])) for b in B[i]],ndigits)

    return [dominant_eignevector, dominant_eigenvalue, i+1]

# Example 1: 2 1 1 1 2 1 1 1 2
# Example: 1 -1 0 -2 4 -2 0 -1 2
# PowerMethod(A, X, 0.0001, 5)
[dominant_eignevector, dominant_eigenvalue, i] = PowerMethod(A, X, 0.0001, 5)
print("The Dominant Eigenvalue =", dominant_eigenvalue)
print("The Dominant Eigenvector =", dominant_eignevector.T)
print("Total Number of iterations =", i)

Eigenvalue Approximation/Power Method with visualization.py


# skewness python
# https://www.google.com/search?q=skewness+python&oq=Skewness+python&aqs=chrome.0.0l4j0i22i30l6.3988j0j4&sourceid=chrome&ie=UTF-8
# https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.skew.html
# https://www.geeksforgeeks.org/scipy-stats-skew-python/

''' Statistical functions
In simple words, skewness is the measure of how much the
probability distribution of a random variable deviates
from the normal distribution.
# https://www.investopedia.com/terms/s/skewness.asp

skewness = 0 : normally distributed.
skewness > 0 : more weight in the left tail of the distribution.
skewness < 0 : more weight in the right tail of the distribution.

'''
# part 1 ----------------------------------
import numpy as np
from scipy.stats import skew
import pandas as pd

arr = np.random.randint(1, 10, 10)
arr = list(arr)
# print(arr)
# # more weight in the right when skew>0,
# # determine skew close enough to zero
# print(skew(arr))
# print(skew([1, 2, 3, 4, 5]))

# part 2 ----------------------------------
# df = pd.read_csv('Data/nba.csv')
df = pd.read_csv('Data/XAUNZD_Daily.csv')

# print(df.tail())

# skewness along the index axis
print(df.skew(axis=0, skipna=True))

# skewness of the data over the column axis
# print(df.skew(axis=1, skipna=True))


import numpy as np
import pandas as pd
from IPython.display import display
import matplotlib.pyplot as plt

n = 1000
mean = (-1, 2)
cov = [[10, 1], [1, 0.5]]
X = np.random.multivariate_normal(mean, cov, n)

df = pd.DataFrame(X)
# Pandas does the centering for us
df = df -df.mean()
print("Centered covariance with Pandas")
covarianceX = df.cov()

print(covarianceX)

# we center it ourselves
X_centered = X - X.mean(axis=0)
print("Centered covariance using numpy")
print(np.cov(X_centered.T))
# extract the relevant columns from the centered design matrix
x = X_centered[:,0]
y = X_centered[:,1]
Cov = np.zeros((2,2))
Cov[0,1] = np.sum(x.T@y)/(n-1.0)
Cov[0,0] = np.sum(x.T@x)/(n-1.0)
Cov[1,1] = np.sum(y.T@y)/(n-1.0)
Cov[1,0]= Cov[0,1]
print("Centered covariance using own code")
print(Cov)

plt.plot(x, y, 'x')
plt.axis('equal')
plt.show()

# diagonalize and obtain eigenvalues, not necessarily sorted
EigValues, EigVectors = np.linalg.eig(Cov)
# sort eigenvectors and eigenvalues
#permute = EigValues.argsort()
#EigValues = EigValues[permute]
#EigVectors = EigVectors[:,permute]
print("Eigenvalues of Covariance matrix")
for i in range(2):
    print(EigValues[i])
FirstEigvector = EigVectors[:,0]
SecondEigvector = EigVectors[:,1]
print("First eigenvector")
print(FirstEigvector)
print("Second eigenvector")
print(SecondEigvector)
#thereafter we do a PCA with Scikit-learn
from sklearn.decomposition import PCA
pca = PCA(n_components = 2)
X2Dsl = pca.fit_transform(X)
print("Eigenvector of largest eigenvalue")
print(pca.components_.T[:, 0])


### Computation on NumPy Arrays: Universal Functions
# Using NP arrays in the traditional iterative manner is slow
# Vectorized operations are preferred - generally implemented in ufuncs

# here's a traditional loop-based approach to operating on arr values to demonstrate
import numpy as np
np.random.seed(0)

def compute_reciprocals(values):
    output = np.empty(len(values))
    for i in range(len(values)):
        output[i] = 1.0 / values[i]
    return output

values = np.random.randint(1, 10, size=5)
# %timeit compute_reciprocals(values)

# now, consider a case with a much bigger array of values:
big_arr = np.random.randint(1, 100, size=1000000)
# %timeit compute_reciprocals(big_array)

# note that both ops are way slower than they should be

##############################
### UFuncs introduction
# Interface for statically typed, compiled routines are known as vectorized operations
# can be achieved by performing an operation on the array, which then is applied to each el
# Advantage of vectorized approach is it pushes loop into the compiled layer, rather than being interpreted piecewise

# sample 1: ufunc operation b/w Scalar type and Array
compute_reciprocals(big_arr)
1.0 / big_arr
# %timeit (1.0 / big_array)

# sample 2: ufunc operation b/w Array type and Array
np.arange(5) / np.arange(1, 6)

# sample 3: ufunc operation on Multi-dimensional array
x = np.arange(9).reshape((3,3))
2 ** x

# the Pythonic way :: replace loops with vectorized expressions like the above when possible

##############################
### Exploring additional UFuncs
# Two types, Unary ufuncs and Binary ufuncs

## Array Arithmetic
# NumPy ufuncs use simple arithmetic operators
x = np.arange(4)
x
x + 5
x - 5
x * 2
x / 2
x // 2  # floor division
-x
x ** 2
x % 2

# can also string together operations, standard operations order respected
-(0.5*x + 1) ** 2

# Arithmetic operators in this context are wrappers for specific NP funcs. I.e. + evaluates to add()
np.add(x, 2)    # long form
x + 2           # wrapper equivalent

# We also have Boolean and Bitwise operators translated to NP ufuncs. Covered in "2.6-boolMasking.py"

## Absolute Value
# np plays nice with python standard library absolute val function abs()
x = np.array([-2, -1, 0, 1, 2])
abs(x)
# np has its own ufunc np.absolute() --- also callable as np.abs()
np.absolute(x)
np.abs(x)

x = np.array([3 - 4j, 4 - 3j, 2 + 0j, 0 + 1j])  # complex nums example
abs(x)
np.abs(x)

## Trigonometric ufuncs
# we can start by defining an array of angles
theta = np.linspace(0, np.pi, 3)
theta
np.sin(theta)
np.cos(theta)
np.tan(theta)
# and inverse trig funcs
x = [-1, 0, 1]
np.arcsin(x)
np.arccos(x)
np.arctan(x)

# side-note: recall that doubles get 15 decimal digits of precision.
#            to get less messy output, must round.
#            NON-Ufunc:  np.round(np.sin(theta), decimals=15) # rounds to 15th (dec precision limit)
#                ufunc:  np.rint( np.sin(theta))              # rounds to 0 th decimal

## Exponents and logarithms
import math;
x = np.array([1, 2, 3])

math.e ** x
np.exp(x)

2 ** x
np.exp2(x)

3 ** x
np.power(3, x)

x = [1, 2, 4, 10]           # NOTE: x is a list --> cannot use ufunc operators like ** or +

np.log(x)                   # natural logarithm
np.log2(x)                  # base 2 logarithm
np.log10(x)                 # base 10 logarithm

# related exp/log funcs, good for precision on small input vals
x = [0, .001, .01, .1]
np.exp(x)
np.expm1(x)             # Calculates ``exp(x) - 1``
np.log1p(x)             # Calculates ``log(1 + x)``

## Specialized ufuncs
# some additional examples: hyperbolic trig funcs, bitwise arithmetic, comparison operators,
# radian > degree conversions, rounding/remainders, etc. See NumPy ufunc docs

# another source of ufuncs :: scipy.special
from scipy import special

# Gamma functions (generalized factorials) & related funcs
x = [1, 5, 10]
special.gamma(x)    # gamma
special.gammaln(x)  # ln|gamma
special.beta(x, 2)  # beta

# Error function (integral of Gaussian), complement, and inverse
x = np.array([0, 0.3, 0.7, 1.0])
special.erf(x)
special.erfc(x)
special.erfinv(x)

###################################
### Advanced UFunc Features
# 3 topics: Out, Aggregates, Outer Products

## Specifying output: using "out"
# We can directly specify mem location to store result of a calculation using "out". Imperative style -> huge efficiency gain
x = np.arange(5)
y = np.empty(5)
np.multiply(x, 10, out=y)

# Can also utilize this with array views
y = np.zeros(10);   y = np.power(2, x, out=y[::2])  # y[::2] is a size=5 View of y. Save result into that view.
y = np.zeros(10);   y = 2 ** x                      # Requires additional temp array + full copy

## Aggregates
# With reduce() method, we can repeatedly execute a given operation to all elements of the array
# returning a single scalar value
x = np.arange(1, 6)
np.add.reduce(x)
np.multiply.reduce(x)

# with accumulate() method, we reduce() while storing all intermediate outputs in array
np.add.accumulate(x)
np.multiply.accumulate(x)

# note that there are dedicated NumPy funcs for these particular cases
# np.sum, np.prod, np.cumsum, np.cumprod - respectively

## Outer products (generalized cross product)
# Any ufunc can compute the output of all pairs of two different inputs with outer()
x = np.arange(1, 6)
np.multiply.outer(x, x)

# ufunc.at() and ufunc.reduceat() will be covered later in Fancy Indexing, and build on this idea

# additional ufuncs available in the NumPy/SciPy docs