C ++ - uma melhoria em relação à solução FUZxxl
Eu não mereço absolutamente nenhum crédito pelo próprio método de computação e, se nenhuma abordagem melhor aparecer nesse meio tempo, a recompensa deve ir para a FUZxxl à direita.
#define _CRT_SECURE_NO_WARNINGS // a Microsoft thing about strcpy security issues
#include <vector>
#include <string>
#include <fstream>
#include <iostream>
#include <cstdlib>
#include <ctime>
#include <cstring>
using namespace std;
#include "divsufsort.h"
// graceful exit of sorts
void panic(const char * msg)
{
cerr << msg;
exit(0);
}
// approximative timing of various steps
struct tTimer {
time_t begin;
tTimer() { begin = time(NULL); }
void print(const char * msg)
{
time_t now = time(NULL);
cerr << msg << " in " << now - begin << "s\n";
begin = now;
}
};
// load input pattern
unsigned char * read_sequence (const char * filename, int& len)
{
ifstream file(filename);
if (!file) panic("could not open file");
string str;
std::string line;
while (getline(file, line)) str += line;
unsigned char * res = new unsigned char[str.length() + 1];
len = str.length()+1;
strcpy((char *)res, str.c_str());
return res;
}
#ifdef FUZXXL_METHOD
/*
* Compute for all k the number of k-mers. kmers will contain at index i the
* number of (i + 1) mers. The count is computed as an array of differences,
* where kmers[i] == kmersum[i] - kmersum[i-1] + 1 and then summed up by the
* caller. This algorithm is a little bit unclear, but when you write subsequent
* suffixes of an array on a piece of paper, it's easy to see how and why it
* works.
*/
static void count(const unsigned char *buf, const int *sa, int *kmers, int n)
{
int i, cl, cp;
/* the first item needs special treatment */
/*
kuroi neko: since SA now includes the null string, kmers[0] is indeed 0 instead of 1
*/
// kmers[0]++;
for (i = 1; i < n; i++) {
/* The longest common prefix of the two suffixes */
cl = n - (sa[i - 1] > sa[i] ? sa[i - 1] : sa[i]);
#ifdef ICAP
cl = (cl > ICAP ? ICAP : cl);
#endif
for (cp = 0; cp < cl; cp++)
if (buf[sa[i - 1] + cp] != buf[sa[i] + cp])
break;
/* add new prefixes to the table */
kmers[cp]++;
}
}
#else // Kasai et al. method
// compute kmer cumulative count using Kasai et al. LCP construction algorithm
void compute_kmer_cumulative_sums(const unsigned char * t, const int * sa, int * kmer, int len)
{
// build inverse suffix array
int * isa = new int[len];
for (int i = 0; i != len; i++) isa[sa[i]] = i;
// enumerate common prefix lengths
memset(kmer, 0, len*sizeof(*kmer));
int lcp = 0;
int limit = len - 1;
for (int i = 0; i != limit; i++)
{
int k = isa[i];
int j = sa[k - 1];
while (t[i + lcp] == t[j + lcp]) lcp++;
// lcp now holds the kth longest commpn prefix length, which is just what we need to compute our kmer counts
kmer[lcp]++;
if (lcp > 0) lcp--;
}
delete[] isa;
}
#endif // FUZXXL_METHOD
int main (int argc, char * argv[])
{
if (argc != 2) panic ("missing data file name");
tTimer timer;
int blen;
unsigned char * sequence;
sequence = read_sequence(argv[1], blen);
timer.print("input read");
vector<int>sa;
sa.assign(blen, 0);
if (divsufsort(sequence, &sa[0], blen) != 0) panic("divsufsort failed");
timer.print("suffix table constructed");
vector<int>kmers;
kmers.assign(blen,0);
#ifdef FUZXXL_METHOD
count(sequence, &sa[0], &kmers[0], blen);
timer.print("FUZxxl count done");
#else
compute_kmer_cumulative_sums(sequence, &sa[0], &kmers[0], blen);
timer.print("Kasai count done");
#endif
/* sum up kmers differences */
for (int i = 1; i < blen; i++) kmers[i] += kmers[i - 1] - 1;
timer.print("sum done");
/* human output */
if (blen>10) blen = 10; // output limited to the first few values to avoid cluttering display or saturating disks
for (int i = 0; i != blen; i++) printf("%d ", kmers[i]);
return 0;
}
Simplesmente usei Kasai et al. algoritmo para calcular LCPs em O (n).
O resto é uma mera adaptação do código FUZxxl, usando recursos C ++ mais concisos aqui e ali.
Deixei o código de computação original para permitir comparações.
Como os processos mais lentos são a construção de SA e a contagem de LCP, removi a maioria das outras otimizações para evitar sobrecarregar o código para obter ganhos negligenciáveis.
Estendi a tabela SA para incluir o prefixo de comprimento zero. Isso facilita a computação do LCP.
Não forneci uma opção de limitação de comprimento, sendo o processo mais lento agora o cálculo do SA que não pode ser reduzido (ou pelo menos não vejo como poderia ser).
Também removi a opção de saída binária e a exibição limitada aos 10 primeiros valores.
Suponho que esse código seja apenas uma prova de conceito; portanto, não é necessário desorganizar as telas ou saturar os discos.
Construindo o executável
Eu tive que compilar todo o projeto (incluindo a versão litedivsufsort
) do x64 para superar o limite de alocação do Win32 2Gb.
divsufsort
O código lança vários avisos devido ao uso intenso de int
s em vez de size_t
s, mas isso não será um problema para entradas abaixo de 2 GB (que exigiriam 26 GB de RAM de qualquer maneira: D).
Compilação Linux
compile main.cpp
e divsufsort.c
use os comandos:
g++ -c -O3 -fomit-frame-pointer divsufsort.c
g++ -O3 main.cpp -o kmers divsufsort.o
Suponho que a divsufsort
biblioteca regular funcione bem no Linux nativo, desde que você possa alocar um pouco mais do que 3Gb.
Performances
O algoritmo Kasai requer a tabela SA inversa, que consome mais 4 bytes por caractere, totalizando 13 (em vez de 9 com o método FUZxxl).
O consumo de memória para entrada de referência é, portanto, acima de 3Gb.
Por outro lado, o tempo de computação é dramaticamente aprimorado e todo o algoritmo agora está em O (n):
input read in 5s
suffix table constructed in 37s
FUZxxl count done in 389s
Kasai count done in 27s
14 92 520 2923 15714 71330 265861 890895 2482912 5509765 (etc.)
Outras melhorias
A construção da SA é agora o processo mais lento.
Alguns bits do divsufsort
algoritmo devem ser paralelos a qualquer recurso interno de um compilador desconhecido para mim, mas, se necessário, o código deve ser fácil de se adaptar a multithreading mais clássico ( à exemplo do C ++ 11).
A lib também possui uma carga de parâmetros, incluindo vários tamanhos de balde e o número de símbolos distintos na cadeia de entrada. Eu só dei uma olhada superficial neles, mas suspeito que comprimir o alfabeto pode valer a pena tentar se suas cordas forem infinitas permutações de ACTG ( e você estiver desesperado por apresentações).
Também existem alguns métodos paralelizáveis para calcular o LCP a partir do SA, mas como o código deve ser executado em menos de um minuto em um processador um pouco mais poderoso que o meu insignificante i3-2100@3.1GHz e todo o algoritmo está em O (n), duvido disso. valeria a pena o esforço.
J
, uma solução ingênua seria simplesmente `[: ~.]` Mas acho que isso não será suficiente.