Dois artigos sobre séries trigonométricas

Traduzi dois artigos, nomeadamente, Sobre a possibilidade de representar uma função por uma série trigonométrica de Riemann e Sobre um teorema concernente às séries trigonométricas de Cantor cuja importância é inegável. De facto, a noção de integral que é considerada habitualmente foi introduzida no primeiro dos artigos de modo a possibilitar uma abordagem ao problema. No segundo dos artigos, encontra-se a demonstração de que se a soma de uma série trigonométrica for nula então todos os coeficientes o são. A elaboração deste resultado conduziu o autor, em trabalhos posteriores, a introduzir a noção de conjunto.

Publicado em Matemática | Etiquetas , , , | Publicar um comentário

Algumas notas sobre o conceito de derivada

No artigo A análise do ponto de vista histórico apus uma ligação para um conjunto de textos que versam sobre alguns dos principais temas de análise dum ponto de vista histórico. Neste, vou destacar as actualizações que efectuei no texto A derivada relativamente ao conceito de derivada segundo José Anastácio da Cunha e à extensão que dei relativamente ao método dos círculos de Descartes. Comecemos pelo primeiro.

A definição de derivada baseada em infinitésimos

No livro XV dos seus Principios mathematicos o autor expôs uma teoria algo rigorosa de derivada. Começou por definir infinitamente grande como sendo a variável que poderá sempre assumir um valor superior a qualquer grandeza dada. Por outro lado, diz-se que uma variável é infinitésimo se puder sempre assumir um valor em módulo inferior a qualquer grandeza dada. O polinómio

p(x)=a_nx^n+a_{n-1}x^{n-1}+\cdots+a_1x

é um infinitésimo se x também o for. De facto, seja \varepsilon uma grandeza positiva arbitrária. Como x é um infinitésimo, podemos sempre torná-lo inferior a 1. Neste caso, \left|x^n\right|>|x| sempre que n>1 e, portanto,

\left|p(x)\right|<\left(\left|a_n\right|+\left|a_{n-1}\right|+\cdots+\left|a_1\right|\right)|x|

Se M for o máximo dos \left|a_i\right| então

\left|p(x)\right|<nM|x|

Como x é um infinitésimo podemos atribuir-lhe um valor inferior a \frac{\varepsilon}{nM}, advindo

\left|p(x)\right|<\varepsilon

como se pretendia.

Definiu diferencial do seguinte modo. Sendo f(x) uma função de x, o seu diferencial df é tal que

f'(x)=\frac{df}{dx}

não depende de dx e

\frac{f(x+dx)-f(x)}{dx}-f'(x)

é um infinitésimo.

Mostremos que

d\left(x^n\right)=nx^{n-1}

De facto,

\frac{d\left(x^n\right)}{dx}=nx^{n-1}

que não depende de dx. Por outro lado,

\frac{(x+dx)^n-x^n}{dx}-nx^{n-1}=\binom{n}{2}x^{n-2}dx+\binom{n}{3}x^{n-3}dx^2+\cdots+(dx)^{n-1}

que é um infinitésimo para qualquer valor finito de x.

Não é difícil demonstrar as principais propriedades da derivada com base nesta definição, nomeadamente, as derivadas da soma de funções, produtos de funções e composições de funções. Apresentamos aqui o caso da função implícita que não foi estudado pelo autor. Pretendemos mostrar que, sendo y uma função de x definida implicitamente pela equação

f(x,y)=0

o seu diferencial é dado por

dy=-\frac{\frac{\partial f}{\partial x}}{\frac{\partial f}{\partial y}}dx

se \frac{\partial f}{\partial y}\ne 0 numa vizinhança do ponto onde a função se encontra definida. Ora, facilmente verificamos que

\frac{dy}{dx}=-\frac{\frac{\partial f}{\partial x}}{\frac{\partial f}{\partial y}}

não depende de dx. Resta provar que

\frac{y(x+dx)-y(x)}{dx}+\frac{\frac{\partial f}{\partial x}}{\frac{\partial f}{\partial y}}

ou, o que vem dar ao mesmo,

\frac{\partial f}{\partial y}\frac{y(x+dx)-y(x)}{dx}+\frac{\partial f}{\partial x}

é um infinitésimo. Se abreviarmos por dy a quantidade y(x+dx)-y(x), da equação f(x,y)=0 segue-se também que f(x+dx,y+dy)=0. Então também

\frac{f(x+dx,y+dy)-f(x,y)}{dx}=0

que podemos escrever como

\frac{f(x+dx,y+dy)-f(x+dx,y)}{dy}\frac{y(x+dx)-y(x)}{dx}+\frac{f(x+dx,y)-f(x,y)}{dx}=0

A expressão anterior pode ainda se colocada na forma

\left[\frac{f(x+dx,y+dy)-f(x+dx,y)}{dy}-\frac{\partial f}{\partial y}(x+dx,y)\right]\frac{y(x+dx)-y(x)}{dx}+\frac{f(x+dx,y)-f(x,y)}{dx}-\frac{\partial f}{\partial x}+\frac{\partial f}{\partial y}(x+dx,y)\frac{y(x+dx)-y(x)}{dx}+\frac{\partial f}{\partial x}=0

Como as quantidades

\frac{f(x+dx,y+dy)-f(x+dx,y)}{dy}-\frac{\partial f}{\partial y}(x+dx,y)

e

\frac{f(x+dx,y)-f(x,y)}{dx}-\frac{\partial f}{\partial x}

são infinitésimos concluímos, da equação anterior, que

dh=\frac{\partial f}{\partial y}(x+dx,y)\frac{y(x+dx)-y(x)}{dx}+\frac{\partial f}{\partial x}

é um infinitésimo. Se f(x,y) admitir derivadas contínuas então

dl=\frac{\partial f}{\partial y}(x+dx,y)-\frac{\partial f}{\partial y}(x,y)

é um infinitésimo se dx também o for. Segue-se que

\frac{\partial f}{\partial y}\frac{y(x+dx)-y(x)}{dx}+\frac{\partial f}{\partial x}=dh-dl\frac{y(x+dx)-y(x)}{dx}

Como

dl\frac{y(x+dx)-y(x)}{dx}

é um infinitésimo, também o será

\frac{\partial f}{\partial y}\frac{y(x+dx)-y(x)}{dx}+\frac{\partial f}{\partial x}

como era pretendido.

O método do círculo revisitado

O método do círculo na determinação da tangente a uma curva definida por uma condição algébrica afigura-se muito mais intuitivo do que o método dos diferenciais por não depender dum conceito tão abstracto como é o de limite. Com isto em mente, decidi tentar deduzir o algoritmo das derivações a partir deste método. Estas são as conclusões.

Seja f(x,y)=0 uma equação algébrica, sendo f(x,y) uma expressão polinomial. De modo a determinarmos a recta que lhe é tangente, consideramos a família de círculos de centro no ponto arbitrário (p,0) que intersecta a curva no ponto \left(x_1,y_1\right). Esta intersecção determina-se por intermédio da resolução do sistema de equações

\left\lbrace\begin{array}{l}f(x,y)=0\\ \left(x-p\right)^2+y^2=\left(x_1-p\right)^2+y_1^2\end{array}\right.

Se fixarmos y, o algoritmo da divisão aplicado a f(x,y) permite-nos escrever

f(x,y)=f\left(x_1,y\right)+\left(x-x_1\right)f'_x(x,y)

onde f'_x(x,y) é uma função polinomial. Se fixarmos x_1, o algoritmo da divisão conduz-nos a

f\left(x_1,y\right)=f\left(x_1,y_1\right)+\left(y-y_1\right)f'_y\left(x_1,y\right)

Como f\left(x_1,y_1\right)=0, concluímos que

f(x,y)=\left(x-x_1\right)f'_x(x,y)+\left(y-y_1\right)f'_y\left(x_1,y\right)

O sistema de equações pode-se, portanto, escrever como

\left\lbrace\begin{array}{l}\left(x-x_1\right)f'_x(x,y)+\left(y-y_1\right)f'_y\left(x_1,y\right)=0\\ \left(x-x_1\right)\left(x+x_1-2p\right)+\left(y-y_1\right)\left(y+y_1\right)\end{array}\right.

Se multiplicarmos a primeira equação por y+y_1, a segunda por -f'_y\left(x_1,y\right) e somarmos obtemos a equação

\left(x-x_1\right)\left\lbrack\left(y+y_1\right)f'_x(x,y)-\left(x+x_1-2p\right)f'_y\left(x_1,y_1\right)\right\rbrack=0

De modo a que x_1 seja raiz dupla da equação é necessário que seja satisfeita a identidade

y_1f'_x\left(x_1,y_1\right)-\left(x_1-p\right)f'_y\left(x_1,y_1\right)=0

Daqui determinamos p e, por conseguinte, a normal à curva no ponto \left(x_1,y_1\right) tem a direcção do vector

\left(f'_x\left(x_1,y_1\right),f'_y\left(x_1,y_1\right)\right)

Se p(x) for uma função polinomial, sabemos que esta se poderá escrever como

p(x)=p'(x)(x-a)+p(a)

Definimos, deste modo, p'(x) como sendo o polinómio que resulta da identidade

p(x)-p(a)=p'(x)(x-a)

Interessa-nos agora determinar o valor de p'(a). Para o efeito, observamos que p'(x)=0 se p(x) for a função constante. Sendo p(x)=q(x)+r(x) então

p(x)-p(a)=q(x)-q(a)+r(x)-r(a)=\left[q'(x)+r'(x)\right](x-a)

seguindo-se que p'(a)=q'(a)+p'(a). Se for agora p(x)=q(x)r(x) temos

p(x)-p(a)=q(x)r(x)-p(a)r(a)=\left[q(x)-q(a)\right]r(x)+\left[r(x)-r(a)\right]q(a)

A equação anterior admite ainda a representação

p(x)-p(a)=\left[q'(x)r(x)+q(a)r'(x)\right](x-a)

Segue-se daqui a identidade p'(a)=q'(a)r(a)+q(a)r'(a). Em particular, se p(x)=kq(x) com k constante, então p'(a)=kq'(a). Se p(x)=x^2, concluímos que p'(a)=2a. Por seu turno, se p(x)=x^3=x^2\cdot x então p'(a)=3a^2. O método da indução permite-no concluir que se p(x)=x^n então p'(a)=na^{n-1}. Segue-se finalmente que se

p(x)=a_nx^n+a_{n-1}x^{n-1}+\cdots+a_1x+a_0

então

p'(a)=na_{n}x^{n-1}+(n-1)a_{n-1}x^{n-2}+\cdots+a_2x+a_1

Da função implícita é possível deduzir os casos das funções radicais, da função inversa e da função composta.

Publicado em Matemática, Sem categoria | Etiquetas , , , , | Publicar um comentário

Assinatura de procedimentos em SQL Server

Para além da minha função de administrador de bases-de-dados, desempenho ainda a de analista de sistemas na qual desenvolvo e analiso soluções de integração entre sistemas heterogéneos. Neste âmbito, foi-me requerido o melhoramento do seguinte processo:

  1. Sempre que existe a necessidade de processar um determinado conjunto de dados, um email é enviado ao operador;
  2. O operador executa uma aplicação que extrai um conjunto de dados em forma tabular de uma base-de-dados Oracle;
  3. O operador executa uma macro de Excel sobre os dados contidos no ficheiro, incluindo informação adicional;
  4. O operador envia o resultado ao seu cliente.

Não é difícil concluir uma melhoria trivial sobre este processo, nomeadamente, a eliminação dos três primeiros passos, com o auxílio de uma aplicação web que poderia ser facilmente criada pela equipa de desenvolvimento. O processo anterior evoluiria para:

  1. O utilizador autentica-se e inicia a extracção do relatório;
  2. A aplicação conecta-se à base-de-dados Oracle e extrai e processa os dados necessários;
  3. O utilizador descarrega ou visualiza o relatório final.

Neste caso específico, poderia ser criado na base-de-dados um utilizador cujas permissões se cingiriam à leitura das tabelas requeridas para a extracção dos dados cujas credenciais poderiam ser do conhecimento da equipa de desenvolvimento. No entanto, os administradores da base-de-dados não estão predispostos a criar um utilizador com as permissões pretendidas. Por outro lado, disponibilizando à equipa de desenvolvimento as credenciais de um utilizador com permissões demasiado elevadas viola claramente o princípio do menor privilégio, segundo o qual só deverão ser atribuídas apenas as permissões necessárias para a execução de uma tarefa. Dada a quantidade de dados envolvida, a solução passa pela interposição de uma base-de-dados que exponha uma interface para a consulta pretendida. Tendo em conta a experiência da equipa de desenvolvimento, a escolha recaiu sobre o SQL Server.

A extracção de dados Oracle a partir do SQL Server efectua-se facilmente com o auxílio do conector de dados Attunity. Mais uma vez a escolha particular desta tecnologia em detrimento do ODAC (Oracle Database Access Components) deveu-se particularmente à sua aplicabilidade no SSIS (SQL Server Integration Services). Após a instalação do Attunity, é necessário configurar uma conexão ODBC sobre a qual é criado um Linked Server no SQL Server e mapeado um login, que será designado por elevated_login ao utilizador conhecido com permissões elevadas. Não serão aqui detalhados os passos para o efeito. É também criado o login e respectivo utilizador rectricted_login que não será mapeado no Linked Server.

Se criarmos o procedimento na base-de-dados MyDatabase


CREATE PROCEDURE [dbo].[MyProcedure]
WITH EXECUTE AS 'elevated_login'
AS
BEGIN
SELECT * FROM OPENQUERY(My_Oracle_Linked_Server, '
 SELECT * FROM ORASCHEMA.ORATABLE
');
END;

sobre o qual atribuímos permissões de execução aos utilizadores elevated_login e restricted_login. Se executarmos o procedimento no contexto do utilziador elevated_login, o procedimento retorna-nos os dados pretendidos. A execução no contexto do utilizador restricted_login resulta no erro:

Cannot execute as the server principal because the principal “elevated_login” does not exist, this type of principal cannot be impersonated, or you do not have permission.

De facto, o utilizador restricted_login não tem permissões para personificar o utilizador elevated_login e a chamada do procedimento falha. Por outro lado, o princípio do menor privilégio é violado caso sejam atribuídas ao utilizador mais restrito permissões de personificação de um utilizador elevado. Tais atribuições permitiriam, após personificação, a elevação do utilizador restrito ao nível em que este seria capaz de executar qualquer consulta sobre o Linked Server. Coloca-se, portanto, a questão: É possível dar permissões a um utilizador menos elevado para executar um procedimento num contexto de um utilizador mais elevado sem o personificar? A resposta à pergunta é positiva e o método baseia-se no conceito de certificado.

Um certificado consiste num objecto assinado electronicamente ao qual se encontra associada uma chave pública (ver criptografia de chave pública). Esse objecto permite assistir a criação de conexões seguras entre bases-de-dados espelho, encriptar dados ou assinar packages e procedimentos. Vejamos como é possível recorrer ao certificado para permitir ao utilizador restricted_login executar apenas o procedimento, personificando o elevated_login.

Em primeiro lugar, como o Linked Server é um objecto de servidor, o certificado terá de ser conhecido ao nível da base-de-dados master. Podemos, portanto, criar aí um certificado.


USE master;
GO

CREATE CERTIFICATE [SigningCertificate]
ENCRYPTION BY PASSWORD = '$tr0ngp@$$w0rd'
WITH SUBJECT = 'Certificate for signing a Stored Procedure';
GO

O comando permite criar um certificado, isto é, um par de chave pública e chave privada. A chave privada é subsequentemente encriptada com o auxílio da palavra-chave “$tr0ngp@$$w0rd”. De modo a ser possível importar o par na base-de-dados MyDatabase, exportamos o respectivo conteúdo.


BACKUP CERTIFICATE [SigningCertificate]
TO FILE = 'd:\tmp\SigningCertificate.cer'
WITH PRIVATE KEY (
FILE = 'c:\tmp\SigningCertificate.pvk',
ENCRYPTION BY PASSWORD = 'b4ckup$tr0ngp@$$w0rd'
DECRYPTION BY PASSWORD = '$tr0ngp@$$w0rd'
)
;

Note-se que, para importar o certificado, é necessário conhecer a chave privada que lhe está associada. O processo de exportação encripta a chave privada com o auxílio da palavra-chave “b4ckup$tr0ngp@$$w0rd”. Como é óbvio, é necessário proporcionar a palavra-chave que desencripta a chave privada associada ao certificado que se encontra armazenada na base-de-dados. Criamos um login associado ao certificado e atribuímos-lhe permissões de personificação do elevated_login.


CREATE LOGIN [SigningLogin]
FROM CERTIFICATE [SigningCertificate];

GRANT IMPERSONATE ON LOGIN::elevated_login to [SigningLogin];

Importamos, de seguida, o certificado na base-de-dados MyDatabase onde foi criado o procedimento.


USE MyDatabase;
GO

CREATE CERTIFICATE [SigningCertificate]
FROM FILE = 'd:\tmp\SigningCertificate.cer'
WITH PRIVATE KEY (
FILE = 'd:\tmp\SigningCertificate.pvk',
ENCRYPTION BY PASSWORD = 'M1D4b453$tr0ngp@$$w0rd',
DECRYPTION BY PASSWORD = 'b4ckup$tr0ngp@$$w0rd');
GO

O comando anterior permite importar o certificado e a respectiva chave privada que foi encriptada com o auxílio da chave “b4ckup$tr0ngp@$$w0rd”. Será armazenada na base-de-dados MyDatabase encriptada pela palavra-chave “M1D4b453$tr0ngp@$$w0rd”. Criamos o utilizador associado ao certificado.


CREATE USER [SigningLogin] FROM LOGIN [SigningLogin]
GO

Finalmente, assinamos o procedimento.


ADD SIGNATURE TO OBJECT::[dbo].[MyProcedure]
BY CERTIFICATE [SigningCertificate]
WITH PASSWORD = 'M1D4b453$tr0ngp@$$w0rd';
GO

Se o utilizador restricted_login executar o procedimento assinado, a personificação do elevated_login será conseguida por intermédio do login SigningLogin. Verificamos que as permissões são atribuídas ao certificado ao invés de o serem ao utilizador menos elevado.

Será tentador assumir que é possível mapear o SigningLogin no Linked Server. No entanto, a sua tentativa será fútil uma vez que um utilizador associado a um certificado serve apenas o propósito de assinar objectos e não pode ser usado para conexões às base-de-dados.

Publicado em Computadores e Internet | Etiquetas , , , | Publicar um comentário

Programação CUDA em C#

A tecnologia CUDA, acrónimo para Compute Unified Device Architecture, foi desenvolvida pelo fabricante de placas gráficas NVIDIA e consiste numa plataforma e modelo de programação em C/C++ vocacionada para o aumento do desempenho das aplicações com o auxílio dos GPU (Graphics Processing Unit). Dada a especificidade dos dispositivos gráficos, a sua arquitectura viabiliza a realização paralela de operações matemáticas com um desempenho superior aos CPUs (Central Processing Unit) com custos reduzidos. Esta característica tem disseminado o recurso a este tipo de dispositivos em HPC (High Performance Computing) o que, por si só, é suficiente para considerar o domínio da tecnologia como uma mais-valia. A alternativa ao CUDA, definida pelo consórcio Khronos Group, é proporcionada pelo padrão livre de computação paralela em sistemas heterogéneos OpenCL (Open Computing Language). Este padrão é transversal a um número significativo e crescente de fabricantes de dispositivos tais como a Intel, IBM, AMD ou ARM, incluindo-se mesmo nesta lista a NVIDIA.

Não pretendo aqui justificar a utilização de uma tecnologia em detrimento de outra. Pretendo, por outro lado, descrever como é possível utilizar a tecnologia CUDA com linguagens alternativas como é o caso do C# ou do JAVA. Escolhi descrever CUDA devido ao facto de já ter efectuado algumas digressões neste sentido.

Princípios básicos

É possível encontrar no Cuda Toolkit Documentation toda a informação necessária para desenvolver aplicações CUDA nas linguagens C e C++. Aí encontram-se também definidos os passos para a instalação e configuração. Será suposto, nos exemplos que se seguem, que tantos os drivers da NVIDIA como o respectivo CUDA Toolkit se encontram propriamente instalados e configurados. Supôr-se-á ainda que o sistem onde os programas são executados contêm dispositivos com suporte CUDA.

Começamos por considerar o código de exemplo

// Device code
extern "C" __global__ void VecAdd(float* A, float* B, float* C, int N)
{
int i = blockDim.x * blockIdx.x + threadIdx.x;
if (i < N)
C[i] = A[i] + B[i];
}
// Host code
int main()
{
int i;
int N = 3;
size_t size = N * sizeof(float);

// Allocate input vectors h_A and h_B in host memory
float* h_A = (float*)malloc(size);
float* h_B = (float*)malloc(size);
float* h_C = (float*)malloc(size);

// Initialize input vectors
for(i = 0; i &amp;amp;lt; N; ++i)
{
h_A[i] = (float)i;
h_B[i] = (float)(N + i);
}

// Allocate vectors in device memory
float* d_A;
cudaMalloc(d_A, size);
float* d_B;
cudaMalloc(d_B, size);
float* d_C;
cudaMalloc(d_C, size);

// Copy vectors from host memory to device memory
cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice);
cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice);

// Invoke kernel
int threadsPerBlock = 256;
int blocksPerGrid =
(N + threadsPerBlock - 1) / threadsPerBlock;
VecAdd<<<blocksPerGrid, threadsPerBloc>>>(d_A, d_B, d_C, N);

// Copy result from device memory to host memory
// h_C contains the result in host memory
cudaMemcpy(h_C, d_C, size, cudaMemcpyDeviceToHost);

// Free device memory
cudaFree(d_A);
cudaFree(d_B);
cudaFree(d_C);

// Print result
for(i = 0; i < N; ++i)
{
printf("%f", h_A[i]);
}

printf("\n");

for(i = 0; i < N; ++i)
{
printf("%f", h_B[i]);
}

printf("\n");
for(i = 0; i < N; ++i)
{
printf("%f", h_C[i]);
}

// Free host memory
if(h_A != NULL) free(h_A);
if(h_B != NULL) free(h_B);
if(h_C != NULL) free(h_C);
}

Tata-se da codificação de um programa que recorre a uma placa gráfica para efectuar a soma de dois vectores. Se supusermos que este código se encontra definido no ficheiro AddVector.cu (com extensão CUDA), a sua compilação num programa executável pode ser conseguida com o auxílio do comando:

nvcc -arch=sm_20 -rdc=true AddVector.cu -o AddVector -lcudadevrt -ccbin “C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\bin\x86_amd64”

Assumimos aqui que o Visual Studio 2012 se encontra instalado no computador, uma vez que versões do compilador não são suportadas pelo programa nvcc. De facto, a directoria especificada na opção -ccbin deverá conter qualquer um dos compiladores suportados. A opção -arch permite indiciar ao compilador que o código a ser gerado deverá ser executado por dispositivos que suportem a arquitectura 2.0.

A execução do comando anterior permite-nos criar o programa AddVector.exe que imprime o seguinte resultado:

0.000000 1.000000 2.000000
3.000000 4.000000 5.000000
3.000000 5.000000 7.000000

De um modo resumido, as funções marcadas com o qualificador __global__ são executadas na gráfica mas chamadas no anfitrião. As funções marcadas como __device__ são chamadas e executadas na gráfica. As restantes funções são de anfitrião, cujo qualificador é __host__. No código acima, percebemos a definição da função

VecAdd(float*, float*, float*,int)

que é chamada no código de anfitrião na função main,

VecAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C)

As funções cudaMalloccudaMemcpycudaFree são definidas no CUDA Runtime API que constitui uma interface CUDA para C/C++.

O compilador nvcc

A abordagem anterior não se presta ao desenvolvimento de aplicações CUDA baseadas em outras linguagens de programação por razões óbvias, entre as quais se destaca a utilização da Runtime API. Assim, torna-se necessário perceber como o compilador gera o executável final.

Na secção The CUDA Compilation Trajectory do manual são descritas as fases da compilação de uma aplicação CUDA. O programa de entrada é pré-processado para compilação em binários CUDA (cubin) ou código intermédio (PTX) de dispositivo, os quais são colocados num ficheiro binário fatbinary. O programa de entrada é processado para compilação de anfitrião com o auxílio do compilador C/C++ especificado na linha de comandos. Neste processo, o binário fatbinay é imbuído no código de anfitrião onde as extensões CUDA são tranformadas em construções C++. O resultado final é compilado num objecto de anfitrião. Quando o código de dispositivo é lançado, o fatbinary é inpeccionado de modo a que o programa de anfitrião obtenha a imagem correcta para o GPU actual. O utilitário de compilação CUDA, como descrito na secção Using Separate Compilation in CUDA, pode ser configurado de modo a separar todo o processo de compilação de vários ficheiros CUDA. Esta funcionalidade encontra-se disponível desde a versão 5.0.

Cada etapa da compilação CUDA gera, numa directoria temporária, cada um dos ficheiros resultantes, os quais são eliminados imediatamente antes da sua conclusão. No entanto, como descrito na secção Keeping intermediate phase files o nvcc pode ser configurado por intermédio de uma opção de modo a que todos os ficheiros gerados sejam mantidos na directoria de compilação. Este tipo de compilação pode ser conseguido com o auxílio da execução da seguinte linha de comandos:

nvcc -arch=sm_20 -rdc=true AddVector.cu -o AddVector -lcudadevrt -ccbin “C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\bin\x86_amd64” -keep

É possível identificar três ficheiros, nomeadamente AddVector.ptxAddVector.sm_20.cubinAddVector.fatbin. O primeiro ficheiro, do tipo ptx consiste num conjunto textual de instruções muito semelhante ao assembly para processadores de CPU. O código contido neste ficheiro é orientado para um determinado conjunto de arquitecturas virtuais. Numa outra fase da compilação do nvcc, são gerados os binários cubin que são suportados por uma arquitectura real (e que poderão não funcionar com outras arquitecturas). No entanto, a arquitectura real deverá constituir uma implementação da arquitectura virtual de modo a que a compilação seja bem sucedida. Este processo permite mitigar o efeito da compatibilidade das aplicações face à evolução das GPU. De facto, o driver CUDA permite a compilação de ficheiros ptx em tempo de execução durante o qual já é conhecida a arquitectura real da GPU. Os binários fatbinary contêm os códigos de várias arquitecturas reais que implementam uma determinada arquitectura virtual.

O CUDA driver API

É natural assumirmos que o cubin imbuído no distribuível é carregado no GPU aquando das chamadas do kernel. Tal carregamento é conseguido com o auxílio de funções de mais baixo nível descritas no CUDA driver API. No sistema operativo Windows, essas funções são disponibilizadas pela dll nvcuda.dll instalada na pasta System32.

É possível mapear a interface disponibilizada pela driver API em funções C# com o auxílio do atributo DllImport. De facto, tal mapeamento pode ser encontrado no projecto matutils. A título de exemplo, seja considerado seguinte código:


static void Main(string[] args)
 {
 var cudaResult = CudaApi.CudaInit(0);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("CUDA function failed.");
 }

// Obtém o número de dispositivos disponíveis
 var deviceCount = default(int);
 cudaResult = CudaApi.CudaDeviceGetCount(ref deviceCount);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("CUDA function failed.");
 }

 for (int i = 0; < deviceCount; ++i)
 {
 var currentDevice = default(int);
 cudaResult = CudaApi.CudaDeviceGet(ref currentDevice, i);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("CUDA function failed.");
 }

 var deviceName = new StringBuilder();
 cudaResult = CudaApi.CudaDeviceGetName(deviceName, 64, i);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("CUDA function failed.");
 }

 var major = default(int);
 var minor = default(int);
 cudaResult = CudaApi.CudaDeviceGetAttribute(
 ref major,
 ECudaDeviceAttr.ComputeCapabilityMajor,
 i);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("CUDA function failed.");
 }

cudaResult = CudaApi.CudaDeviceGetAttribute(
 ref minor,
 ECudaDeviceAttr.ComputeCapabilityMinor,
 i);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("CUDA function failed.");
 }

Console.WriteLine(
 "GPU: {0}; Compute Capability: {1}.{2}",
 deviceName,
 major,
 minor);
 }

}

Constata-se facilmente que a primeira etapa consiste na inicialização do driver. A execução da função CuInit, mapeada no matutils como CudaInit, é obrigatória sempre que se pretenda recorrer às GPU em CUDA. O seguimento do código permite obter o número de dispositivos CUDA conectados ao anfitrião e obter, relativamente a cada um deles, o respectivo nome e poder de computação. A compilação do AddVector.cu com a linha de comandos

nvcc -arch=compute_30 -code=sm_30 -rdc=true AddVector.cu -lcudadevrt -ccbin “C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\bin\x86_amd64” -cubin -o AddVector.sm_30.cubin

permite gerar um ficheiro cubin que pode ser carregado e executado numa GPU com poder computacional 3.0. O código que se segue permite carregar o módulo que contém a função VecAdd compilado anteriormente e executá-lo com o auxílio do lançamento de um kernel.


static void Main(string[] args)
 {
 // Inicializa CUDA e avalia os dispositivos existentes
 var cudaResult = CudaApi.CudaInit(0);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Obtém o primeiro dispositivo
 var device = default(int);
 cudaResult = CudaApi.CudaDeviceGet(ref device, 0);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// O contexto é automaticamente colocado
// como corrente para a linha de fluxo actual
 var context = default(SCudaContext);
 cudaResult = CudaApi.CudaCtxCreate(
ref context,
ECudaContextFlags.SchedAuto, device);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Carrega o módulo no contexto actual
 var module = default(SCudaModule);
 cudaResult = CudaApi.CudaModuleLoad(
ref module,
"Data\\AddVector.sm_30.cubin");
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Obtém a função a ser chamada
 var cudaFunc = default(SCudaFunction);
 cudaResult = CudaApi.CudaModuleGetFunction(
 ref cudaFunc,
 module,
 "VecAdd");
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

var elemensNum = 10;

//var start = 0;
 var firstVector = new int[elemensNum];
 var secondVector = new int[elemensNum];
 var result = new int[elemensNum];
 for (int i = 0; i < elemensNum; ++i)
 {
 firstVector[i] = i + 1;
 secondVector[i] = elemensNum - i;
 }

// Reserva o primeiro vector
 var firstCudaVector = default(SCudaDevicePtr);
 cudaResult = CudaApi.CudaMemAlloc(
 ref firstCudaVector,
 Marshal.SizeOf(typeof(int)) * elemensNum);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Reserva o segundo vector
 var secondCudaVector = default(SCudaDevicePtr);
 cudaResult = CudaApi.CudaMemAlloc(
 ref secondCudaVector,
 Marshal.SizeOf(typeof(int)) * elemensNum);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Reserva o terceiro vector
 var resultCudaVector = default(SCudaDevicePtr);
 cudaResult = CudaApi.CudaMemAlloc(
 ref resultCudaVector,
 Marshal.SizeOf(typeof(int)) * elemensNum);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

var cudaSize = default(SCudaDevicePtr);
 cudaResult = CudaApi.CudaMemAlloc(
 ref cudaSize,
 Marshal.SizeOf(typeof(int)));
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Efectua a cópia do primeiro vector para o dispositivo
 var handle = GCHandle.Alloc(
firstVector,
GCHandleType.Pinned);
 var size = Marshal.SizeOf(typeof(int));
 var hostPtr = handle.AddrOfPinnedObject();

cudaResult = CudaApi.CudaMemcpyHtoD(
 firstCudaVector,
 hostPtr,
 elemensNum * size);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

handle.Free();

// Efectua a cópia do segundo vector para o dispositivo
 handle = GCHandle.Alloc(
secondVector,
GCHandleType.Pinned);
 hostPtr = handle.AddrOfPinnedObject();

cudaResult = CudaApi.CudaMemcpyHtoD(
 secondCudaVector,
 hostPtr,
 elemensNum * size);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

handle.Free();

var vectorSizePtr = Marshal.AllocHGlobal(
Marshal.SizeOf(typeof(int)));
 cudaResult = CudaApi.CudaMemcpyHtoD(
 cudaSize,
 hostPtr,
 size);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

Marshal.FreeHGlobal(vectorSizePtr);

// Reserva espaço para o vector de argumentos do kernel
 var managedPtrArray = new IntPtr[4];
 var ptrSize = Marshal.SizeOf(typeof(IntPtr));
 var unmanagedArrayPtr = Marshal.AllocHGlobal(ptrSize * 3);

// Procede à criação dos objectos em código não gerido
 var managedElementPtr = Marshal.AllocHGlobal(Marshal.SizeOf(
typeof(SCudaDevicePtr)));
 managedPtrArray[0] = managedElementPtr;
 Marshal.StructureToPtr(
firstCudaVector,
 managedElementPtr,
false);
 Marshal.WriteIntPtr(unmanagedArrayPtr, 0, managedElementPtr);

managedElementPtr = Marshal.AllocHGlobal(
Marshal.SizeOf(typeof(SCudaDevicePtr)));
 managedPtrArray[1] = managedElementPtr;
 Marshal.StructureToPtr(
secondCudaVector,
managedElementPtr,
false);
 Marshal.WriteIntPtr(
unmanagedArrayPtr,
ptrSize,
managedElementPtr);

managedElementPtr = Marshal.AllocHGlobal(
Marshal.SizeOf(typeof(SCudaDevicePtr)));
 managedPtrArray[2] = managedElementPtr;
 Marshal.StructureToPtr(
resultCudaVector,
managedElementPtr,
false);
 Marshal.WriteIntPtr(
unmanagedArrayPtr,
2 * ptrSize,
managedElementPtr);

managedElementPtr = Marshal.AllocHGlobal(
Marshal.SizeOf(typeof(SCudaDevicePtr)));
 managedPtrArray[3] = managedElementPtr;
 Marshal.StructureToPtr(
cudaSize,
 managedElementPtr,
false);
 Marshal.WriteIntPtr(
unmanagedArrayPtr,
3 * ptrSize,
managedElementPtr);

// Realiza a chamada
 cudaResult = CudaApi.CudaLaunchKernel(
 cudaFunc,
 (uint)elemensNum,
 1,
 1,
 1,
 1,
 1,
 0,
 new SCudaStream(),
 unmanagedArrayPtr,
 IntPtr.Zero);

cudaResult = CudaApi.CudaCtxSynchronize();

// Liberta o conjunto de argumentos reservado
 Marshal.FreeHGlobal(unmanagedArrayPtr);
 for (int i = 0; i &amp;amp;lt; 4; ++i)
 {
 var current = managedPtrArray[i];
 Marshal.FreeHGlobal(current);
 }

// Copia de volta o terceiro vector para o anfitrião
 handle = GCHandle.Alloc(result, GCHandleType.Pinned);
 hostPtr = handle.AddrOfPinnedObject();
 cudaResult = CudaApi.CudaMemcpyDtoH(
 hostPtr,
 resultCudaVector,
 size * elemensNum);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

handle.Free();

// Imprime o conteúdo dos vectores
 for (int i = 0; i < elemensNum; ++i)
 {
 Console.Write("{0} ", firstVector[i]);
 }

Console.WriteLine();

for (int i = 0; i < elemensNum; ++i)
 {
 Console.Write("{0} ", secondVector[i]);
 }

Console.WriteLine();

for (int i = 0; i < elemensNum; ++i)
 {
 Console.Write("{0} ", result[i]);
 }

// Liberta o primeiro vector do GPU
 cudaResult = CudaApi.CudaMemFree(firstCudaVector);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Liberta o segundo vector do GPU
 cudaResult = CudaApi.CudaMemFree(secondCudaVector);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Liberta o vector do resultado do GPU
 cudaResult = CudaApi.CudaMemFree(resultCudaVector);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Liberta o espaço reservado para conter o número
// de elementos de cada vector
 cudaResult = CudaApi.CudaMemFree(cudaSize);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Remove o módulo do contexto actual
 cudaResult = CudaApi.CudaModuleUnload(module);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }

// Descarta o contexto
 cudaResult = CudaApi.CudaCtxDestroy(context);
 if (cudaResult != ECudaResult.CudaSuccess)
 {
 throw new Exception("A CUDA error has occurred.");
 }
 }

Dada a pretensão de utilizar CUDA, é necessário, em primeiro lugar, inicializar o driver CUDA. De seguida, é obtido o primeiro dispositivo registado cujo valor ordinal é 0. Um contexto é muito semelhante a um processo de anfitrião onde, por exemplo, cada conjunto de variáveis tem um espaço de endereçamento próprio. As funções são, portanto, carregadas e executadas no âmbito de um processo. Todos os recursos reservados num determinado contexto são libertados aquando da sua destruição. Os passos que se seguem à criação do contexto são o carregamento do módulo que contém a função que pretendemos executar que, neste caso, é a VecAdd, a obtenção do seu apontador para chamada de execução a partir do anfitrião e o lançamento do kernel respectivo. Como as funções do driver executam no exterior da máquina virtual do .NET, é necessária uma interligação entre processos .NET e processos de código não gerido. Não serão aqui discutidas as  formas de transferir dados entre código gerido e não gerido.

Após a execução do kernel é sincronizado o contexto do dispositivo com o processo de anfitrião. Apesar da destruição do contexto libertar os recursos reservados, talvez constitua uma boa prática implementar a libertação dos recursos do contexto de modo a ser possível reutilizá-lo em computações futuras.

 

Publicado em Computadores e Internet | Etiquetas , , , | Publicar um comentário

A análise do ponto de vista histórico

Nestes últimos tempos tenho investigado alguns dos principais temas de análise com o intuito de expor as ideias básicas de um ponto de vista histórico. O texto, que se encontra em fase de desenvolvimento e será submetido a futuras actualizações, pode ser encontrado aqui, organizado por capítulos. Apesar de aí encontrarmos uma exposição mais detalhada, talvez seja pertinente o resumo de alguns dos tópicos mais interessantes. Exponho-os aqui numa ordem arbitrária.

Dirijo, em primeiro lugar, a atenção para a função gama que pode ser encarada como a função que define x!=x(x-1)(x-2)\cdots 1 para valores não inteiros de x. Numa carta de 1729 dirigida a Goldbach, Bernoulli sugeriu a seguinte função de interpolação para n!,

n!\approx \left(A+\frac{n}{2}\right)^{n-1}\left(\frac{2}{1+n}\cdot\frac{3}{2+n}\cdot\frac{4}{3+n}\cdots\frac{A}{A-1+n}\right)

O autor notou que, quanto maior for o valor de A, maior é a aproximação da expressão ao valor de n!. De facto, com n=3 e A=16, temos

3!\approx \left(16+\frac{2}{2}\right)^2\left(\frac{2}{17}\frac{3}{18}\right)=6\frac{1}{204}

isto é, proporciona-nos o valor de 3! a menos de um valor tão pequeno quanto \frac{1}{204}. Se n for um valor inteiro, verificamos facilmente que, de acordo com a expressão, temos

\lim_{k\to+\infty}\left\lbrack \frac{2\times 3\times\cdots\times n}{(k+1)(k+2)\cdots (k+n-1)}\left(k+\frac{n}{2}\right)^{n-1}\right\rbrack

Se notarmos que

\frac{1}{(k+1)(k+2)\cdots (k+n-1)}=\frac{k!}{(k+n-1)!}=\frac{1}{n!}\frac{k!}{(n+1)(n+2)\cdots (n+k-1)}

Podemos, portanto, definir

x!=\lim_{k\to +\infty}\left\lbrack \frac{k!}{(x+1)(x+2)\cdots(x+k-1)}\left(k+\frac{x}{2}\right)^{x-1}\right\rbrack

para qualquer valor de x>0. Convém notar que o autor quedou-se na primeira expansão e, portanto, podemos considerar que, apesar de estar próximo da definição de uma função de interpolação para o factorial de um número, não a escreveu explicitamente.
Uns dias mais tarde, Euler escreveu uma carta ao mesmo destinatário onde sugeriu a fórmula

x!=\lim_{k\to +\infty}\left\lbrack \frac{k!}{(n+1)(n+2)\cdots (n+k)}(k+1)^x\right\rbrack

e a demonstração de que realmente x! coincide com o valor do factorial de x quando x é um valor inteiro. Se observarmos os resultados de ambos os autores, concluímos que

x!=\lim_{k\to +\infty}\left\lbrack \frac{k!}{(x+1)(x+2)\cdots(x+k)}\left(k+\alpha(x)\right)^x\right\rbrack

onde \alpha(x) é uma função sempre finita de x. A consideração de \alpha(x)=0 conduz-nos à definição de Gauss, nomeadamente,

x!=\lim_{k\to +\infty}\left\lbrack \frac{k!k^x}{(x+1)(x+2)\cdots(x+k)}\right\rbrack

No seu trabalho sobre o cálculo integral, Legendre introduziu a notação \Gamma(x) para a função que satisfaz a relação de recorrência \Gamma(x+1)=x\Gamma(x), seguindo-se que \Gamma(x+1)=x! e, portanto,

\Gamma(x)=\lim_{k\to +\infty}\left\lbrack \frac{k!k^x}{x(x+1)(x+2)\cdots(x+k)}\right\rbrack

A representação integral \Gamma(x)=\int_0^{+\infty}t^xe^{-t} foi obtida por Euler, tendo-se inspirado no trabalho de Wallis sobre a quadratura da circunferência.

Como segundo ponto, considero aqui a expansão do binómio e algumas das suas consequências. No seu tratado sobre o triângulo aritmético, Pascal estudou a estrutura triangular de números que recebeu o seu nome. Nessa mesma obra publicou algumas das aplicações de tal estrutura, entre elas, encontrando-se a expansão do binómio com expoente inteiro positivo. De modo a determinar uma fórmula fechada para cada entrada do triângulo, Pascal observou que, sendo \binom{n}{k} o coeficiente binomial então vale a identidade

\frac{\binom{n}{k+1}}{\binom{n}{k}}=\frac{n-k}{k+1}

Como \binom{n}{0}=1, concluímos facilmente que

\binom{n}{k}=\frac{n!}{k!(n-k)!}

Uma vez conhecida a fórmula para os coeficientes binomiais, não é difícil mostrar a sua validade com base nas suas propriedades. Estas propriedades podem ser usadas para obter a expansão do binómio para valores inteiros do expoente. Estas mesmas propriedades permitem mostrar a identidade de Leibniz, nomeadamente,

\frac{d^n(fg)}{dx^n}=\sum_{i=0}^n\binom{n}{i}\frac{d^{n-i}f}{dx^{n-i}}\frac{d^ig}{dx^i}

A generalização da expansão do binómio a quaisquer valores do expoente foi obtida por Newton com o auxílio do seu método das fluxões aliado à teoria das equações infinitas. Apresentamos agora um esboço da sua demonstração, recorrendo aos trâmites do cálculo diferencial. Seja então f(x)=(x+1)^\alpha o binómio que pretendemos expandir. Sabemos que a sua derivada, f'(x) é dada por f'(x)=\alpha(x+1)^{\alpha-1}, isto é, a função f(x) satisfaz a seguinte equação diferencial

(1+x)f'(x)-\alpha f(x)=0

sujeita à condição fronteira f(0)=1. Procuramos, portanto, escrever f(x) na forma

f(x)=a_0+a_1 x+a_2 x^2+\cdots+a_nx^n+\cdots

Se substituirmos esta fórmula na equação diferencial anterior obtemos

0=\left(a_1-\alpha a_0\right)+\left(2a_2+a_1-\alpha a_1\right)x+\left(3a_3+2a_2-\alpha a_2\right)x^2+\cdots+\left\lbrack (n+1)a_{n+1}+(n-\alpha)a_n\right\rbrack x^n+\cdots

A equação vale para qualquer valor de x se se anularem todos os coeficientes, isto é,

\left\lbrace \begin{matrix} a_1-\alpha a_0=0\\ 2a_2+(1-\alpha)a_1=0\\ 3a_3+(2-\alpha)a_2=0\\ \vdots\\ (n+1)a_{n+1}+(n-\alpha)a_n=0\\ \cdots \end{matrix} \right.

A solução do sistema proporciona-nos

a_k=\binom{\alpha}{k}=\frac{\alpha(\alpha-1)\cdots(\alpha-k+1)}{k!}

e, portanto, de um modo geral,

(x+1)^\alpha=\sum_{k=0}^{+\infty}\binom{\alpha}{k}x^k

Um método semelhante permitiu ao mesmo autor obter a expansão das funções trigonométricas. Para o efeito, começou por considerar uma partícula que se move com um movimento circular uniforme ao longo de uma circunferência de raio uniátio de equação x^2+y^2=1. Sabemos que tal movimento é o resultado da sobreposição dos movimentos coordenados

\left\lbrace \begin{matrix} x=\cos t\\ y=\sin t \end{matrix} \right.

Como a partícula se move com velocidade unitária, então

\frac{ds}{dt}=1=\sqrt{\left(\frac{dx}{dt}\right)^2+\left(\frac{dy}{dt}\right)^2}

representado t, neste caso, o ângulo associado ao deslocamento da partícula medido em radianos. A derivação da equação da circunferência, x^2+y^2=1 conduz-nos a

x\frac{dx}{dt}+y\frac{dy}{dt}=0

que, substituído na equação \frac{ds}{dt}=1 resulta em

\frac{dy}{dt}=\sqrt{1-y^2}

A expansão do binómio \sqrt{1-y^2}=\left(1-y^2\right)^{\frac{1}{2}} permite-nos obter uma série para a função t=\arcsin y. Podemos, porém, obter uma expansão para y=\sin t do seguinte modo. Começamos por derivar a equação anterior, advindo

\frac{d^2y}{dt^2}=-\frac{y}{\sqrt{1-y^2}}\frac{dy}{dy}=-y

ou \frac{dy}{dt}+y=0. Observamos ainda que y(0)=0 e \frac{dy}{dt}(t=0)=1. Se considerarmos que

\sin t=a_0+a_1t+a_2t^2+a_3t^3+\cdots+a_nt^n

a sua substituição na equação diferencial aliada à consideração das condições iniciais conduz-nos à expansão

\sin t=\sum_{k=0}^{+\infty}(-1)^k\frac{x^{2k+1}}{(2k+1)!}

O mesmo raciocínio poderá ser utilizado para obter a expansão

\cos t=\sum_{k=0}^{+\infty}(-1)^k\frac{x^{2k}}{(2k)!}

Como terceiro ponto, considero a fórmula de De Moivre, a qual foi originalmente obtida com o auxílio do método das fluxões com um raciocínio em muito semelhante ao que foi utilizado nas expansões consideradas no ponto anterior. De acordo com esta fórmula, temos

\left(\cos x+i\sin x\right)^n=\cos(nx)+i\sin(nx)

Esta fórmula mostra-se facilmente com o auxílio das fórmulas da soma para as funções trigonométricas

\cos(x+y)=\cos x\cos y-\sin x\sin y

e

\sin(x+y)=\cos x\sin y+\cos y\sin x

De facto, a utilização de tais fórmulas permite-nos escrever

\left(\cos x + i\sin x\right)\left(\cos y + i\sin y\right)=\cos(x+y)+i\sin(x+y)

A aplicação recursiva da mesma, com y=x proporciona-nos o resultado enunciado. A demonstração original, apesar de ser mais complicada, permite, mais uma vez, proporcionar-nos uma aplicação do cálculo diferencial. Se definirmos y=\sin(n\theta) e x=\sin\theta, segue-se que

\frac{dy}{d\theta}=n\sqrt{1-y^2}

e

\frac{dx}{d\theta}=\sqrt{1-x^2}

de onde concluímos a expressão

\frac{dy}{dx}=\frac{n\sqrt{1-y^2}}{\sqrt{1-x^2}}

Se fizermos agora as substituições y=i\eta e x=i\xi obtemos

\frac{d\eta}{d\xi}=\frac{\sqrt{1+\eta^2}}{1+\xi^2}

Vejamos que equação diferencial satisfará a função

\varphi=\sqrt{1-\eta^2}+\eta.

Se derivarmos, obtemos a equação separável

\frac{d\varphi}{d\xi}=\frac{n\varphi}{\sqrt{1+\xi^2}}

cuja solução a escolher é aquela que satisfaz a condição \varphi(0)=1. A solução da equação diferencial proporciona-nos a solução

\varphi=\left(\sqrt{1+\xi^2}+\xi^2\right)^n

que, uma vez que \xi=i\sin\theta e \eta=i\sin(n\theta) constitui a fórmula apresentada.

No quarto ponto discutimos a fórmula de Euler de um ponto de vista histórico. No seu artigo de 1702, Bernoulli submeteu um artigo sobre a integração de fracções racionais com base na expansão em fracções parciais. Como exemplo de aplicação, observou que, da identidade,

\frac{1}{1+x^2}=\frac{1}{2}\frac{1}{1+ix}+\frac{1}{2}\frac{1}{1-ix}

se segue o integral

\int \frac{dx}{1+x^2}=\frac{1}{2i}\log\frac{1+ix}{1-ix}

Por outro lado, se aplicarmos a transformação x=\sqrt{1/t^2-1} ao mesmo integral obtemos

\int\frac{dt}{\sqrt{1-t^2}}=\int\frac{dx}{\sqrt{1+x^2}}

O autor pode observar, deste modo, uma relação entre a função logaritmo e a quadratura do círculo. Porém, coube a Euler, numa carte endereçada a Maupertuis, a aplicação do mesmo método para obter

\int_0^1\frac{dx}{1+x^2}=\int_1^\frac{\sqrt{2}}{2}\frac{dt}{\sqrt{1-t^2}}=\frac{\pi}{4}

isto é,

\log\frac{1+i}{1-i}=\log i=i\frac{\pi}{2}

A exponenciação da identidade anterior conduz-nos à interessante fórmula

i=e^{i\frac{\pi}{2}}

A fórmula mais geral pode ser obtida a partir da conhecida expansão para a função exponencial

e^x=\sum_{n=0}^{+\infty}\frac{x^n}{n!}

substituindo x por ix.

Relativamente ao quinto ponto, será pertinente enveredar por um tema que é frequentemente associado à análise complexa mas que talvez tenha sido iniciado no âmbito da análise real. Estamos aqui a referir-nos à teoria dos resíduos. Nas suas lições sobre o cálculo das funções, Lagrange expos uma teoria das funções analíticas com base na expansão em série de Taylor com resto. Por exemplo, por um processo de interpolação, podemos obter

\frac{1}{i}=\frac{1}{x}-\frac{i-x}{x^2}+\frac{(i-x)^2}{x^3}-\frac{(i-x)^3}{x^4}+\cdots+(-1)^{n-1}\frac{(i-x)^{n-1}}{x^n}+(-1)^n\frac{(i-x)^{n+1}}{ix^n}

onde i aqui não representa a unidade imaginária. Trata-se da expansão da função f(x)=\frac{1}{x} em torno do ponto x. Facilmente constatamos que a expressão não é válida quando fazemos x=0 e dizemos que a função nesse ponto não é analítica. O autor observou que as funções habituais são analíticas em toda a parte com excepção de alguns casos pontuais. Em particular, uma função racional \frac{p(x)}{q(x)} é analítica em todos os pontos com excepção dos zeros de q(x). Suponhamos agora, como o fez Cauchy, que q(x)=(x-\alpha)^mq_1(x) onde q_1(\alpha)\ne0. Segue-se que

f(x)=\frac{p(x)}{q(x)}=\frac{g(x)}{(x-\alpha)^m}

onde g(x) é uma função analítica no ponto \alpha, podendo não o ser, com efeito, em outros pontos. Vale, portanto, a expansão

g(x)=g(\alpha)+g'(\alpha)(x-\alpha)+\frac{g''(\alpha)}{2!}(x-\alpha)^2+\cdots+\frac{g^{(m-1)}}{(m-1)!}(x-\alpha)^{m-1}+\psi(x)(x-\alpha)^m

sendo \psi(\alpha) assume um valor finito. Vemos, deste modo, que

f(x)=\frac{g(x)}{(x-\alpha)^m}=\psi(x)+\sum_{k=1}^m\frac{a_k}{(x-\alpha)^k}

onde

\lim_{x\to\alpha}\frac{d^{m-k}}{dx^{m-k}}\frac{(x-\alpha)^mf(x)}{(m-k)!}

É útil notar que se \beta\ne\alpha for um zero de ordem n de q(x) então

f(x)=\psi_1(x)+\sum_{k=1}^n\frac{b_k}{(x-\beta)^k}+\sum_{k=1}^m\frac{a_k}{(x-\alpha)^k}

onde os coeficientes b_k se calculam a partir de f(x) por intermédio da mesma expressão. Definimos, assim, um processo para determinar a expansão em fracções parciais de uma função racional. Porém, tal método requer a determinação de \alpha+\beta+\cdots coeficientes onde \alpha, \beta, … são as ordens dos zeros do denominador. Para simplificarmos o processo observamos que, por exemplo, o coeficiente de ordem -2 de f(x) é igual ao coeficiente de ordem -1 de (x-\alpha)f(x). Esta observação permite construir uma função h(z,x) de tal forma que, sendo a_{-1}(x) o coeficiente de ordem -1 de h(z,x) em torno do ponto z=\alpha, temos

f(x)=\psi(x)+a_{-1}(x)

Ao valor a_{-1} de f(x) damos a designação de resíduo de f(x) no ponto \alpha e denotamos por Res(f(x),x=\alpha). Mostra-se que, sendo x_i com i=1,2,\cdots,n os zeros de q(x),

f(x)=Res\left(\frac{f\left(\frac{1}{z}\right)}{z(1-xz)},z=0\right)+\sum_{i=1}^nRes\left(\frac{f(z)}{x-z},z=x_i\right)

Uma das demonstrações do teorema fundamental da álgebra consiste em mostrar que, sendo p(x) um polinómio não constante então, fazendo f(x)=\frac{1}{p(x)}, temos sempre, se considerarmos o conjunto dos números complexos,

Res\left(\frac{f\left(\frac{1}{z}\right)}{z(1-xz)},z=0\right)=0

e, portanto,

f(x)=\sum_{i=1}^nRes\left(\frac{f(z)}{x-z},z=x_i\right)

Como f(x) é não constante, deverá existir pelo menos um x_i tal que p\left(x_i\right)=0. De facto, de acordo com o teorema de Liouville, se f(x) for uma função analítica então terá de ser constante. Este teorema implica imediatamente que se p(x) não for constante então f(x) não é analítica de onde se segue que deverá existir um x_i tal que p\left(x_i\right)=0. É útil notar ainda que o teorema foi originalmente enunciado pelo autor no decurso do seu estudo das funções duplamente periódicas sobre o qual construíu a sua teoria das funções elípticas.

Como sexto ponto, não incluirei qualquer resultado mas farei alguams observações históricas que considero curisoas. Começando com Ampère, este é sobejamente conhecido pelo trabalho que desenvolveu no âmbito do electromagnetismo e, em particular, pelo seu estudo sobre o campo magnético produzido por correntes eléctricas. Dois dos seus interesses são menos conhecidos. O primeiro (vide), prende-se com a teoria que aventou, independentemente de Avogadro, sobre a constituição molecular da matéria. O segundo resume-se aos seus trabalhos em matemática. Num conjunto de artigos, trabalhou o estendeu o método das características de Monge para a resolução de equações diferenciais em derivadas parciais. Num artigo de 1806, o mesmo autor apresentou a sua teoria das funções analíticas no qual é frequentemente considerar-se ter demonstrado o resultado erróneo de que uma função contínua é diferenciável em toda a parte com a excepção de um número finito de pontos. No entanto, o objectivo do artigo prende-se com a simplificação da teoria de Lagrange das funções analíticas. O seu ponto de partida para o estabelecimento da existência da derivada de uma função é o seguinte:
Uma função da variável x advém nula ou infinita quando x tende para zero se a função diminuir ou aumentar quando x se aproxima de zero de modo que seja inferior a qualquer quantidade dada no primeiro caso e superior a qualquer quantidade dada no segundo.
Além disso, considerou que uma função não é derivável se a sua derivada for infinita. Neste caso, as funções que oscilam indefinidamente numa vizinhança do ponto, tal como \frac{1}{x}\sin\left(\frac{1}{x}\right) não poderão ser consideradas uma vez que apesar do seu valor não estar definido na origem, para qualquer quantidade dada existem sempre valores de x tais que o valor da função é inferior a essa quantidade. Além disso, o conceito de continuidade surgiria cerca de onze anos mais tarde.
Como segunda curiosidade destaco a opinião de Rolle, introdutor de um dos mais importantes teoremas em análise, que era um fervoroso crítico dessa disciplina. Este autor expos o teorema que recebe o seu nome no âmbito da teoria a que designou por método das cascatas. Este método, apesar de ser muito semelhante ao do cálculo diferencial em termos algorítmicos, distanciava-se deste em termos metafísicos. A sua refutação por parte Varignon ajudou a clarificar alguns aspectos mais obscuros do cálculo diferencial. Por outro lado, incutiu a necessidade de elevar o rigor da disciplina.
Por fim, é interessante lembrar o papel do matemático José Anastácio da Cunha que antecipou, nos seus Principios Mathematicos, alguns dos resultados que viriam, muitos anos mais tarde, a ser atribuídos a outros autores. Entre estes, refiro a noção de convergência de uma série que lhe permitiu construir um estudo rigoroso das funções exponencial e logaritmica.

Publicado em Matemática, Sem categoria | Etiquetas , , , , , , , , , , | 1 Comentário

Migração de dados com o SSIS no modo diferencial

Parte do trabalho que agora desenvolvo prende-se com o tratamento de dados oriundos de um conjunto heterogéneo de fontes com o auxílio do SSIS. Um método para a actualização de uma determinada base-de-dados central consiste em eliminar todos os registos e actualizá-la com os dados mais recentes, o qual requer o movimento de uma enorme quantidade de dados sempre que se pretenda proceder à actualização. Um segundo método consiste apenas na migração dos dados associados às diferenças. Este método, apesar de ser mais elaborado do ponto de vista técnico, tem a vantagem de ser mais eficaz.

Partilhei o ficheiro Migração de dados no modo diferencial.zip no qual exponho o método diferencial sobre um conjunto de ficheiros de XML e onde tentei expor algumas técnicas que permitem a fácil generalização a outros cenários.

Publicado em Computadores e Internet, Sem categoria | Etiquetas , , , , , , | Publicar um comentário

Um texto e uma tradução

O facto de aqui ter deixado de publicar textos em matemática com a mesma regularidade que antes  não significa que tenha deixado de me interessar ou sequer de ter tempo para a disciplina. Com efeito, vou coleccionando várias ideias em rascunhos, à medida que o tempo mo permite, que pretendo partilhar quando considerar que têm um certo grau de maturidade.

Ultimamente tenho implementado alguns dos algoritmos mais conhecidos, nomeadamente no âmbito da álgebra linear e teoria das matrizes. À medida que o vou fazendo, vou anotando as ideias teóricas de um ponto de vista que me parece não ser excessivamente abstracto. Daqui resultou o Alguns resultados em teoria das matrizes onde expus algumas dessas ideias, deixando a nota de que o texto poderá ser continuamente enriquecido.

Para além disso, terminei a tradução que havia começado há algum tempo atrás do artigo seminal de Poincaré, Analysis Situs, que se diz ter revolucionado a área de topologia. Como se trata de um artigo que incluiu cinco suplementos subsequentes, trata-se de uma tradução que poderá ser aumentada.

Publicado em Matemática | Etiquetas , , , , | Publicar um comentário