Sul calcolo delle funzioni elementari

Messaggioda sellacollesella » 24/12/2023, 22:11

PREMESSA

Leggendo alcune discussioni in vari forum con un bacino d'utenza più o meno grande, ho notato che la maggior parte delle persone è nella mia stessa situazione, ossia non ha la più pallida idea di come vengano effettivamente computate le funzioni elementari, perlomeno quelle di base quali seno, coseno, arcotangente, esponenziale naturale e logaritmo naturale.

Pertanto, ho pensato di condividere con voi alcune nozioni che ho messo da parte in questi ultimi giorni di studio autonomo, con la ovvia premessa di prendere tutto con il beneficio del dubbio, essendo esperto del nulla avrò compreso tali nozioni sicuramente in modo ingenuo, senza solide conoscenze di matematica e di informatica che indubbiamente sono necessarie per mettere in piedi un discorso serio.


NEL PASSATO

Un primo modo per computare tali funzioni elementari consiste di sole addizioni, sottrazioni, spostamento di bit e tabelle di ricerca, indispensabile quando non è disponibile alcun moltiplicatore hardware (ad esempio, in semplici microcontrollori e FPGA). Ecco, questo è ciò che fa CORDIC, algoritmo ideato da Volder nel 1956 per le funzioni seno e coseno, quindi esteso da Walther nel 1971 alle funzioni iperboliche, esponenziali e logaritmi naturali, ... che ha permesso sostanzialmente il lancio della prima calcolatrice tascabile, ossia la mitica HP-35.

D'altro canto, oggigiorno la tecnologia a diposizione è decisamente più avanzata rispetto a 50 anni fa, quindi non è affatto detto che sia necessario passare per il suddetto algoritmo, bensì si può optare a qualcosa di più efficiente. Nella fattispecie, l'idea alla base è cominciare scopiazzando da CORDIC, ossia attraverso una riduzione di portata dell'input ricondursi ad un calcolo equivalente internamente ad uno specifico intervallo dove l'algoritmo converge, quindi passare per un'approssimazione di tipo polinomiale oppure di tipo razionale.


NEL PRESENTE

È naturale che il pensiero corra immediatamente alle approssimazioni di Taylor nel caso polinomiale e alle approssimazioni di Padé nel caso razionale, in quanto sono di semplice calcolo e rispetto le quali abbiamo una discreta dimestichezza dovuta al fatto che nell'analisi matematica sono ovunque come il prezzemolo. D'altro canto, tali scelte non sono le migliori possibili in questo ambito, in particolare hanno la meglio i cosiddetti polinomi minimax (o le rispettive approssimazioni razionali). I motivi sono essenzialmente due: a parità di grado offrono un errore massimo inferiore e inoltre l'errore è spalmato uniformemente in un dato intervallo, a differenza dei precedenti che sono ottimi nell'intorno del centro, ma scadenti verso gli estremi dell'intervallo.

L'altra faccia della medaglia, però, è che la determinazione di quei benedetti polinomi minimax non è banale e lo standard al riguardo porta all'algoritmo di Remez, il quale dati in input la funzione elementare e l'intervallo in cui si desidera approssimarla, quindi il grado del polinomio approssimante, tramite un processo iterativo calcola i coefficienti di tale polinomio, che sottostando al teorema dell'equioscillazione ne garantisce l'unicità.


POLINOMI MINIMAX

Un codice piuttosto succinto, seppur non molto robusto, è il seguente:

Codice:
f[x_] = Sin[x];
{a, b, n} = {-Pi/4, Pi/4, 10};

u = Table[i, {i, a, b, (b - a)/(n + 1)}];
Do[p[x_] = Sum[StringJoin["c", ToString[i]] x^i, {i, 0, n}];
   v = Table[p[u[[i]]] - f[u[[i]]] == (-1)^i k, {i, n + 2}];
   p[x_] = p[x] /. NSolve[v][[1]];
   w = NSolve[{TrigToExp[D[p[x] - f[x], x]] == 0, a < x < b}];
   u = Sort@N@Join[{a, b}, w[[All, 1, 2]]], {j, 20}];

Chop@p[x]
Plot[p[x] - f[x], {x, a, b}, AxesLabel -> {"x", "p(x)-f(x)"}]

che lanciato in Wolfram Mathematica sputa quanto segue:

Codice:
0.999999999976264 x -
0.16666666589651935 x^3 +
0.008333326335356108 x^5 -
0.0001983867334444072 x^7 +
0.000002713535577336344 x^9

Immagine

a differenza di quanto si otterrebbe con la rispettiva approssimazione di Taylor:

Codice:
x -
0.16666666666666666 x^3 +
0.008333333333333333 x^5 -
0.0001984126984126984 x^7 +
0.0000027557319223985893 x^9

Immagine


FUNZIONI ELEMENTARI PRIMITIVE

Ora abbiamo tutti gli strumenti per scrivere dei "codici giocattolo" circa le funzioni elementari di base:

Codice:
sin[x_] := Module[{n, pi, y, siny, cosy},
        {n, pi, y} = {0, 3.141592653589793, x};
        While[y < -pi/4, n = n + 1; y = y + pi/2];
        While[y > +pi/4, n = n - 1; y = y - pi/2];
        siny = 0.999999999976264 y - 0.16666666589651935 y^3 +
               0.008333326335356108 y^5 - 0.0001983867334444072 y^7 +
               0.000002713535577336344 y^9;
        cosy = 0.9999999999999444 - 0.4999999999935193 y^2 +
               0.04166666654402005 y^4 - 0.0013888880398758362 y^6 +
               0.000024798929682192885 y^8 - 0.00000027173490012904213 y^10;
        If[Mod[n, 4] == 0, siny, If[Mod[n, 4] == 1, -cosy, If[Mod[n, 4] == 2, -siny, cosy]]]]

Codice:
cos[x_] := Module[{n, pi, y, siny, cosy},
        {n, pi, y} = {0, 3.141592653589793, x};
        While[y < -pi/4, n = n - 1; y = y + pi/2];
        While[y > +pi/4, n = n + 1; y = y - pi/2];
        siny = 0.999999999976264 y - 0.16666666589651935 y^3 +
               0.008333326335356108 y^5 - 0.0001983867334444072 y^7 +
               0.000002713535577336344 y^9;
        cosy = 0.9999999999999444 - 0.4999999999935193 y^2 +
               0.04166666654402005 y^4 - 0.0013888880398758362 y^6 +
               0.000024798929682192885 y^8 - 0.00000027173490012904213 y^10;
        If[Mod[n, 4] == 0, cosy, If[Mod[n, 4] == 1, -siny, If[Mod[n, 4] == 2, -cosy, siny]]]]

Codice:
arctan[x_] := Module[{m, n, pi, y, arctany},
           {m, n, pi, y} = {0, 1, 3.141592653589793, x};
           If[y < 0, n = -1; y = -x];
           If[y > 1, m = 1; y = 1/y];
           arctany = 0.9999999146206229 y + 0.000004440916687621521 y^2 -
                     0.3334229328258918 y^3 + 0.0009160176902325137 y^4 +
                     0.19467852030293517 y^5 + 0.01810645296260239 y^6 -
                     0.17583841962361396 y^7 + 0.011728511647071702 y^8 +
                     0.1956314075888416 y^9 - 0.19507021552110818 y^10 +
                     0.08234872936245016 y^11 - 0.013684264265137207 y^12;
           If[m == 0, n arctany, n (pi/2 - arctany)]]

Codice:
exp[x_] := Module[{n, log2, y, expy},
        {n, log2, y} = {1, 0.6931471805599453, x};
        While[y < 0, n = n/2; y = y + log2];
        While[y > log2, n = 2 n; y = y - log2];
        expy = 0.9999999999999809 + 1.0000000000054843 y +
               0.49999999974080833 y^2 + 0.16666667141752853 y^3 +
               0.041666622484762016 y^4 + 0.008333568911715338 y^5 +
               0.0013881265875033293 y^6 + 0.00019993071014806345 y^7 +
               0.00002299592844852003 y^8 + 0.000003910027293592432 y^9;
        n expy]

Codice:
log[x_] := Module[{n, log2, y, logy},
        {n, log2, y} = {0, 0.6931471805599453, x};
        While[y < 1, n = n - 1; y = 2 y];
        While[y > 2, n = n + 1; y = y/2];
        logy = -2.758798873492117 + 8.482177884092106 y -
               16.404818190077595 y^2 + 25.509315252813245 y^3 -
               29.970734938543238 y^4 + 26.57580667306714 y^5 -
               17.810152466522702 y^6 + 8.975140918706439 y^7 -
               3.350162015718218 y^8 + 0.8989936219430049 y^9 -
               0.16408089302388532 y^10 + 0.01824637444011346 y^11 -
               0.0009333476666561241 y^12;
        n log2 + logy]


FUNZIONI ELEMENTARI DERIVATE

A sua volta, risulta possibile definite tutte le altre funzioni elementari, tra le quali: \[
\begin{aligned}
& \tan(x) = \frac{\sin(x)}{\cos(x)}, \quad
\log_a(x) = \frac{\log(x)}{\log(a)}, \quad
x^y = e^{y\log(x)}, \\
& \text{surd}(x,n) =
\begin{cases}
-(-x)^{\frac{1}{n}} & \text{se} \; x < 0 \, \land \, \text{mod}(n,2) = 1 \\
x^{\frac{1}{n}} & \text{se} \; x > 0 \\
\end{cases}, \\
& \arcsin(x) = \arctan\left(\frac{x}{\left(1-x^2\right)^{\frac{1}{2}}}\right), \quad
\arccos(x) = \frac{\pi}{2} - \arcsin(x), \\
& \sinh(x) = \frac{e^x-e^{-x}}{2}, \quad
\cosh(x) = \frac{e^x+e^{-x}}{2}, \quad
\tanh(x) = \frac{\sinh(x)}{\cosh(x)}, \\
& \text{arcsinh}(x) = \log\left(x+\left(x^2+1\right)^{\frac{1}{2}}\right), \quad
\text{arccosh}(x) = \log\left(x+\left(x^2-1\right)^{\frac{1}{2}}\right), \\
& \text{arctanh}(x) = \frac{\log(1+x)-\log(1-x)}{2}.
\end{aligned}
\]
CONCLUSIONI

Va da sé che tali funzioni siano state implementate in modo molto ingenuo, in particolare ciò può essere visto come un punto di partenza per un'analisi seria, specie nella fase di rimodulazione dell'input in cui per numeri molto grandi può verificarsi una sensibile perdita di precisione, a cui poi andranno affiancati tutta una serie di accorgimenti per fare in modo che eventuali input non consentiti siano immediatamente arrestati e così via.

Con questo io avrei finito lo sproloquio, se a qualcuno facesse piacere aggiungere qualche osservazione o qualche invettiva causa le troppe stupidaggini scritte lo faccia pure, non faranno altro che bene, soprattutto per coloro che un domani dovessero incappare qui tramite i motori di ricerca. Buon Natale a tutti voi! :smt114
sellacollesella
Average Member
Average Member
 
Messaggio: 579 di 959
Iscritto il: 08/04/2022, 12:43

Re: Sul calcolo delle funzioni elementari

Messaggioda obnoxious » 25/12/2023, 14:41

https://en.wikipedia.org/wiki/Lookup_table

Sono abbastanza sicuro che le LUTs vengano utilizzate in prodotti industriali. O almeno ho sentito parecchie volte i tizi delle FPGAs e DSPs parlarne.
Morì mentre noialtri fumavamo nel cortile
obnoxious
Average Member
Average Member
 
Messaggio: 347 di 681
Iscritto il: 22/03/2019, 11:45

Re: Sul calcolo delle funzioni elementari

Messaggioda sellacollesella » 26/12/2023, 11:23

Ciao obnoxious, innanzitutto grazie per l'intervento!

In verità, questo mio interesse sul calcolo delle funzioni elementari, perlomeno in modo superficiale, è nato qualche anno fa quando sentii parlare per la prima volta di CORDIC e ne rimasi subito affascinato, in quanto poi a catena mi documentai sulla HP-35 e così via, tutte cose su cui non sapevo davvero niente!

D'altro canto, poi questo interesse fu messo in un cassetto perché ero sommerso dagli esami universitari e quindi avevo altri grattacapi a cui dovermi dedicare. In quest'ultima settimana, invece, essendomi preso qualche giorno di pausa dai libri universitari è riemerso ancora più pulsante il desiderio di approfondire la suddetta questione e il papiro di cui sopra ne è una diretta conseguenza.

Ecco che allora il tuo intervento mi ha fatto rivalutare le tabelle di ricerca, perché a pelle mi sembravano un metodo troppo grezzo legato a quanto si faceva manualmente prima dell'avvento delle calcolatrici tascabili e quindi, al più, ottimizzato da CORDIC. Invece, a quanto pare, quando la velocità ha la priorità sulla precisione ha il suo perché, anzi, non è affatto male! Infatti, ho provato a scrivere un codice per il logaritmo naturale:

Codice:
log[x_] := Module[{m, n, y, z, logz},
  {m, n, y} = {0, 1, x};
  While[y < 1, m = m - 1; y = 2 y];
  While[y > 2, m = m + 1; y = y/2];
  z = {1.00, 1.01, 1.02, 1.03, 1.04, 1.05, 1.06, 1.07, 1.08, 1.09,
       1.10, 1.11, 1.12, 1.13, 1.14, 1.15, 1.16, 1.17, 1.18, 1.19,
       1.20,  1.21, 1.22, 1.23, 1.24, 1.25, 1.26, 1.27, 1.28, 1.29,
       1.30, 1.31, 1.32, 1.33, 1.34, 1.35, 1.36, 1.37, 1.38, 1.39,
       1.40, 1.41, 1.42, 1.43, 1.44, 1.45, 1.46, 1.47, 1.48, 1.49,
       1.50, 1.51, 1.52, 1.53, 1.54, 1.55, 1.56, 1.57, 1.58, 1.59,
       1.60, 1.61, 1.62, 1.63, 1.64, 1.65, 1.66, 1.67, 1.68, 1.69,
       1.70, 1.71, 1.72, 1.73, 1.74, 1.75, 1.76, 1.77, 1.78, 1.79,
       1.80, 1.81, 1.82, 1.83, 1.84, 1.85, 1.86, 1.87, 1.88, 1.89,
       1.90, 1.91, 1.92, 1.93, 1.94, 1.95, 1.96, 1.97, 1.98, 1.99, 2.00};
  logz = {0, 0.00995033, 0.0198026, 0.0295588, 0.0392207, 0.0487902,
          0.0582689, 0.0676586, 0.0769610, 0.0861777, 0.0953102, 0.104360,
          0.113329, 0.122218, 0.131028, 0.139762, 0.148420, 0.157004,
          0.165514, 0.173953, 0.182322, 0.190620, 0.198851, 0.207014,
          0.215111, 0.223144, 0.231112, 0.239017, 0.246860, 0.254642,
          0.262364, 0.270027, 0.277632, 0.285179, 0.292670, 0.300105,
          0.307485, 0.314811, 0.322083, 0.329304, 0.336472, 0.343590,
          0.350657, 0.357674, 0.364643, 0.371564, 0.378436, 0.385262,
          0.392042, 0.398776, 0.405465, 0.412110, 0.418710, 0.425268,
          0.431782, 0.438255, 0.444686, 0.451076, 0.457425, 0.463734,
          0.470004, 0.476234, 0.482426, 0.488580, 0.494696, 0.500775,
          0.506818, 0.512824, 0.518794, 0.524729, 0.530628, 0.536493,
          0.542324, 0.548121, 0.553885, 0.559616, 0.565314, 0.570980,
          0.576613, 0.582216, 0.587787, 0.593327, 0.598837, 0.604316,
          0.609766, 0.615186, 0.620576, 0.625938, 0.631272, 0.636577,
          0.641854, 0.647103, 0.652325, 0.657520, 0.662688, 0.667829,
          0.672944, 0.678034, 0.683097, 0.688135, 0.693147};
  While[z[[n]] < y, n = n + 1];
  m logz[[101]] + logz[[n - 1]] + (y - z[[n - 1]]) (logz[[n]] - logz[[n - 1]])/(z[[n]] - z[[n - 1]])]

Plot[Log[x] - log[x], {x, 10^-3, 10^3}, AxesLabel -> {"x", "ε(x)"}]

che se lanciato in Wolfram Mathematica sputa quanto segue:

Immagine
da cui si evince un errore massimo dell'ordine di \(10^{-5}\), cosa a cui non avrei mai creduto! :o

Però, se poi confronto il tempo per tracciare quest'ultimo grafico, tramite polinomio impiega \(0.10\,s\) mentre tramite tabelle impiega \(0.30\,s\), per cui credo di non aver implementato a dovere quest'ultimo codice! :smt012
sellacollesella
Average Member
Average Member
 
Messaggio: 588 di 959
Iscritto il: 08/04/2022, 12:43

Re: Sul calcolo delle funzioni elementari

Messaggioda feddy » 03/01/2024, 18:25

Non ho letto tutta la discussione, ma per rispondere a quesiti come questo spesso conviene guardare direttamente ai sorgenti. Ovviamente in principio è tutto "compiler-dependent"... :)

Cercando su google (C/C++): https://stackoverflow.com/questions/499 ... tandard-li
Avatar utente
feddy
Moderatore
Moderatore
 
Messaggio: 3049 di 5934
Iscritto il: 26/06/2016, 00:25
Località: SISSA


Torna a Analisi Numerica e Ricerca Operativa

Chi c’è in linea

Visitano il forum: Nessuno e 1 ospite