Un dépôt GitHub pour mes programmes Rust
Article mis en ligne le 9 novembre 2021
dernière modification le 10 novembre 2021

par Laurent Bloch

 Agencer les modules pour construire un exécutable

Ne reculant devant aucune vanité, je décide de placer mes programmes dans un dépôt GitHub. Auparavant, il faut que ces programmes soient organisés de façon à pouvoir construire un exécutable binaire, ce qui impose quelques corrections. Certaines subtilités de l’organisation des cageots (crates) et des modules m’échappent encore mais grâce à la coopération des internautes (merci Stack Overflow !) j’arrive à quelque chose qui fonctionne.

Pour construire l’exécutable je m’en remets à Cargo. Il faut lui indiquer que j’utilise un cageot externe, en l’occurrence simple-matrix de Nicolas Memeint, ce qui s’écrit ainsi dans le fichier Cargo.toml :

  1. [package]
  2. name = "needleman_wunsch"
  3. version = "0.1.0"
  4. authors = ["Laurent Bloch <lb@laurentbloch.org>"]
  5. edition = "2018"
  6.  
  7. # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
  8.  
  9. [dependencies]
  10. simple-matrix = "0.1"

Télécharger

Le contenu du répertoire src est modifié comme suit.

 Programme principal

  1. // src/main.rs :
  2.  
  3. pub mod fasta_multiple_cmp;
  4.  
  5. use fasta_multiple_cmp::get_filenames;
  6.  
  7. fn main() {
  8.     get_filenames();
  9. }

Télécharger

 Lire les séquences

  1. // src/fasta_multiple_cmp.rs :
  2.  
  3. // https://linuxfr.org/forums/programmationautre/posts/rust-lire-des-donnees-de-type-i8-depuis-un-fichier
  4. // https://www.it-swarm-fr.com/fr/file-io/quelle-est-la-maniere-de-facto-de-lire-et-decrire-des-fichiers-dans-rust-1.x/1054845808/
  5. // https://docs.rs/simple-matrix/0.1.2/simple_matrix/
  6.  
  7. pub mod build_sequences_matrix;
  8.  
  9. use std::env;
  10. use std::fs;
  11. use std::fs::File;
  12. use std::io;
  13. use std::io::Read;
  14. use std::io::{prelude::*, BufReader};
  15. use std::io::Lines;
  16. use std::fs::Metadata;
  17. use std::str;
  18.  
  19. use crate::fasta_multiple_cmp::build_sequences_matrix::print_seq;
  20. use crate::fasta_multiple_cmp::build_sequences_matrix::build_matrix;
  21.  
  22. pub struct Config {
  23.     pub query_filename: String,
  24.     pub bank_filename: String,
  25.     pub match_bonus: f32,
  26.     pub gap_penalty: f32
  27. }
  28.  
  29. impl Config {
  30.     pub fn new(args: &[String]) -> Config {
  31.         if args.len() < 5 {
  32.             panic!("pas assez d'arguments");
  33.         }
  34.         let query_filename = args[1].clone();
  35.         let bank_filename = args[2].clone();
  36.         let match_bonus: f32 = args[3].parse()
  37.             .expect("Ce n'est pas un nombre !");
  38.         let gap_penalty: f32 = args[4].parse()
  39.             .expect("Ce n'est pas un nombre !");
  40.        
  41.         Config {query_filename, bank_filename, match_bonus, gap_penalty}
  42.     }
  43. }
  44.  
  45. pub fn get_filenames() {
  46.     let args: Vec<String> = env::args().collect();
  47.     let config = Config::new(&args);
  48.    
  49.     println!("Alignement de {} avec {} \n", config.query_filename, config.bank_filename);
  50.    
  51.     let f_query = fasta_open_file(config.query_filename);
  52.     let f_bank = fasta_open_file(config.bank_filename);
  53.  
  54.     read_sequences(f_query,
  55.                    f_bank,
  56.                    config.match_bonus,
  57.                    config.gap_penalty);
  58.  
  59. }
  60.  
  61. fn fasta_open_file(filename: String) -> File {
  62.     let f = File::open(filename).expect("Fichier non trouvé !");
  63.     f
  64. }
  65.  
  66. fn get_sequence<B: BufRead>(count: &mut u8, ident: &mut String, lines: &mut Lines<B>)
  67.                             -> (String, Vec<u8>) {
  68.     let mut sequence: (String, Vec<u8>) = (String::new(), vec![]);
  69.     let mut sequence_nuc: Vec<u8> = vec![];
  70.    
  71.     for line in lines {
  72.         let the_line = line.unwrap();
  73.         if the_line.len() > 0 {
  74.             let first = &the_line[0..1];
  75.             match first {
  76.                 first if first == ">" => {
  77.                     if *count == 0 {
  78.                         *ident = the_line.clone();
  79.                         *count += 1;
  80.                     } else {
  81.                         sequence = (ident.to_string(), sequence_nuc.clone());
  82.                         println!("Numéro : {}", count);
  83.                         *ident = the_line.clone();
  84.                         sequence_nuc = vec![];
  85.                         *count += 1;
  86.                         return sequence;
  87.                     }
  88.                 }
  89.                 first if first != ">" => {
  90.                     sequence_nuc.extend(the_line.as_bytes())}
  91.                 &_ => {}
  92.             }
  93.         }
  94.     }
  95.     sequence = (ident.to_string(), sequence_nuc.clone());
  96.     println!("Numéro : {}", count);
  97.     sequence
  98. }
  99.  
  100. fn read_sequences(f_query: File,
  101.                   f_bank: File,
  102.                   match_bonus: f32,
  103.                   gap_penalty: f32) {
  104.     let fq = BufReader::new(&f_query);
  105.     let mut fq_iter = fq.lines();
  106.     let mut count: u8 = 0;
  107.     let mut ident = String::new();
  108.     let query_sequence = get_sequence(&mut count, &mut ident, &mut fq_iter);
  109.     print_seq(&query_sequence);
  110.  
  111.     let fb = BufReader::new(&f_bank);
  112.     let mut fb_iter = fb.lines();
  113.     count = 0;
  114.     loop {
  115.         let bank_sequence = get_sequence(&mut count, &mut ident, &mut fb_iter);
  116.         if bank_sequence.1.len() == 0 {
  117.             break} else {
  118.             //          print_seq(&bank_sequence);
  119.             build_matrix(&query_sequence,
  120.                          &bank_sequence,
  121.                          match_bonus,
  122.                          gap_penalty);
  123.         }
  124.     }
  125. }

Télécharger

 Construire la matrice d’alignement, calculer les scores

  1. // src/fasta_multiple_cmp/build_sequences_matrix.rs :
  2.  
  3. // This module was inspired by Vincent Esche's Seal crate,
  4. // but simplified and much more basic, without mmap and so on.
  5. // For pedagogic use.
  6.  
  7. use simple_matrix::Matrix;
  8. use std::str;
  9. use std::char;
  10.    
  11. pub fn build_matrix(sequence1: &(String, Vec<u8>),
  12.                     sequence2: &(String, Vec<u8>),
  13.                     match_bonus: f32,
  14.                     gap_penalty: f32) {
  15.  
  16.     let l_seq1: usize = (sequence1.1).len();
  17.     let l_seq2: usize = (sequence2.1).len();
  18.  
  19.     println!("Longueur première séquence : {} ", l_seq1);
  20.     println!("Longueur seconde séquence : {} ", l_seq2);
  21.        
  22.     let mut the_mat: Matrix::<f32> = Matrix::new(l_seq2+1, l_seq1+1);
  23.  
  24.     init_matrix(&mut the_mat, l_seq2+1, l_seq1+1, 0.0);
  25.  
  26.     nw_matrix(&mut the_mat, l_seq2+1, l_seq1+1, match_bonus, gap_penalty, &sequence1.1, &sequence2.1);
  27.  
  28.     print_ident(&sequence1);
  29.     print_ident(&sequence2);
  30.  
  31. //      print_matrix(&the_mat, &sequence2.1, l_seq2+1, l_seq1+1);
  32.  
  33.     print_score(&the_mat, l_seq2+1, l_seq1+1);
  34.    
  35. }
  36.  
  37. fn nw_matrix(the_mat: &mut Matrix::<f32>,
  38.              lin: usize,
  39.              col: usize,
  40.              match_bonus: f32,
  41.              gap_penalty: f32,
  42.              seq1: &Vec<u8>,
  43.              seq2: &Vec<u8>) {
  44.     for j in 1..col {
  45.         the_mat.set(0, j, gap_penalty * j as f32) ;
  46.     }
  47.     let mut score: f32 = 0.0;
  48.     for i in 1..lin {
  49.         the_mat.set(i, 0, gap_penalty * i as f32) ;
  50.         for j in 1..col {
  51.             if seq1[j-1] == seq2[i-1] {
  52.                 score = match_bonus} else {
  53.                 score = 0.0}
  54.             the_mat.set(i, j, max3(the_mat.get(i-1,j-1).unwrap()
  55.                                    + score,
  56.                                    the_mat.get(i-1,j).unwrap()
  57.                                    + gap_penalty,
  58.                                    the_mat.get(i,j-1).unwrap()
  59.                                    + gap_penalty));
  60.         }
  61.     }
  62. }
  63.  
  64. fn max3(v1: f32, v2: f32, v3: f32) -> f32 {
  65.     let tmp = f32::max(v2, v3);
  66.     if v1 > tmp {
  67.         return v1 } else {
  68.         return tmp };
  69. }
  70.    
  71. fn init_matrix(the_mat: &mut Matrix::<f32>, lin: usize, col: usize, val: f32) {
  72.     for i in 0..lin {
  73.         for j in 0..col {
  74.             the_mat.set(i, j, val) ;
  75.         }
  76.     }
  77. }
  78.  
  79. // "print_seq" affiche une séquence selon différents formats.
  80. pub fn print_seq(sequence: &(String, Vec<u8>)) {
  81.     println!("Ident : {:?}", sequence.0);
  82. //              println!("Séquence : {:?}", sequence.1);
  83.                 let sequence_str = str::from_utf8(&sequence.1).unwrap().to_string();
  84.     println!("Séquence : {}", &sequence_str);
  85. }
  86.  
  87. fn print_vector(the_vec: &Vec<u8>) {
  88.     let vec_str = str::from_utf8(the_vec).unwrap().to_string();
  89.     print!("{} ", "   ");
  90.     for c in vec_str.chars() {
  91.         print!("{} ", c);
  92.     }
  93.     print!("{}", "   \n");
  94. }
  95.    
  96. fn print_matrix(the_mat: &Matrix::<f32>, seq2: &Vec<u8>, lin: usize, col: usize) {
  97.     for i in 0..lin {
  98.         if i > 0 {print!("{} ", char::from(seq2[i-1]))} else
  99.         {print!("{} ", " ")};
  100.         for j in 0..col {
  101.             print!("{} ", the_mat.get(i, j).unwrap());
  102.         }
  103.         print!("{}", "\n")
  104.     }
  105. }
  106.  
  107. fn print_score(the_mat: &Matrix::<f32>, lin: usize, col: usize) {
  108.     println!("Score de similarité : {} ", the_mat.get(lin-1, col-1).unwrap());
  109.     print!("{}", "\n")
  110. }
  111.  
  112. fn print_ident(sequence: &(String, Vec<u8>)) {
  113.     println!("Ident : {:?}", sequence.0);
  114. }

Télécharger

 Créer un dépôt Git

Avec Git, chaque arborescence de répertoires qui contient le code source d’un projet devient un dépôt de ce projet, cf. cet excellent manuel en ligne (en français, ou presque toute autre langue au choix). Il suffit de lancer la commande d’initialisation du dépôt :

  1. git init

qui créera les répertoires .git (données de référence du dépôt), src (le code des programmes) et target (les exécutables, données de déboguage et autres).

Il faut ensuite ajouter au dépôt les fichiers intéressants, ainsi par exemple :

  1. git add src/fasta_multiple_cmp.rs
  2. git add src/fasta_multiple_cmp/build_sequences_matrix.rs
  3. ...

Télécharger

Il est bon, pardon, indispensable, pour un dépôt public, de prévoir un document de licence, pour ce qui me concerne j’ai choisi la licence Apache, moins rigide que la GPL ou même que la LGPL, mais après tout dépend du contexte dans lequel vous travaillez et de vos jugements sur la question.

Il est très souhaitable de rédiger un document d’introduction qui explique ce que fait votre programme et comment il s’utilise. GitHub semble préférer que ce texte soit écrit selon le langage de balisage Markdown et se nomme README.md, vous pouvez consulter ici leREADME.md du projet concerné, que j’ai baptisé fasebare.

Les commandes précédentes programment des opérations mais ne les effectuent pas, il faudra pour cela les commettre, au moyen de la commande de commission, qui vous demandera de rédiger quelques mots pour expliquez ce que vous commettez :

  1. git commit -a

 GitHub

GitHub est de nos jours le dépôt de codes source à la mode pour les logiciels libres, bien que ce soit une entreprise commerciale, rachetée par Microsoft. Dès la page d’accueil on vous propose d’ouvrir un compte, c’est ainsi que tout commence. Une fois le compte créé vous pouvez créer un projet. Le site vous propose une interface Web, soit un cliquodrome, je trouve que c’est plus simple d’usage par ligne de commande. Pour ce faire GitHub vous demandera de créer un token d’authentification pour pouvoir accéder à votre dépôt depuis votre machine locale.

Ensuite, pour envoyer votre projet sur GitHub (où j’ai créé le projet fasebare, vide, mon identifiant est laubloch), depuis le répertoire racine de votre dépôt local, c’est simple :

  1. git push https://github.com/laubloch/fasebare/

 Construire un exécutable

Oui, pour créer un binaire exécutable avec Cargo :

  1. cargo build --release

Le binaire sera ici :

  1. ./target/release/needleman_wunsch

 Importer le projet dans Framagit

Framagit est un autre dépôt de projets au format Git, basé sur le logiciel GitLab. C’est un des nombreux services libres créés par l’association Framasoft, il aurait donc été incorrect que je n’y publie pas mon code. Rien n’est plus simple : aussitôt identifié sur Framagit, on me propose d’importer des projets depuis d’autres dépôts, notamment GitHub. Voilà qui fut fait. Mon programme d’écolier est maintenant doublement immortel.

  README du paquet # fasebare

 Synopsis

The crate needleman_wunsch of the fasebare package consists of two Rust modules :

* fasta_multiple_cmp provides functions to read biological sequences (DNA, RNA or proteins) in FASTA formatted files ;
* sequences_matrix provides functions to build an alignment matrix of two sequences and to compute their similarity score, according to the Needleman-Wunsch algorithm.

The Data directory contains test data with artificial sequence data, and also true sequences extracted from the Genbank databank, in order to try the programs.

The cargo build system will build a standalone program to be invoked from the command line. The program has been built and run only with the Linux OS, but maybe it would run with other OS.

 Motivation

The first aim of this package was for the author to learn the programming language Rust, and to apply it to a domain he knows a bit, Bioinformatics. The author’s site gives some explanations of this approach (in French...).

These programs are intended for pedagogic use, if you use them for professional or scientific projects, it will be at your own risks.

 Credits

These programs invoke the following crate :

* simple_matrix

To take into account the dependency to this package, the Cargo.toml file must be :

  1. [package]
  2. name = "needleman_wunsch"
  3. version = "0.1.0"
  4. authors = ["Laurent Bloch <lb@laurentbloch.org>"]
  5. edition = "2018"
  6.  
  7. # See more keys and their definitions at
  8. # https://doc.rust-lang.org/cargo/reference/manifest.html
  9.  
  10. [dependencies]
  11. simple-matrix = "0.1"

Télécharger

The author found hints and inspiration from totof2000, Unknown, Nicolas Memeint.

 Principle of operations (summary)

Usually biologists work about a sequence of interest, which we will name the “query sequence”, and they try to compare it with a batch of sequences, the “bank”, in order to select the sequences of the bank with the higher similarity scores.

The similarity scores between two sequences are computed according to the Needleman-Wunsch algorithm. This algorithm build an alignment matrix. One sequence has its letters placed horizontally on the top of the matrix, each letter on the top of a column. The second sequence has its letter placed vertically on the left of the matrix, each letter on the left of a row. One extra line is placed below the top sequence, and one extra column is placed on the right of the left sequence. Each cell of the matrix will contain the score of each individual pair of letters.

To fill the matrix, the program computes each score for each individual pair of letters according to one of three situations (definitions borrowed from Wikipedia) :

* Match : The two letters at the current index are the same.
* Mismatch : The two letters at the current index are different.
* Gap : The best alignment involves one letter aligning to a gap in the other sequence.

So the algorithm needs two parameters to work : the value of the gap penalty, and the value of the mismatch penalty (or, alternatively, the value of the match bonus, which is the solution adopted for our program).

You could refer to the Wikipedia article for further explanations and details.

 To build and invoke the program :

For the developper, the command line to build the program is (from the base directory of the project) :

  1. cargo build

Then, you can invoke the program is as follows :

  1. cargo run <path to the file of the query sequence>
  2.           <path to the file of the sequences bank>
  3.           <value of the match bonus>
  4.           <value of the gap penalty (negative or zero)>

Télécharger

For instance, with test files from this repository :

  1. cargo run Data/seq_orchid2.fasta Data/sequences_orchid.fasta 1.0 -0.5

To build an executable binary file proceed as follows :

  1. cargo build --release

The executable file will be there :

  1. ./target/release/needleman_wunsch

Remember, with Rust, no runtime, so this executable is executable anywhere with your data.