Lorsque qu’une société, quel que soit son secteur d’activité, doit industrialiser (ou mettre en production) une application impliquant une partie calculatoire stratégique (algèbre linéaire, optimisation, régression, prévisions, séries temporelles, datamining, statistiques avancées etc…), il est largement admis qu’une phase de prototypage est nécessaire voire indispensable en amont. En effet, cette phase de prototypage va permettre de rapidement tester différents scénarios et de valider les modèles ou les techniques mathématiques et statistiques qui seront privilégiés avant d’être mis en production.
Il apparait que pour la plus grande majorité ces organisations, les outils d’analyse numérique utilisés lors de la phase de prototypage sont complètement différents de ceux utilisés par l’application finale. Ce choix est légitime puisque les exigences sont différentes de part et d’autre. En amont (phase de prototypage), on a besoin de développer rapidement, de pouvoir facilement basculer d’une hypothèse à une autre (pour mon cas, cet algorithme est-il plus approprié que celui-ci ?), d’importer et de visualiser un jeu de données sans avoir à écrire trop de lignes de code. En aval (mise en production), les exigences vont davantage être axées sur les performances de la partie calculatoire (vitesse d’exécution), sur sa robustesse (capacité à traiter des gros volumes de données et à gérer efficacement des problèmes de divergences numériques par exemple) et sa fiabilité (précision des résultats). Oui, il y a clairement un hiatus entre ces 2 mondes. La technique la plus naturelle pour passer d’un monde à l’autre consiste à recoder le prototype (au moins la partie analytique) dans le langage imposé par l’application finale. Mais n’y aurait-t-il pas une approche plus raisonnable, plus productive et plus sûr s’agissant de la partie analytique ?
Elle consiste à utiliser strictement le même « moteur de calculs » pour le prototype et pour l’application finale.
Python.
Dans la pratique, il n’est en effet pas toujours facile d’assembler soi-même les différents composants open-source Python entre eux : on doit les télécharger depuis différents sites, vérifier que les versions soient compatibles entre elles et la documentation est parfois de qualité inégale. De plus, aucun support technique n’est assuré.
L’environnement de prototypage PyIMSL Studio est supporté et entièrement documenté et a en partie été conçu pour pallier cette lacune lié à l’hétérogénéité des composants : un click sur la fenêtre de bienvenue de PyIMSL Studio suffit pour installer tout ce petit monde sur votre machine, en assurant que l’ensemble soit cohérent :
Bien sûr. Il nous faut donc commencer par la phase de prototypage en Python. Au choix, on peut créer un projet Python avec l’EDI Eclipse comme ci-dessous,
ou travailler directement en ligne de commandes interactives avec IPython :
La 1ère chose à vérifier est que l’on a bien accès à l’ensemble des algorithmes d’IMSL C depuis Python en exécutant la commande import imsl.test sous une console IPython, ou depuis la ligne de commandes Python. La figure suivante illustre le résultat attendu :
Nous allons maintenant pouvoir envisager un cas concret minimaliste qui montrera les avantages de notre solution. Il s’agit de mettre en production une application devant minimiser une fonction non linéaire soumise à des contraintes non linéaires d’égalité et d’inégalité. Notre problème mathématique se formule de la manière suivante :
Ce choix n’est pas tout à fait anodin : en effet, développer un tel solveur de manière fiable, performante et robuste (et dans 2 langages différents !) nécessite une très grande expertise en analyse numérique.
Côté prototypage, nous allons utiliser le solveur constrainedNlp(). Il s’agit d’une API Python vers le solveur IMSL C constrained_nlp(). De fait, on s’affranchit du re-développement risqué d’un tel algorithme. Le prototype Python qui en découle est le suivant :
# Imports necessaires
from numpy import *
from imsl.math.constrainedNlp import constrainedNlp
from imsl.stat.machine import machine
from imsl.math.writeMatrix import writeMatrix
# Definition de la fonction a minimiser et des contraintes
def fcn(n, x, iact, result, ierr):
tmp1 = x[0] - 2.0e0
tmp2 = x[1] - 1.0e0
if iact == 0:
result[0] = tmp1 * tmp1 + tmp2 * tmp2
elif iact == 1:
result[0] = x[0] - 2.0e0 * x[1] + 1.0e0
elif iact == 2:
result[0] = -(x[0]*x[0]) / 4.0e0 - x[1]*x[1] + 1.0e0
ierr = 0
n = 2 # Nb de variables
m = 2 # Nb total de contraintes
meq = 1 # Nb de contraintes d'egalite
ibtype = 0 # Type de bornes (fourni plus bas)
inf = 1e300000
xlb = [-inf, -inf] # Bornes inferieures fixees a -infini
xub = [ inf, inf] # Bornes superieures fixees a +infini
# Appel au solveur d'IMSL (API Python)
x = constrainedNlp(fcn, m, meq, ibtype, xlb, xub)
# Affichage de la solution
fmt = "%15.13f"
clabels = ["", "x1", "x2"]
writeMatrix ("La solution optimale est : ",x ,
writeFormat=fmt,
colLabels=clabels)
Après avoir configuré et construit le projet Python sous Eclipse par exemple, on l’exécute et la console rend quasi instantanément le résultat suivant :
La solution optimale est :
x1 x2
0.8228756555323 0.9114378277661
A présent, transposons ce code Python en langage C, pour notre application de production. Comme il l’a été annoncé au début de cet article, le point chaud est la partie calculatoire : comment la passer de Python vers C ? Notre code en C va-t-il rendre des résultats suffisamment proches ?
Le code C à produire est le suivant :
#include <stdio.h>
#include "imsl.h"
#define M 2
#define MEQ 1
#define N 2
void fcn(int n, double x[], int iact, double *result, int *ierr);
int main()
{
int ibtype = 0;
double *x;
static double xlb[N], xub[N];
xlb[0] = xlb[1] = imsl_d_machine(8); // -inf
xub[0] = xub[1] = imsl_d_machine(7); // +inf
// Appel au solveur d'IMSL C
x = imsl_d_constrained_nlp(fcn, M, MEQ, N, ibtype, xlb, xub, 0);
// Affichage de la solution
printf("La solution optimale est :\n");
printf("x1 = %15.13f x2 = %15.13f\n", x[0], x[1]);
}
void fcn(int n, double x[], int iact, double *result, int *ierr)
{
double tmp1, tmp2;
tmp1 = x[0] - 2.0e0;
tmp2 = x[1] - 1.0e0;
switch (iact) {
case 0:
*result = tmp1 * tmp1 + tmp2 * tmp2;
break;
case 1:
*result = x[0] - 2.0e0 * x[1] + 1.0e0;
break;
case 2:
*result = -(x[0]*x[0]) / 4.0e0 - x[1]*x[1] + 1.0e0;
break;
default: ;
break;
}
*ierr = 0;
return;
}
Il est très similaire dans son ensemble à son homologue Python, nous avons pris soin d’utiliser les mêmes noms de variables pour souligner cette similitude. Mais penchons-nous davantage sur la partie « solveur ».
En Python : x = constrainedNlp(fcn, m, meq, ibtype, xlb, xub)
En C : x = imsl_d_constrained_nlp(fcn, M, MEQ, N, ibtype, xlb, xub, 0);
On remarque que l’API est quasiment identique entre les 2 langages (noms de fonctions extrêmement proches, mêmes arguments, même ordre. Le langage C impose néanmoins de préciser le nombre de variables N et la fin de la liste des arguments par un 0. Le préfixe imsl_d_ indique par ailleurs qu’on appelle une fonction de la librairie IMSL C en double précision). De fait, il n’a pas été nécessaire :
- De recoder cet algorithme complexe ;
- De faire « coller » deux APIs totalement différentes, issues de 2 langages totalement différents.
Mais vérifions toutefois que notre code C fonctionne bien et qu’il rend des résultats suffisamment proches de ceux rendus par notre prototype. Après une compilation, une édition de liens et une exécution classiques, le verdict est sans appel :
La solution optimale est :
x1 = 0.8228756555323 x2 = 0.9114378277661
Appuyez sur une touche pour continuer...
Les résultats ne sont pas « proches » mais tout bonnement identiques !
Ce tutoriel nous a permis de montrer qu’avec l’environnement de prototypage et de mise en production PyIMSL Studio :
De plus, l’algorithme que nous avons utilisé est parallèle (repose sur la technologie OpenMP), ce qui lui permet de profiter des performances des machines multi-cœur, mais c’est une autre histoire…