Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

Fortran & unpack

20 réponses
Avatar
Julien K.
Bonjour,

j'écris un script de lecture de données binaires produites par un code de
calcul en F77; une valeur de charge particulière est affectée aux mailles
inactives, typiqument 999.99. Les valeurs sont des REALs écrits en binaire
non formatté.

Malheureusement dans la lecture du fichier je ne récupère pas exactement
cette valeur, je suis confronté à un problème de ce genre:

$ perl -e 'print unpack("f",(pack("f",999.99))) ;'
999.989990234375

$ perl -e 'print sprintf("%.4f",unpack("f",(pack("f",999.99)))) ;'
999.9900

Comment puis-je faire pour récupérer 999.99, valeur utilisée dans des
tests? Utiliser sprintf résoud le problème mais ça ne me semble pas /propre/.

Merci de vos lumières, docs...

Julien

10 réponses

1 2
Avatar
Paul Gaborit
À (at) Tue, 29 Jan 2008 15:18:15 +0100,
"Julien K." écrivait (wrote):
j'écris un script de lecture de données binaires produites par un code de
calcul en F77; une valeur de charge particulière est affectée aux mailles
inactives, typiqument 999.99. Les valeurs sont des REALs écrits en binaire
non formatté.

Malheureusement dans la lecture du fichier je ne récupère pas exactement
cette valeur, je suis confronté à un problème de ce genre:

$ perl -e 'print unpack("f",(pack("f",999.99))) ;'
999.989990234375

$ perl -e 'print sprintf("%.4f",unpack("f",(pack("f",999.99)))) ;'
999.9900

Comment puis-je faire pour récupérer 999.99, valeur utilisée dans des
tests? Utiliser sprintf résoud le problème mais ça ne me semble pas /propre/.


À mon avis, vous feriez mieux de travailler en DOUBLE plutôt qu'en
FLOAT. La quasi totalité des processeurs modernes ne savent plus
utiliser les floats (ils font leurs calculs en double et filtrent au
dernier moment pour simuler les floats). Il n'y a que lorsqu'on est
très très contraint en place mémoire qu'on utilise les floats mais
c'est quand même très rare.

Bon, ceci étant dit, vous n'avez peut-être pas le choix...

Le problème constaté est tout fait normal et n'a rien à voir avec Perl
ou perl. C'est juste que la valeur stockée par un float ne peut pas
être tout pile 999.99 donc si on l'affiche avec trop de précision on
voit les derniers bits erronés.

La solution du sprintf est pas mal pour arrondir mais le .4f est
arbitraire (pourquoi 4 décimales après la virguel, et si la valeur
vaut 1e-5 ou 1e20...). Je propose donc d'utiliser une même limite arbitraire
mais ne dépendant pas de l'ordre de la valeur :

perl -e 'print sprintf("%g", unpack("f",(pack("f",999.99)))), "n" ;'

--
Paul Gaborit - <http://perso.enstimac.fr/~gaborit/>
Perl en français - <http://perl.enstimac.fr/>

Avatar
Julien K.
On 29-01-2008, Paul Gaborit wrote:

À (at) Tue, 29 Jan 2008 15:18:15 +0100,
"Julien K." écrivait (wrote):
[...]
$ perl -e 'print unpack("f",(pack("f",999.99))) ;'
999.989990234375

$ perl -e 'print sprintf("%.4f",unpack("f",(pack("f",999.99)))) ;'
999.9900

Comment puis-je faire pour récupérer 999.99, valeur utilisée dans des
tests? Utiliser sprintf résoud le problème mais ça ne me semble pas /propre/.


À mon avis, vous feriez mieux de travailler en DOUBLE plutôt qu'en
FLOAT. La quasi totalité des processeurs modernes ne savent plus
utiliser les floats (ils font leurs calculs en double et filtrent au
dernier moment pour simuler les floats). Il n'y a que lorsqu'on est
très très contraint en place mémoire qu'on utilise les floats mais
c'est quand même très rare.


Dans le code Fortran? Oh je vais pas chercher à modifier cette masse
informe (avec des variables de 6 caractères max!) même si j'ai les sources
je risquerais de faire une connerie.

Je retiens toutefois la proposition pour un futur module de calcul.

Bon, ceci étant dit, vous n'avez peut-être pas le choix...


L'autre alternative est de lire une sortie ASCII du code; je fixe
arbitrairement le format de sortie donc ça revient à peu-près au même que de
fixer l'arrondi du binaire (sauf que c'est plus lent à lire).

Le problème constaté est tout fait normal et n'a rien à voir avec Perl
C'est juste que la valeur stockée par un float ne peut pas être tout pile
999.99 donc si on l'affiche avec trop de précision on voit les derniers
bits erronés.


Oui, ça je sais, je cherchais un moyen de limiter l'affectation des
variables à la partie codée (8 octets) du float (et pas me trimbaler le
bourrage de la mantisse).

La solution du sprintf est pas mal pour arrondir mais le .4f est
arbitraire (pourquoi 4 décimales après la virgule,


C'est la première qui marche ;-)

et si la valeur vaut 1e-5 ou 1e20...). Je propose donc d'utiliser une
même limite arbitraire mais ne dépendant pas de l'ordre de la valeur :

perl -e 'print sprintf("%g", unpack("f",(pack("f",999.99)))), "n" ;'


Oui c'est effectivement mieux mais ça implique toujours de passer par une
chaîne de caractères pour arriver à un float... Enfin vu le temps
d'exécution c'est négligeable.

Merci pour ta réponse.

Julien


Avatar
Paul Gaborit
À (at) Tue, 29 Jan 2008 19:45:20 +0100,
"Julien K." écrivait (wrote):
On 29-01-2008, Paul Gaborit wrote:

À (at) Tue, 29 Jan 2008 15:18:15 +0100,
"Julien K." écrivait (wrote):
[...]
$ perl -e 'print unpack("f",(pack("f",999.99))) ;'
999.989990234375

$ perl -e 'print sprintf("%.4f",unpack("f",(pack("f",999.99)))) ;'
999.9900

Comment puis-je faire pour récupérer 999.99, valeur utilisée dans des
tests? Utiliser sprintf résoud le problème mais ça ne me semble pas /propre/.
[...]



Bon, ceci étant dit, vous n'avez peut-être pas le choix...


L'autre alternative est de lire une sortie ASCII du code; je fixe
arbitrairement le format de sortie donc ça revient à peu-près au même que de
fixer l'arrondi du binaire (sauf que c'est plus lent à lire).


J'en conclus donc que tu lis un fichier binaire (plutôt que la sortie
ASCII). Bon... C'est très facile à faire à partir du moment où on sait
comment est constitué le fichier Fortran.

Le problème constaté est tout fait normal et n'a rien à voir avec Perl
C'est juste que la valeur stockée par un float ne peut pas être tout pile
999.99 donc si on l'affiche avec trop de précision on voit les derniers
bits erronés.


Oui, ça je sais, je cherchais un moyen de limiter l'affectation des
variables à la partie codée (8 octets) du float (et pas me trimbaler le
bourrage de la mantisse).


Heu... je ne comprends pas ce que tu veux dire. Les floats sont
généralement stockés sur 4 octets et les doubles sur 8 octets. Et dans
ces octets, on trouve la mantisse et l'exposant. De quel 'bourrage'
parles-tu ?

La solution du sprintf est pas mal pour arrondir mais le .4f est
arbitraire (pourquoi 4 décimales après la virgule,


C'est la première qui marche ;-)

et si la valeur vaut 1e-5 ou 1e20...). Je propose donc d'utiliser une
même limite arbitraire mais ne dépendant pas de l'ordre de la valeur :

perl -e 'print sprintf("%g", unpack("f",(pack("f",999.99)))), "n" ;'


Oui c'est effectivement mieux mais ça implique toujours de passer par une
chaîne de caractères pour arriver à un float... Enfin vu le temps
d'exécution c'est négligeable.


Là encore je ne comprends pas... Si les données proviennent d'un
fichier binaire Fortran (dont tu connais le format), il suffit de lire
les octets regroupés correctement (par 4 pour des floats, par 8 pour
doubles...) puis de leur appliquer 'unpack' pour en faire des nombres
Perl :

my $float = unpack("F", $quatre_octets_d_un_float);
my $double = unpack("D", $double_octets_d_un_float);

Nulle besoin de passer par la représentation textuelle des valeurs.

--
Paul Gaborit - <http://perso.enstimac.fr/~gaborit/>
Perl en français - <http://perl.enstimac.fr/>



Avatar
Paul Gaborit
À (at) Wed, 30 Jan 2008 00:22:20 +0100,
J'écrivais :
Là encore je ne comprends pas... Si les données proviennent d'un
fichier binaire Fortran (dont tu connais le format), il suffit de lire
les octets regroupés correctement (par 4 pour des floats, par 8 pour
doubles...) puis de leur appliquer 'unpack' pour en faire des nombres
Perl :

my $float = unpack("F", $quatre_octets_d_un_float);
my $double = unpack("D", $double_octets_d_un_float);


Sur cette dernière ligne, je voulais écrire :

my $double = unpack("D", $huit_octets_d_un_double);

--
Paul Gaborit - <http://perso.enstimac.fr/~gaborit/>
Perl en français - <http://perl.enstimac.fr/>

Avatar
Julien K.
On 29-01-2008, Paul Gaborit wrote:

À (at) Tue, 29 Jan 2008 19:45:20 +0100,
"Julien K." écrivait (wrote):
On 29-01-2008, Paul Gaborit wrote:

À (at) Tue, 29 Jan 2008 15:18:15 +0100,
"Julien K." écrivait (wrote):
[...]
$ perl -e 'print unpack("f",(pack("f",999.99))) ;'
999.989990234375

$ perl -e 'print sprintf("%.4f",unpack("f",(pack("f",999.99)))) ;'
999.9900

Comment puis-je faire pour récupérer 999.99, valeur utilisée dans des
tests? Utiliser sprintf résoud le problème mais ça ne me semble pas /propre/.
[...]



Bon, ceci étant dit, vous n'avez peut-être pas le choix...


L'autre alternative est de lire une sortie ASCII du code; je fixe
arbitrairement le format de sortie donc ça revient à peu-près au même que de
fixer l'arrondi du binaire (sauf que c'est plus lent à lire).


J'en conclus donc que tu lis un fichier binaire (plutôt que la sortie
ASCII). Bon... C'est très facile à faire à partir du moment où on sait
comment est constitué le fichier Fortran.


Justement à partir du code source je le sais.

Le problème constaté est tout fait normal et n'a rien à voir avec Perl
C'est juste que la valeur stockée par un float ne peut pas être tout pile
999.99 donc si on l'affiche avec trop de précision on voit les derniers
bits erronés.


Oui, ça je sais, je cherchais un moyen de limiter l'affectation des
variables à la partie codée (8 octets) du float (et pas me trimbaler le
bourrage de la mantisse).


Heu... je ne comprends pas ce que tu veux dire. Les floats sont
généralement stockés sur 4 octets et les doubles sur 8 octets.


Oui 4 plutôt que 8.

Et dans ces octets, on trouve la mantisse et l'exposant. De quel 'bourrage'
parles-tu ?


Eh bien celui qui fait que perl ne lise pas exactement ce qui est

La solution du sprintf est pas mal pour arrondir mais le .4f est
arbitraire (pourquoi 4 décimales après la virgule,


C'est la première qui marche ;-)



D'ailleurs c'est la seule, '%g' ne donne pas de précision suffisante donc
autant utiliser "%.4f".

et si la valeur vaut 1e-5 ou 1e20...). Je propose donc d'utiliser une
même limite arbitraire mais ne dépendant pas de l'ordre de la valeur :

perl -e 'print sprintf("%g", unpack("f",(pack("f",999.99)))), "n" ;'


Oui c'est effectivement mieux mais ça implique toujours de passer par une
chaîne de caractères pour arriver à un float... Enfin vu le temps
d'exécution c'est négligeable.


Là encore je ne comprends pas...


Ben pourtant c'est clair: je passe d'un float (binaire) à un nombre (perl)
qui est converti en chaîne par sprintf() puis utilisé dans des opérations en
tant que nombre.

Si les données proviennent d'un fichier binaire Fortran (dont tu connais
le format), il suffit de lire les octets regroupés correctement (par 4
pour des floats, par 8 pour doubles...) puis de leur appliquer 'unpack'
pour en faire des nombres Perl :


C'est ce que je fais et c'est ce qui me donne les erreurs d'arrondi. J'ai
mis un exemple en ligne de commande pour faire simple mais ceci devrait
t'en dire plus (code adapté ie sans contrôle, use strict...):

<<<<
# header
$head_fmt = q!i2f2a16i3! ;
read($FH,$f,length(pack($head_fmt)),0) ;
# ($step $period $per_$time $tot_time $var $ncol $nrow $nlay)
@t = unpack($head_fmt,$f) ;
($nrow,$ncol) = @t[5,6] ;
# matrice ($nrow x $ncol)
$line_fmt = sprintf('f%i',$ncol) ;
for $i (0..$nrow-1)
{ read($FH,$f,length(pack($line_fmt)),0) ;
push( @{ $t }, [ unpack($line_fmt,$f) ] ;
}






Le 'sprintf()' qui motive ma question est appliqué à la dernière ligne:
<<<<
push(@{ $t },[ map (sprintf('%.4f',$_),unpack($line_fmt,$f)) ]) ;






Est-ce plus clair?

Nul besoin de passer par la représentation textuelle des valeurs.


Pour donner un exemple concis, si :-).

Julien




Avatar
Julien K.
On 30-01-2008, Julien K. wrote:
On 29-01-2008, Paul Gaborit wrote:


Tiens le téléphone m'a interrompu dans ma phrase...

Et dans ces octets, on trouve la mantisse et l'exposant. De quel 'bourrage'
parles-tu ?


Eh bien celui qui fait que perl ne lise pas /exactement/ ce qui est dans
le fichier (ou ce que je pense y trouver). Maintenant je m'y prends
peut-être mal pour la lecture de données binaires...

Julien

Avatar
Thierry B.
--{ Julien K. a plopé ceci: }--

Et dans ces octets, on trouve la mantisse et l'exposant. De quel 'bourrage'
parles-tu ?


Eh bien celui qui fait que perl ne lise pas /exactement/ ce qui est dans
le fichier (ou ce que je pense y trouver). Maintenant je m'y prends
peut-être mal pour la lecture de données binaires...


Les nombres flottants exprimé en binaire ne sont jamais exacts,
la plupart du temps. Je t'invite à regarder les messages récents
dans le ng comp.lang.fortran, où ce genre de question revient
régulièrement.


--
...
... Mmmm
...
By the way, all OSes are bloated, no ?


Avatar
Paul Gaborit
À (at) Wed, 30 Jan 2008 11:54:47 +0100,
"Julien K." écrivait (wrote):
C'est ce que je fais et c'est ce qui me donne les erreurs d'arrondi. J'ai
mis un exemple en ligne de commande pour faire simple mais ceci devrait
t'en dire plus (code adapté ie sans contrôle, use strict...):

<<<<
# header
$head_fmt = q!i2f2a16i3! ;
read($FH,$f,length(pack($head_fmt)),0) ;
# ($step $period $per_$time $tot_time $var $ncol $nrow $nlay)
@t = unpack($head_fmt,$f) ;
($nrow,$ncol) = @t[5,6] ;
# matrice ($nrow x $ncol)
$line_fmt = sprintf('f%i',$ncol) ;
for $i (0..$nrow-1)
{ read($FH,$f,length(pack($line_fmt)),0) ;
push( @{ $t }, [ unpack($line_fmt,$f) ] ;
}






Le 'sprintf()' qui motive ma question est appliqué à la dernière ligne:
<<<<
push(@{ $t },[ map (sprintf('%.4f',$_),unpack($line_fmt,$f)) ]) ;






Est-ce plus clair?

Nul besoin de passer par la représentation textuelle des valeurs.


Pour donner un exemple concis, si :-).


(Je mets de côté le $t utilisé dans @{ $t } et donc on ne sait rien
mais j'imagine que c'est là où tu veux stocker tes valeurs...)

Donc tu crois qu'il faut que tu remplaces la ligne :

push( @{ $t }, [ unpack($line_fmt,$f) ] ;

par :

push(@{ $t },[ map (sprintf('%.4f',$_),unpack($line_fmt,$f)) ]) ;

C'est absolument inutile ! Avec la première version de cette ligne,
les valeurs *flottantes* envoyées dans @{ $t } sont exactement les
mêmes que celles écrites par Fortran dans le fichier binaire... mais
comme Perl n'utilise que des doubles (en interne), tu récupères
nécessairement plus de précision qu'en Fortan. C'est d'ailleurs
signalé dans la doc de 'pack'. Petit extrait (en français) :

Sachez que Perl utilise des doubles en interne pour tous les
calculs numériques et qu'une conversion de double vers float puis
retour vers double amène à une perte de précision (i.e.,
unpack("f", pack("f", $foo)) est généralement différent de $foo).

Sauf que là, l'opération se fait dans l'autre sens. Mais ça ne change
rien à la situation : lorsqu'on enlève des bits, on perd en précision
et lorsqu'on en ajoute, ils sont évidemment faux.

--
Paul Gaborit - <http://perso.enstimac.fr/~gaborit/>
Perl en français - <http://perl.enstimac.fr/>





Avatar
Julien K.
On 30-01-2008, Paul Gaborit wrote:

À (at) Wed, 30 Jan 2008 11:54:47 +0100,
"Julien K." écrivait (wrote):
C'est ce que je fais et c'est ce qui me donne les erreurs d'arrondi. J'ai
mis un exemple en ligne de commande pour faire simple mais ceci devrait
t'en dire plus (code adapté ie sans contrôle, use strict...):

<<<<
# header
$head_fmt = q!i2f2a16i3! ;
read($FH,$f,length(pack($head_fmt)),0) ;
# ($step $period $per_$time $tot_time $var $ncol $nrow $nlay)
@t = unpack($head_fmt,$f) ;
($nrow,$ncol) = @t[5,6] ;
# matrice ($nrow x $ncol)
$line_fmt = sprintf('f%i',$ncol) ;
for $i (0..$nrow-1)
{ read($FH,$f,length(pack($line_fmt)),0) ;
push( @{ $t }, [ unpack($line_fmt,$f) ] ;
}






Le 'sprintf()' qui motive ma question est appliqué à la dernière ligne:
<<<<
push(@{ $t },[ map (sprintf('%.4f',$_),unpack($line_fmt,$f)) ]) ;






Est-ce plus clair?

Nul besoin de passer par la représentation textuelle des valeurs.


Pour donner un exemple concis, si :-).


(Je mets de côté le $t utilisé dans @{ $t } et donc on ne sait rien
mais j'imagine que c'est là où tu veux stocker tes valeurs...)


Exact.

Donc tu crois qu'il faut que tu remplaces la ligne :

push( @{ $t }, [ unpack($line_fmt,$f) ] ;

par :

push(@{ $t },[ map (sprintf('%.4f',$_),unpack($line_fmt,$f)) ]) ;



C'est absolument inutile !


Pas tant inutile que ça, toutes les matrices sont chargées de la sorte,
les tests de valeur fonctionnent... Bref ça marche (alors que sans, non).

Je parle même pas des horreurs épileptiques que sortent les fonctions de
contour avec des valeurs non /nettoyées/ de ces scories.

Autre remarque sous-jacente: une fois le sprintf() appliqué je me retrouve
avec des valeurs à 4 chiffres après la virgule dans les matrices; elles sont
probablement considérées comme des chaînes de caractère... jusqu'à la
première opération arithmétique où elles redeviennent (ou au moins le
résultat de l'opération) des nombres. Il n'y a pas là de perte de précision
(à mon sens), mon problème vient donc uniquement de pack()/unpack()
utilisé avec un float... C'est pour ça que je corrige à la lecture.

Avec la première version de cette ligne, les valeurs *flottantes* envoyées
dans @{ $t } sont exactement les mêmes que celles écrites par Fortran dans
le fichier binaire... mais comme Perl n'utilise que des doubles (en
interne), tu récupères nécessairement plus de précision qu'en Fortan.


Précision au sens "nombre de chiffre" je suppose (pas au sens de "se
rapproche de la valeur exacte"). Donc ce ne sont pas "exactement les mêmes
que celles [du] fichier binaire" puisque lorsque je veux les vérifier elles
sont différentes; j'ajoute même que lorsque (sans le sprintf) je teste la
présence de '999.99' il n'y a aucune valeur qui corresponde.

Dois-je à tn avis effectuer les tests en binaire avec ce que renvoie
pack('f',999.99)?

C'est d'ailleurs signalé dans la doc de 'pack'. Petit extrait (en
français) :

Sachez que Perl utilise des doubles en interne pour tous les
calculs numériques et qu'une conversion de double vers float puis
retour vers double amène à une perte de précision (i.e.,
unpack("f", pack("f", $foo)) est généralement différent de $foo).


Cette dernière phrase décrit exactement la situation que je rencontre,
je l'avais loupée.

Utiliser le module PDL arrangerait-il les choses? Je commence à croire que
la modification du code pour tout passer en DOUBLE dans le source Fortran
serait la meilleure solution à moyen terme.

Sauf que là, l'opération se fait dans l'autre sens. Mais ça ne change
rien à la situation : lorsqu'on enlève des bits, on perd en précision
et lorsqu'on en ajoute, ils sont évidemment faux.


Justement je souhaiterais ne toucher à rien, une comparaison entre
(float)s me convient tout à fait vu les grandeurs physiques en jeu.

Je vais finir par recoder tout ça en C++, je sens que ça va pas tarder.

Julien






Avatar
Julien K.
On 30-01-2008, Thierry B. wrote:
--{ Julien K. a plopé ceci: }--

Et dans ces octets, on trouve la mantisse et l'exposant. De quel 'bourrage'
parles-tu ?


Eh bien celui qui fait que perl ne lise pas /exactement/ ce qui est
dans le fichier (ou ce que je pense y trouver). Maintenant je m'y prends
peut-être mal pour la lecture de données binaires...


Les nombres flottants exprimé en binaire ne sont jamais exacts,
la plupart du temps. Je t'invite à regarder les messages récents
dans le ng comp.lang.fortran, où ce genre de question revient
régulièrement.


Merci de la suggestion, je regarde ça de-suite.

Julien



1 2