Alignements Smith-Waterman distribués avec SOAP::Lite et BioPerl

1. Problèmatiques

IBM et Genomining ont récemment lancé un projet nommé Décrypthon, a l'occasion du Téléthon annuel. Il s'agit, pour l'utilisateur lambda, de télécharger un économisateur d'écran un peu particulier. Celui ci est doté d'un moteur de comparaison de séquences protéiques basé sur la méthode SW, et de modules permettant de récupérer des données (séquences protéiques) sur un serveur distant, et de lui communiquer les résultats des comparaisons. Le but ultime est modestement de comparer toutes les protéines entre elles. Il s'agit d'un mode de fonctionnement que l'on nomme P2P (Peer To Peer) : la puissance de calcul du système ne se trouve pas sur une seule machine mais sur un ensemble de machines plus ou moins puissantes, réparties sur le réseau local ou Internet.

Le programme de calcul n'étant pas disponible sous Linux, j'ai donc décidé d'en ré-écrire une version similaire, mais ne fonctionnant que sous Unix. Il ne s'agit pas d'un gros travail, puisque le client fait 150 lignes de code Perl, et le serveur 100. En revanche, on tire profit des nombreux modules Perl disponibles et évitant au bioinformaticien de réinventer la roue (ex : SOAP et l'alignement protéique). Notons toutefois que cette implémentation n'est pas compatible avec les softs du Décrypthon.


Exemple d'alignement Smith-Waterman :


A partir de il s'agit simplement d'une implementation minimale, qui néanmoins pourrait etre pleinement utile sur un parc de machines Unix.

1.1 Problèmes et solutions

Les problémes à résoudre sont de plusieurs ordres :

- liées à l'utilisation d'un économisateur d'écran
- liées à au découpage du jeu de données initial en morceaux
- liées au transfert de données entre client et serveur
- liées à la procedure d'alignement
- lié au stockage persistant des séquences et résultats chez le client. il est bien évident qu'une base MySQL n'est pas utilisable. D'abord parce que cela alourdit la procedure (il faut installer MySQL sur chaque client, créer des bases, des tables, gerer les droits d'accès, installer des modules d'accès pour Perl, etc).

1.1.1 liées à l'utilisation d'un économisateur d'écran

Il existe sous Linux un programme général d'économisateur d'écran, nommé xscreensaver. Celui-ci fonctionne en tant que "wrapper" du programme économisateur. Cela signifie que une fois xscreensaver lancé, celui attends une période d'inactivité de la part de l'utilisateur. Lorsque celle-ci survient, il lance l'un des programmes externes spécifiés dans le fichier .xscreensaver, lequel peut - mais ce n'est pas nécéssaire - afficher des animations à l'écran. 

Lorsque un événement utilisateur survient, xscreensaver envoie le signal SIGTERM afin de le "tuer". Cette méthode de "terminaison" étant quelque peu brutale, il peut être nécéssaire d'intercepter ce signal au sein du programme SWC .

# XScreenSaver Preferences File
# Written by xscreensaver-demo 3.28 for olly on Thu Apr 4 13:35:55 2002.
# http://www.jwz.org/xscreensaver/

timeout: 0:10:00
cycle: 0:10:00
lock: False
lockTimeout: 0:00:00
passwdTimeout: 0:00:30
visualID: default
installColormap:True
verbose: True
timestamp: False
splash: True
splashDuration: 0:00:05
demoCommand: xscreensaver-demo
prefsCommand: xscreensaver-demo -prefs
helpURL: http://www.jwz.org/xscreensaver/man.html
loadURL: netscape -remote 'openURL(%s)' || netscape '%s'
nice: 10
fade: True
unfade: False
fadeSeconds: 0:00:03
fadeTicks: 20
captureStderr: True
font: *-medium-r-*-140-*-m-*
dpmsEnabled: False
dpmsStandby: 0:00:10
dpmsSuspend: 0:00:10
dpmsOff: 0:00:10
programs: /home/olly/swc/swc.pl
pointerPollTime:0:00:05
windowCreationTimeout:0:00:30
initialDelay: 0:00:00
sgiSaverExtension:True
mitSaverExtension:False
xidleExtension: True
procInterrupts: True
overlayStderr: True


1.1.1 liées à au découpage du jeu de données initial en morceaux

Dans ce type d'architecture, il est nécéssaire d'y avoir au niveau du serveur un ordonnancateur, c'est a dire un programme de répartition des  taches à effectuer par les clients.

Nous mettrons en place un ordonnancateur simple, basé sur la notion de "job".

Un job est constitué d'une séquence cible, et d'un jeu de séquences à comparer avec cette séquence cible. La taille de ce jeu de séquences est égale à 40 au maximum. A chaque fois que le client se connecte en demandant un nouveau jeu de données, un nouveau job est créé afin de stocker. Les paramêtres du nouveau job sont obtenus à partir du dernier job créé auparavant.  Une table dans MySQL va servir à stocker les jobs.


1.1.3 liées au transfert de données entre client et serveur

Le transfert de données entre client et serveur se fait grace au protocole SOAP. Il s'agit d'un protocole de RPC (Remote Procedure Call), basé sur XML pour le codage de l'information transmise. Sans rentrer dans les détails, nous dirons qu'il s'agit d'un standard permettant d'appeler une procédure distante (sur autre site internet, ou sur une autre machine) de façon transparente, c'est-a-dire sans se soucier du transfert physique de données via les réseaux.
Pour implémenter ces transfert, nous utilisons le module Perl SOAP::Lite :

Les problèmatiques restant à résoudre :
- controle et ré-execution des jobs jamais arrivés
- optimisation des transferts et gestion du hors-connexion. En effet, dans sa version actuelle, SWC est uniquement utilisable sur des machines connectées au Net en permanence.

2. Pré-requis

Nous utiliserons les modules Perl suivant : SOAP::Lite to implement BioPerl to DBI modules to access to MySQL database in Perl Nous utiliserons les logiciels (open source) suivant : xscreensaver to launch the SWC client when the user is idle MySQL database server, to store the results, the running jobs, and the results. Bioperl XS extentions : le package bioperl-ext contient des modules avec du code en langage C, interfacé au Perl via le mécanisme XS. En particulier, le package bioperl-ext-06 contient le module Bio::Tools::pSW permettant de réaliser des alignements 2 à 2 par la méthode de Smith-Waterman. Ce package est disponible via CPAN, ou via le serveur FTP dont l'URL est ftp://bioperl.org/pub/external/

Installer un module Perl peut se faire de manière quasi-automatisé grace a un module Perl (souvent pré-installé) nommé CPAN.pm. Par exemple, pour installer le module SOAP::Lite, il suffit de taper :


3. Créer la partie serveur

3.1 Base de données

En tout premier lieu, créer une base de données.
mysql> create database SWC
-> ;
Query OK, 1 row affected (0.56 sec)
Créer ensuite la table amenée à stocker des séquences
drop table if exists SEQUENCES;
CREATE TABLE SEQUENCES (
ID int not null auto_increment primary key,
ACCNUM varchar(10),
SEQUENCE text
);
Puis la table de stockage de JOBS (petit jeux de données envoyés au client)
drop table if exists JOBS;
CREATE TABLE JOBS (
ID int not null auto_increment primary key,
SEQID int,
SEQST int,
SEQEN int
);
Enfin, creer la table permettant de stocker les resultats.
drop table if exists RESULTS;
CREATE TABLE RESULTS (
ACCNUM1 varchar(10),
ACCNUM2 varchar(10),
ALNSEQ1 text,
ALNSEQ2 text,
SCORE float
);


Voici le code du serveur SOAP. Celui fonctionne en tant que "démon" Unix, c'est a dire que l'on s'y connecte via un port (ici 7777) sur lequel celui-ci écoute en permanence. Notons que le serveur SOAP peut (et doit) gérer des accès simultanés.
#!/usr/bin/perl -w

use SOAP::Transport::HTTP;
use DBI();


SOAP::Transport::HTTP::Daemon
-> new (LocalAddr => 'localhost', LocalPort => 7777)
-> dispatch_to('Server')
-> handle;

package Server;

sub connect {

$dbh = DBI->connect( "DBI:mysql:database=SWC;host=localhost",
"",
"",
{'RaiseError' => 1}
);
return $dbh;
}

sub getSequences {
my ($class) = @_;

my $CHUNK = 40;

$dbh = $class->connect();

# get the total number of sequences in the DB
$query = "SELECT count(*) AS COUNT FROM SEQUENCES";
$sth = $dbh->prepare($query);
$sth->execute;
$count = $sth->fetchrow_hashref->{COUNT};

print "got $count sequences in DB ...\n";


# get the last job
$query = "SELECT * FROM JOBS ORDER BY ID DESC LIMIT 1";
$sth = $dbh->prepare($query);
$sth->execute or die ("pb ..\n");
$row = $sth->fetchrow_hashref;
print "Issuing query $query\n";

# if the END of the last job is equal to the number of seqs
if (!$row) {
$seqid = 1;
$seqst = 1;
$seqen = 1 + $CHUNK;
} elsif ($row->{SEQEN} >= $count) {
$seqid = $row->{SEQID} + 1;
$seqst = 1;
$seqen = 1 + $CHUNK;
} else {
$seqid = $row->{SEQID};
$seqst = $row->{SEQEN} + 1;
$seqen = ($row->{SEQEN} + $CHUNK > $count ? $count : $row->{SEQEN} + $CHUNK);
}


# create a new job
$query = "INSERT INTO JOBS VALUES ('null', '$seqid', '$seqst', '$seqen')";

print "Issuing query $query\n";
$dbh->do($query) or die("pb ...\n");

# get the target sequence
$query = "SELECT * FROM SEQUENCES WHERE ID = $seqid";
print "Issuing query $query\n";

$sth = $dbh->prepare($query);
$sth->execute;
@seqs = ();
$row = $sth->fetchrow_hashref;
push(@seqs, $row);


# get the sequences
$query = "SELECT * FROM SEQUENCES WHERE ID >= $seqst AND ID != $seqid LIMIT $CHUNK";
print "Issuing query $query\n";

$sth = $dbh->prepare($query);
$sth->execute;

while ($row = $sth->fetchrow_hashref) {
push(@seqs, $row);
}

return \@seqs;
}

sub addResults {
my ($class, $c) = @_;

$dbh = $class->connect();

print "results :\n";
foreach $aln (@$c) {
print $aln->{ACCNUM1} . "-" . $aln->{ACCNUM2} . "\n";
$query = "INSERT INTO RESULTS VALUES ('" . $aln->{ACCNUM1} . "', '" .
$aln->{ACCNUM2} . "', '" .
$aln->{ALNSEQ1} . "', '" .
$aln->{ALNSEQ2} . "', '" .
$aln->{SCORE} . "')";
#print "$query\n";
$dbh->do($query) or die "oops ...\n";
}
}
4. CREER LA PARTIE CLIENT
#!/usr/bin/perl



use Bio::Tools::pSW;
use Bio::Seq;
use Bio::SeqIO;
use Bio::AlignIO;
use GDBM_File;
use SOAP::Lite;


tie(my(%indexdb), 'GDBM_File',"sequences.db", &GDBM_WRCREAT, 0644);

tie(my(%varsdb), 'GDBM_File',"vars.db", &GDBM_WRCREAT, 0644);

tie(my(%resultsdb), 'GDBM_File',"results.db", &GDBM_WRCREAT, 0644);


sub handler{
local ($signal) = @_;

print "Got the $signal signal, aborting!\n";

$varsdb{"theid"} = $theid;
$varsdb{"j"} = $j;

exit();
}

$SIG{'TERM'} = 'handler';


RETURN:

# list the stored results
while (($k, $v) = each(%resultsdb)) {
print ">$k\n";
print "$v\n";
}


# in case the whole chunk has been computed

print scalar(keys(%resultsdb));
print scalar(keys(%indexdb));


if (scalar(keys(%resultsdb)) == scalar(keys(%indexdb)) - 1) {
# send back the results
@res = ();
while (($k, $v) = each(%resultsdb)) {
($a1, $a2) = $k =~ /^(.+)\|(.+)$/;
($score, $s1, $s2) = $v =~ /^(.+)\|(.+)\|(.+)$/;
my %hash = ("ACCNUM1" => $a1,
"ACCNUM2" => $a2,
"ALNSEQ1" => $s1,
"ALNSEQ2" => $s2,
"SCORE" => $score );
push(@res, \%hash);

}

$soap_response = SOAP::Lite
-> uri('http://localhost/Server')
-> proxy('http://localhost:7777')
-> addResults(\@res);

%indexdb = ();
%resultsdb = ();
%varsdb = ();

}


if (scalar(keys(%indexdb)) == 0) {

# get new sequences

$soap_response = SOAP::Lite
-> uri('http://localhost/Server')
-> proxy('http://localhost:7777')
-> getSequences();

print "Getting data from the soap server !\n";
$res = $soap_response->result;
$cnt = 0;
foreach $seq (@$res) {
$theid = $seq->{ACCNUM} if ($cnt++ == 0);

print $seq->{ACCNUM} . "\n";
$indexdb{$seq->{ACCNUM}} = $seq->{SEQUENCE};
}
$thej = 0;
} else {

# the sequence set is not empty, this could have been a SIGTERM
$thej = ($varsdb{"j"}?$varsdb{"j"}:0);
$theid = $varsdb{"theid"};
}




@keys1 = keys %indexdb;


$factory = new Bio::Tools::pSW( '-matrix' => 'blosum62.bla',
'-gap' => 12,
'-ext' => 2, );


LABEL: for ($j=$thej; $j<=$#keys1; $j++) {

next LABEL if ($theid eq $keys1[$j]);
print "Aligning " . $theid . " and " . $keys1[$j] . " ($j)\n";
$seq1 = Bio::Seq->new( -seq => $indexdb{$theid}, -id => $theid);
$seq2 = Bio::Seq->new( -seq => $indexdb{$keys1[$j]}, -id => $keys1[$j]);

$aln = $factory->pairwise_alignment($seq1, $seq2);

@seqs = $aln->each_seq;

$record = "";
$cle = "";

$record .= $aln->percentage_identity;
$record .= "|";
$record .= $seqs[0]->seq;
$record .= "|";
$record .= $seqs[1]->seq;

$cle .= $seqs[0]->id;
$cle .= "|";
$cle .= $seqs[1]->id;

$resultsdb{$cle} = $record;

}


goto RETURN;

untie %indexdb;
untie %varsdb;
untie %resultsdb;




The On the server side, both the sequences and the results are stored in a MySQL database. This is necessary since concurrent access to the database are likely to arise. create a new database create a new table to store the sequences : mysql> create database SWC -> ; Query OK, 1 row affected (0.56 sec) mysql> use SWC Database changed mysql> CREATE TABLE SEQUENCES ( -> ID int not null auto_increment primary key, -> ACCNUM varchar(10), -> SEQUENCE text -> ); Query OK, 0 rows affected (0.18 sec) mysql> CREATE TABLE JOBS ( ) insert sequences The first web service provides with an associative array of sequences to work with. only one sequence to share with every other ...