Utilizzare la libreria Swiss Ephemeris con Ruby
4 01 2008Da anni mi interesso di astri e ho sempre cercato, nei miei siti personali, di mettere qualche minimo strumento di pubblica utilità.
Quelli che andavano forte di solito erano il calcolatore alba/tramonto del sole, della luna e delle fasi lunari.
Passando dai siti in PHP alle applicazioni Java e ora a Ruby ho avuto modo di vedere e provare parecchie librerie, quella che mi è sempre sembrata la migliore per i miei scopi era Swiss Ephemeris (http://www.astro.com/swisseph/).
Il problema più grosso è che la libreria in questione è scritta in C, che conosco molto poco, quindi mi sono sempre dovuto affidare, per usarla, al buon cuore di qualcuno che realizzasse dei porting o dei wrapper per i linguaggi che mi servivano.
Per Ruby ho trovato una Gem che fa da wrapper (http://rubyforge.org/projects/sweph4ruby/), funziona bene, ma purtroppo, allo stato attuale, incorpora solo un set minimo delle funzioni disponibili nella libreria.
Per questo ho deciso di prendere l’occasione per vedere come potrebbe essere possibile collegarsi da Ruby ad una libreria esterna e con l’occasione scrivere qualcosa che mi permettesse di accedere ai metodi di Swiss Ephemeris che mi interessano.
Il lavoro è in parte una riedizione dei sorgenti di sweph4ruby, riadattati per generare una libreria e non una Gem.
Fondamentalmente i passi che ho seguito sono 3:
1) ho predisposto i sorgenti e le librerie precompilate in C che volevo utilizzare
2) ho preparato i file Ruby per la compilazione e le classi in C e in Ruby che avrebbero fatto da wrapper
3) ho compilato il tutto e installato la library ottenuta
Predisporre i sorgenti
In questo caso Ruby c’entra poco, ho scaricato i sorgenti per Swiss Ephemeris dal sito di riferimento e li ho compilati.
Così facendo ho ottenuto un file chiamato “libswe.a” che è la libreria di funzioni vera e propria e che verrà inclusa nel modulo che utilizzato da Ruby.
Nel caso la compilazione avvenisse su una macchina windows il file che dovrebbe risultare è “swelib32.lib”.
Preparare i file per la compilazione per Ruby
A questo ho posizionato nella stessa directory tutti i files necessari per la compilazione, ovvero la libreria libswe.a e i due headers sweodef.h e swephexp.h, i files di wrap verso la libreria che ho chiamato swissephemeris.c e swissephemeris.rb e infine il file in ruby che genererà il Makefile per la compilazione, ovvero il file extconf.rb.
Il file swissephemeris.c è un file in C che di fatto “mappa” i metodi della libreria permettendo che vengano richiamati da una classe Ruby. Quello che fa è molto semplice, in pratica si occupa di rendere omogeneo il formato delle variabili passate da e verso la classe e di occuparsi di un minimo di gestione degli errori.
Nel caso specifico il mio file, che mappa alcuni metodi disponibili, si presenta così:
#include "ruby.h"
#include "swephexp.h"
static VALUE t_init(VALUE self)
{
return self;
}
static VALUE t_swe_set_ephe_path(VALUE self, VALUE path)
{
swe_set_ephe_path(StringValuePtr(path));
return self;
}
static VALUE t_swe_calc(VALUE self, VALUE julian_day, VALUE body)
{
double results[6];
char serr[AS_MAXCH];
VALUE arr = rb_ary_new();
int id_push = rb_intern("push");
int i =0;
if ( swe_calc(NUM2DBL(julian_day), NUM2INT(body), SEFLG_SPEED, results, serr) < 0 )
rb_raise (rb_eRuntimeError, serr);
for ( i = 0; i < 6; i++)
rb_funcall(arr, id_push, 1, rb_float_new(results[i]));
return arr;
}
static VALUE t_swe_rise_trans(VALUE self,VALUE tjd_ut,VALUE ipl,VALUE lon,VALUE lat,VALUE rsmi)
{
double xpin [3];
char serr[AS_MAXCH];
double results[1];
xpin[0] = NUM2DBL(lon);
xpin[1] = NUM2DBL(lat);
if ( swe_rise_trans(NUM2DBL(tjd_ut),NUM2INT(ipl),'\0',0,NUM2INT(rsmi),xpin,1013.25,10,results,serr) <0 )
rb_raise (rb_eRuntimeError, serr);
return rb_float_new(results[0]);
}
static VALUE t_swe_pheno(VALUE self, VALUE julian_day, VALUE body)
{
double results[20];
char serr[AS_MAXCH];
VALUE arr = rb_ary_new();
int id_push = rb_intern("push");
int i =0;
if ( swe_pheno(NUM2DBL(julian_day), NUM2INT(body), 0, results, serr) < 0 )
rb_raise (rb_eRuntimeError, serr);
for ( i = 0; i < 20; i++)
rb_funcall(arr, id_push, 1, rb_float_new(results[i]));
return arr;
}
VALUE cSweph;
void Init_swissephemeris()
{
cSweph = rb_define_class("Sweph", rb_cObject);
rb_define_method(cSweph, "initialize", t_init, 0);
rb_define_method(cSweph, "swe_set_ephe_path", t_swe_set_ephe_path, 1);
rb_define_method(cSweph, "swe_calc", t_swe_calc, 2);
rb_define_method(cSweph, "swe_rise_trans", t_swe_rise_trans, 5);
rb_define_method(cSweph, "swe_pheno", t_swe_pheno, 2);
}
Il file swissephemeris.rb è ancora più semplice, istanzia l’oggetto C e mappa 1:1 i metodi:
require 'swissephemeris.so'
class Rsweph
SE_ECL_NUT = -1
SE_SUN = 0
SE_MOON = 1
SE_MERCURY = 2
SE_VENUS = 3
SE_MARS = 4
SE_JUPITER = 5
SE_SATURN = 6
SE_URANUS = 7
SE_NEPTUNE = 8
SE_PLUTO = 9
SE_MEAN_NODE = 10
SE_TRUE_NODE = 11
SE_MEAN_APOG = 12
SE_OSCU_APOG = 13
SE_EARTH = 14
SE_CHIRON = 15
SE_PHOLUS = 16
SE_CERES = 17
SE_PALLAS = 18
SE_JUNO = 19
SE_VESTA = 20
SE_NPLANETS = 21
attr_reader :eph_path
def initialize
@eph_path = $LOAD_PATH[1]
@s = Sweph.new()
@s.swe_set_ephe_path(@eph_path)
end
def swe_set_ephe_path(path)
self.initialize() if @s == nil
@eph_path = path
@s.swe_set_ephe_path(@eph_path)
end
def swe_calc(julian_day, body_number)
self.initialize() if @s == nil
raise "Invalid body number #{body_number}" if body_number >= SE_NPLANETS || body_number < SE_ECL_NUT
@s.swe_calc(julian_day, body_number)
end
def swe_rise_trans(julian_day,body_number,lon,lat,rsmi)
self.initialize() if @s == nil
@s.swe_rise_trans(julian_day,body_number,lon,lat,rsmi)
end
def swe_pheno(julian_day,body_number)
self.initialize() if @s == nil
@s.swe_pheno(julian_day,body_number)
end
def decimal_degrees_to_dms(decimal_degrees)
deg = decimal_degrees.to_i
min = ((decimal_degrees - deg)*60).to_i
sec = ((decimal_degrees - deg - min / 60.0) * 60 * 60).to_i
return [deg, min, sec]
end
end
Il file extconf.rb sono poche righe di codice scritte in Ruby che contengono il riferimento ad un paio di librerie e le direttive su come si dovrà chiamare il modulo compilato.
Nel mio caso il file si presenta così:
require 'mkmf'
require 'rbconfig'
include Config
if (CONFIG["host_os"] == "mswin32" )
$LOCAL_LIBS += "swelib32.lib"
else
$LOCAL_LIBS += "libswe.a"
end
dir_config("swissephemeris")
create_makefile("swissephemeris")
Eseguendolo da riga di comando viene generato il file Makefile con tutte le direttive per la compilazione, lanciando un make parte la compilazione che, in base alla piattaforma utilizzata genererà un file .bundle sotto osx o un file .so sotto redhat/ubuntu/etc.
A questo punto basta posizionare il file ottenuto nella directory “/lib” dell’applicazione Rails in cui decidete di utilizzarla, o comunque in una posizione raggiungibile dall’interprete Ruby e utilizzarla attraverso la classe di wrap in Ruby.
Riassumendo, tutta la magia sta nel file extconf.rb che, in pratica, una volta lanciato genera un Makefile su misura per la generazione di una libreria da utilizzare con Ruby. L’unica accortezza e di includere la libreria nelle classi Ruby che dovranno utilizzarla.







