Wiele programów, także bioinformatycznych, uruchamia się podając nazwę programu a także zestaw opcji, które sterują sposobem działania programu, wskazują plik wejściowy oraz wyjściowy itp. Podczas tej lekcji napiszemy tego typu aplikację, która będzie odczytywała plik zawierający sekwencje nukleotydów DNA a następnie znajdywała szukaną sekwencję, odwracała ją i zmieniała w sekwencję komplementarną.
Zasada działania aplikacji
Programy działające w wierszu poleceń, które dotychczas znalazły się w tym kursie, działały na dwa sposoby. Niektóre po prostu uruchamialiśmy i działały od początku do końca bez pobierania informacji od użytkownika (albo ewentualnie pobierały je z pliku), inne umożliwiały na interakcję z użytkownikiem, który wybierał opcje menu czy wpisywał dane.
Wiele programów uruchamianych w wierszu poleceń (ang. Command Line Interface – CLI) działa jednak w innym sposób. Polega on na tym, że przy uruchamiania programu użytkownik podaje szereg parametrów, które następnie są używane przez program. Parametry te mogą dotyczyć tego jak program ma działać, nazwy pliku wejściowego, pliku do którego mają być przekazane wyniki itp.
Na przykład:
clustalw -infile=sekwencje.fasta -outfile=sekwencje_wyrownane.fasta -output=FASTA
Wywołany zostaje program clustalw
służący do dopasowywania sekwencji nukleotydów, który ma pobrać sekwencje z pliku sekwencje.fasta
, a wynik sformatować w formacie FASTA
i zapisać w pliku sekwencje_wyrownane.fasta
Dla początkującego może to się wydawać trudne i mało intuicyjne ale nie bez powodu tego typu programy są bardzo popularne w systemach Uniksowych, także w bioinformatyce. Do najważniejszych zalet takiej obsługi programów należą: szybkość uruchamiania i przekazania parametrów, łatwość dokumentowania wywołania programów wraz z konkretnymi zestawami parametrów, łatwość uruchamiania programów w skryptach co pozwala np. na automatyczne wielokrotne uruchomienie programu z różnymi zestawami parametrów.
Tym razem pokażę jak pisać tego programu w Javie. Napiszemy przy okazji program bioinformatyczny, który może się przydać przy pracy z sekwencjami nukleotydów zapisanymi w formacie FASTA
.
Proste przekazanie opcji z linii komend
Zacznijmy od prostego programu, który będzie przyjmował parametry podczas jego uruchamiania.
Stwórz nowy projekt o nazwie „Parametry”.
Przyjrzyjmy się teraz klasie wygenerowanej automatycznie przez NetBeans:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package parametry; public class Parametry { /** * @param args the command line arguments */ public static void main(String[] args) { // TODO code application logic here } } |
Zauważ, że w pierwszym komentarzu znajduje się informacja, że args
zawiera argumenty linii poleceń. Jest to tablica łańcuchów znaków, która jest parametrem metody main
. Dotychczas nie używaliśmy jej, teraz to się zmieni.
Uzupełnij kodem metodę main
:
1 2 3 4 5 6 7 8 9 10 |
public static void main(String[] args) { System.out.println("Parametry:"); int nr = 1; for(String a : args) { System.out.println("Parametr " + nr + " = " + a); nr++; } } |
Po uruchomieniu z poziomu NetBeans nic ciekawego nie uzyskamy, jedynie w okienku z wynikiem działania programu uzyskamy napis Parametry:
– nie podaliśmy żadnych parametrów. Co prawda da się to zrobić z poziomu NetBeans ale czas uruchomić program z linii komend podając parametry. Najpierw jedna stwórzmy plik jar
. Przypominam, że można to zrobić wybierając z menu Run
->Clean and Build
.
Otwórz teraz okno terminala. W Windows można je uruchomić wpisując cmd
w okienku menu uruchamiania aplikacji, jeśli używasz innych systemów operacyjnych zapewne wiesz jak to zrobić ;-)
Teraz przejdź do podkatalogu dist
, który znajduje się w katalogu z projektem.
Znajdują się tam pliki: Parametry.jar
oraz README.TXT
Wykonaj polecenie:
(znak $
jest znakiem zachęty linii komend, w Twoim terminalu może wyglądać inaczej. Nie przepisuj go.)
$ java -jar parametry.jar
Efekt podobny do poprzedniego. Zatem przekażmy parametry do naszego programu:
$ java -jar parametry.jar par1 par2 par3
Parametry:
Parametr 1 = par1
Parametr 2 = par2
Parametr 3 = par3
Jak widać tym razem możemy wydrukować poszczególne parametry, które przekazaliśmy do programu przy jego uruchamianiu. A skoro tak, to możemy ich także użyć w dowolny inny sposób.
Uzupełnij kod o polecenia:
System.out.println("-----------------");
if (args[0].equalsIgnoreCase("o"))
System.out.println("Otwieram plik "+ args[1]);
else
System.out.println("Błędny parametr");
Trzeba ponownie zbudować plik jar
(Run
->Clean and Build
).
W terminalu musisz wyjść z katalogu dist
i ponownie do niego wejść:
$ cd ..
$ cd dist
Uwaga: W systemie Windows należy wyjść z katalogu dist
przed zbudowaniem jar
-a.
Oczywiście zamiast wchodzić i wychodzić z katalogu dist
można uruchamiać program z katalogu powyżej katalogu dist:
$ java -jar dist/parametry.jar par1 par2 par3
będę się jednak trzymał poprzedniej konwencji.
Teraz znów uruchamiamy program:
1 2 3 4 5 6 7 |
$ java -jar parametry.jar o dane.txt Parametry: Parametr 1 = o Parametr 2 = dane.txt Otwieram plik dane.txt |
Jak widać pierwszy parametr służy do sterowania przebiegiem programu, drugi zawiera nazwę pliku do otwarcia.
Taki sposób daje dość duże możliwości przekazywania parametrów ale ma pewną istotną wadę: jest bardzo sztywny. Trzeba zawsze przekazywać parametry w określonej kolejności. Bardziej użyteczny jest sposób pokazany powyżej na przykładzie programu clustalw
polegający na tym, że parametry są odpowiednio oznaczane, mamy więc do czynienia z parami: nazwą parametru i jego wartością, przy czym kolejność tych par jest dowolna.
Można oczywiście samemu napisać kod, który po pobraniu parametrów rozpoznawałby nazwy parametrów i przypisane do nich wartości. Proponuję jednak użyć już istniejącej biblioteki, która znacznie ułatwia to zadanie.
Biblioteka Commons CLI
Wejdź na stronę: https://commons.apache.org/proper/commons-cli/
Znajduje się na niej krótki opis możliwości biblioteki.
Pobierz aktualną wersję biblioteki Commons CLI
z działu „Download”, obecnie jest to plikcommons-cli–1.4-bin.zip
lubcommons-cli–1.4-bin.tar.gz
. Rozpakuj pobrany plik i umieść w wygodnym miejscu.
Następnie utwórz projekt o nazwie seqreverter
.
Dołącz do projektu plik commons-cli-1.4.jar
oraz dokumentację tej biblioteki znajdującą się w pliku commons-cli-1.4-javadoc.jar
.
Instrukcja wyjaśniająca jak to zrobić znajduje się w lekcji „Java [27] – Pakiety, biblioteki`„.
Teraz umieść w metodzie main
kod:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
// Tworzymy kolekcję przechowującą możliwe opcje Options options = new Options(); // Plik wejściowy. Znaczenie parametrów: // Nazwa krótka (i), nazwa długa: "input", // Czy posiada argument (nazwa pliku): "true", Opis parametru: "Plik wejściowy" Option inputOpt = new Option("i", "input", true, "Plik wejściowy"); // Opcja jest obowiązkowa inputOpt.setRequired(true); // Opcja dodana do obiektu "options" options.addOption(inputOpt); // Nazwa pliku wyjściowego Option outDirOpt = new Option("o", "output", true, "Plik wyjściowy"); // Ta opcja nie jest obowiązkowa outDirOpt.setRequired(false); options.addOption(outDirOpt); // Opcja nazwy sekwencji, która zostanie odwrócona Option seqName = new Option("s", "sequence", true, "Nazwa (lub jej część) sekwencji do odwrócenia"); seqName.setRequired(true); options.addOption(seqName); // Parser opcji CommandLineParser parser = new DefaultParser(); // Obiekt służący formatowaniu pomocy HelpFormatter helpFormatter = new HelpFormatter(); // Tu będzie przychowywana lista opcji parsowana wg. ich opisu zawartego w "options" CommandLine cmd; try { // Parsuwanie opcji cmd = parser.parse(options, args); } catch (org.apache.commons.cli.ParseException ex) { // Jeśli się nie udało, np. przez brak wymaganych opcji i/lub ich parametrów // wyświetlany jest odpowiedni komunikat System.out.println("*** ERROR: ***"); System.out.println(ex.getMessage()); // Drukowanie pomocy helpFormatter.printHelp("java -jar seqreverter", options); // Wyjście z programu z kodem błędu (1) System.exit(1); return; } // Drukujemy przekazane opcje System.out.println("Przekazane opcje:"); String fileIn = cmd.getOptionValue("input"); System.out.println("Plik wejściowy:\t" + fileIn); String fileOut = cmd.getOptionValue("output"); System.out.println("Plik wyjściowy:\t" + fileOut); String seq = cmd.getOptionValue("sequence"); System.out.println("Sekwencja:\t" + seq); |
Zbuduj plik jar
, przejdź do katalogu dist
uruchom program bez parametrów:
$ java -jar seqreverter.jar
*** ERROR: ***
Missing required options: i, s
usage: java -jar seqreverter
-i,--input Plik wejściowy
-o,--output Plik wyjściowy
-s,--sequence Nazwa (lub jej część) sekwencji do odwrócenia
Jak widać program przerwał działanie i wyświetlił informację o tym jakich obowiązkowych opcji brakuje i wyświetlił krótką pomoc dotyczącą możliwych opcji.
Wywołajmy zatem program z obowiązkowymi opcjami:
$ java -jar seqreverter.jar -i dane.fasta -s Zea
Przekazane opcje:
Plik wejściowy: dane.fasta
Plik wyjściowy: null
Sekwencja: Zea
Program się uruchomił i wyświetlił wartości przekazanych opcji.
Teraz użyjmy wszystkich opcji:
$ java -jar seqreverter.jar -i dane.fasta -s Zea -o rezultat.fasta
Przekazane opcje:
Plik wejściowy: dane.fasta
Plik wyjściowy: rezultat.fasta
Sekwencja: Zea
Można też użyć długich nazw opcji, przy czym można mieszać nazwy krótkie i długie:
$ java -jar seqreverter.jar --input dane.fasta -s Zea --output rezultat.fasta
Przekazane opcje:
Plik wejściowy: dane.fasta
Plik wyjściowy: rezultat.fasta
Sekwencja: Zea
Teraz pozostaje „tylko” dodać sensowną funkcjonalność naszemu programowi.
Dodajemy funkcjonalność
Pliki FASTA
to pliki tekstowe służące do przechowywania sekwencji DNA, RNA albo białek w dość prostym formacie. Może on wyglądać np. tak:
Magnolia_stellata
CTGCTAACTCTCAGTTTGGTCCTACTTCTGGTTCATTTTGTTACTAA
AAACGGAGGGGGAAACTCAGTACCAAATGCTTGGCAATCCTTGGTAG
AGCTTATTCATGATTTCGTGCCGAACCCGGTAAACGAACAAATAGGT
GGTCTTTCCGGAAATGTTCAACAAAAGTTTTCCCCTCGCATCT
Solanum_tuberosum
CTACTAACTCTCAGTTTGGTCCTACTTTTGGTTTATTTTGTTACTAA
AAAGGGAGGAGGAAACTCAGTACCAAATGCTTGGCAATCCTTGGTAG
AGCTTATTTATGATTTCGTGCTGAACCCGGTAAACGAACAAATAGGT
GGTCTTTCCGGAAATGTTAAACAAAAGTTTTCCCCTCGCATCT
Mimulus_guttatus
AGATGCAAGGGAAAAACTTTTGTTTCACATTTCCGGAAAGACCACCT
ATTTGTTCGTTTACCAGGTTCGGCACGAAATCATAAATAAGCTCTAC
CAAGGATTGCCAAGCATTTGGTACTGAGTTTCCTCCTCCCTTTTTAG
TAACAAAATGAACAAAAAGTAGGACCAAACTGAGAGTGAGTAG
Orobanche_coerulescens
CTGCTAACTCTCAGTTTGGTCCTACTTCTGATTCATTTTGTTACTAA
AAAGGGAGGAGGAAACTCAGTACCAAATGCTTGGCAATCCGTGGTAG
AGTTTATTTATGATTTCGTGCTGAACCTGGTAAACGAACAAATAGGG
GGTCTTTCCGGAAATGTTAAACAAAAGTTTTTCCCTTGCATCT
Orobanche_eliator
CTACTCACTCTCAGTTTGGTCCTACTTTTTGTTCATTTTGTTACGAA
AAAGGGAGGAGGAAAGTCAGTACCAAATGCTTTTCAATCCGTGTTAG
AGCTTATTTATGATTTTGTGCCGAACCTGGTAAACGAACAAATAGGT
GGTCTTTCCGGAAATGTGAAACAAAAGTTTTTCCCTTGCATCT
Jak widać linie zaczynające się od znaku >
zawierają opis sekwencji, np. nazwę gatunku, kolejne linie zawierają sekwencję nukleotydów (lub aminokwasów), aż kolejnej do linii ze znakiem >
. Sekwencja nukleotydów może być podzielona na wiele linii, ale może też zawierać się w jednej.
Program seqreverter
będzie odczytywał plik w formacie FASTA
a następnie dokonywał zmiany sekwencji, której opis będzie zawierał przekazany do programu łańcuch znaków w ten sposób, że najpierw odwróci sekwencję a następnie utworzy na jej podstawie sekwencję komplementarną. Tego typu czynność często wykonuje się przy pracy z sekwencjami nukleotydów. Zauważ, że sekwencja gatunku Mimulus guttatus różni się znacznie od pozostałych, jest wobec nich odwrócona i (częściowo) komplementarna.
Nasz program będzie miał nieco ograniczoną funkcjonalność – nie uwzględnia pozostałych symboli kodu IUPAC i działa wyłącznie na sekwencjach DNA (nie uwzględnia „U”).
Utwórz plik o nazwie dane.fasta
, umieść w nim powyższe sekwencje i zapisz w katalogu z projektem (powyżej katalogu dist
).
Teraz utwóż drugą klasę w naszym projekcie i nazwij ją Sekwencja
.
Zawartość pliku Sekwencja.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package seqreverter; public class Sekwencja { private String opis; private String nukleotydy; public String getOpis() { return opis; } public void setOpis(String opis) { this.opis = opis; } public String getNukleotydy() { return nukleotydy; } public void setNukleotydy(String nukleotydy) { this.nukleotydy = nukleotydy; } } |
Jak widać jest to prosta klasa, zawierająca dwa pola odpowiadające opisowi sekwencji i samej sekwencji nukleotydów a także odpowiednie akcesory i modyfikatory.
Teraz do klasy głownej dodaj kilka metod a także uzupełnij metodę main
. Kompletny kod pliku Seqreverter.java
powinien wyglądać tak:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
package seqreverter; import java.io.File; import java.io.FileNotFoundException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Scanner; import java.util.logging.Level; import java.util.logging.Logger; import javax.sound.midi.Sequence; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; public class Seqreverter { public static void main(String[] args) { // Tworzymy kolekcję przechowującą możliwe opcje Options options = new Options(); // Plik wejściowy. Znaczenie parametrów: // Nazwa krótka (i), nazwa długa: "input", // Czy posiada argument (nazwa pliku): "true", Opis parametru: "Plik wejściowy" Option inputOpt = new Option("i", "input", true, "Plik wejściowy"); // Opcja jest obowiązkowa inputOpt.setRequired(true); // Opcja dodana do obiektu "options" options.addOption(inputOpt); // Nazwa pliku wyjściowego Option outDirOpt = new Option("o", "output", true, "Plik wyjściowy"); // Ta opcja nie jest obowiązkowa outDirOpt.setRequired(false); options.addOption(outDirOpt); // Opcja nazwy sekwencji, która zostanie odwrócona Option seqName = new Option("s", "sequence", true, "Nazwa (lub jej część) sekwencji do odwrócenia"); seqName.setRequired(true); options.addOption(seqName); // Parser opcji CommandLineParser parser = new DefaultParser(); // Obiekt służący formatowaniu pomocy HelpFormatter helpFormatter = new HelpFormatter(); // Tu będzie przychowywana lista opcji parsowana wg. ich opisu zawartego w "options" CommandLine cmd; try { // Parsuwanie opcji cmd = parser.parse(options, args); } catch (org.apache.commons.cli.ParseException ex) { // Jeśli się nie udało, np. przez brak wymaganych opcji i/lub ich parametrów // wyświetlany jest odpowiedni komunikat System.out.println("*** ERROR: ***"); System.out.println(ex.getMessage()); // Drukowanie pomocy helpFormatter.printHelp("java -jar seqreverter", options); // Wyjście z programu z kodem błędu (1) System.exit(1); return; } // Drukujemy przekazane opcje System.out.println("Przekazane opcje:"); String fileIn = cmd.getOptionValue("input"); System.out.println("Plik wejściowy:\t" + fileIn); String fileOut = cmd.getOptionValue("output"); System.out.println("Plik wyjściowy:\t" + fileOut); String seq = cmd.getOptionValue("sequence"); System.out.println("Sekwencja:\t" + seq); // Czytanie zawartości pliku wejściowego ArrayList sekwencje = czytajPlik(fileIn); sekwencje = odwroc(sekwencje, seq); // Jeśli podany został plik wyjściowy to zapisz w nim if(fileOut != null) zapisz(sekwencje, fileOut); // Jeśli nie został podany to zapisz wynik w pliku wejściowym else zapisz(sekwencje, fileIn); } /** * Metoda czytająca plik wejściowy, zwraca listę z obiektami typu Sekwencja */ static private ArrayList czytajPlik(String file) { File fileIn = new File(file); ArrayList sekwencje = new ArrayList(); try { Scanner skaner = new Scanner(fileIn); Sekwencja seq = new Sekwencja(); String linia = ""; // Pętla odczytująca kolejne linie kodu while (skaner.hasNextLine()) { linia = skaner.nextLine(); // Jeśli linia zawiera znak ">" to znaczy, że to jest linia opisu // sekwencji, a więc to początek nowej sekwencji. // Tworzy sie nowy obiekt typu Sekwencja if (linia.contains(">")) { // Skoro nowa sekwencja to trzeba dodać do listy starą, która jest już kompletna // ale tylko wtedy, jeśli własnie nie odczytujemy pierwszej sekwencji. // W takim wypadku wartość pola Opis == null. if (seq.getOpis() != null) { sekwencje.add(seq); } // Tworzymy nowy obiekt typu Sekwencja seq = new Sekwencja(); // Ustawiamy opis seq.setOpis(linia); // Ustawiamy początkową wartość sekwecnji nukleotydy, jeśli tego // nie zrobimy otrzymamy sekwencję zaczynającą się od łąńcucha "null" seq.setNukleotydy(""); } else { // Jeśli nie ma w linii znaku ">" to znaczy, że zawiera ona sekwencję // nukleotydów (albo jest to linia pusta), dodajemy ją zatem do // poprzedniej części sekwencji nukleotydów. // W ten sposób łączymy wiele linii sekwencji nukleotydów seq.setNukleotydy(seq.getNukleotydy() + linia); } } // Koniec plikum dodajemy obiekt do listy if (!skaner.hasNextLine() && seq.getOpis() != null) { sekwencje.add(seq); } } catch (FileNotFoundException e) { System.out.println("Plik " + file + " nie został znaleziony!"); } return sekwencje; } /** * Metoda odwraca sekwencję nukleotydów której opis zawiera szukany łańcuch znaków */ private static ArrayList odwroc(ArrayList sekwencje, String szukane) { // Pętla operuje na kolejnych obiektach seq for (int i = 0; i < sekwencje.size(); i++) { Sekwencja seq = sekwencje.get(i); // jeśli opis zawiera szukany łańcuch.... if (seq.getOpis().contains(szukane)) { // W ten sposób odwracamy łańcuch znaków String reverse = new StringBuilder(seq.getNukleotydy()).reverse().toString(); // Wywołanie metody zmien, która tworzy komplementarną sekwencję nukleotydów reverse = zmien(reverse); // Ustawiamy odwróconą i komplementarną sekwencję nukleotydów... seq.setNukleotydy(reverse); //... iumieszczamy obiekt ponownie w liście sekwencje.set(i, seq); } } return sekwencje; } /** * Metoda, która tworzy komplementarną sekwencję nukleotydów */ private static String zmien(String sekwencja) { // Najpierw zmieniamy litery na liczby sekwencja = sekwencja.replaceAll("A", "1"); sekwencja = sekwencja.replaceAll("T", "2"); sekwencja = sekwencja.replaceAll("C", "3"); sekwencja = sekwencja.replaceAll("G", "4"); // Teraz liczby zmieniamy na litery oznaczające komplementarne nukleotydy sekwencja = sekwencja.replaceAll("1", "T"); sekwencja = sekwencja.replaceAll("2", "A"); sekwencja = sekwencja.replaceAll("3", "G"); sekwencja = sekwencja.replaceAll("4", "C"); return sekwencja; } /** * Zapis pliku */ private static void zapisz(ArrayList sekwencje, String filename) { PrintWriter out = null; try { out = new PrintWriter(filename); // Zostają pobrane kolejne obiekty a nastepnie zostają pobierane z nich // opis oraz sekwencja nukleotydów for (Sekwencja s : sekwencje) { out.println(s.getOpis()); out.println(s.getNukleotydy()); } out.close(); } catch (FileNotFoundException ex) { System.out.println("Nie mogę zapisać danych w pliku " + filename); Logger.getLogger(Seqreverter.class.getName()).log(Level.SEVERE, null, ex); } } } |
Wyjaśnienia zasady działania znajdują się w komentarzach.
Teraz utwóż plik jar
, przejdź do katalogu dist
i wykonaj polecenie:
$ java -jar seqreverter.jar -i ../dane.fasta -s Mimulus -o ../rezultat.fasta
Powyżej katalogu dist
znajdziesz plik rezultat.fasta
.
Otwóż go w edytorze tekstu i porównaj z plikiem dane.fasta
. Teraz sekwencja Mimulus guttaus pasuje do pozostałych.
Zauważ, że sekwencje nukleotydów zawarte są teraz w jednej linii. Ponadto sekwencja Mimulus_guttatus jest odwrócona.
Jeśli nie podasz pliku wejściowego to zmieniony zostanie plik wejściowy. Jeśli zatem chcesz zmienić wiele sekwencji, których wyszukanie wymaga podania różnych łańcuchów znaków to dobrym pomysłem jest najpierw skopiowanie plikufasta
a następnie dokonywanie kolejnych modyfikacji na kopii.
Leave a Reply
You must be logged in to post a comment.