2022년 2월 27일 일요일

43 오극관 앰프의 전원 트랜스 교체

돌고 돌아 결국은 원래의 모습으로 돌아갔다. 50VA급 220V:220V 트랜스를 써서 얻은 전압을 정류하면 43 오극관에 그대로 공급하기에는 너무 높다. 몇 개의 저항을 거쳐서 160V 정도로 낮추었더니 전원 트랜스에서 듣기 싫은 떨림이 발생하였다. 똑같은 전원 트랜스를 6LQ8 싱글 및 푸시풀 앰프에서는 전혀 문제 없이 쓰고 있다.

아세아 전원의 50VA 220V:220V 복권 트랜스. 3개째 구입한 것이다. 다음에 6LQ8 PP amp를 하나 더 만들게 되면 그때 사용하자. 어쩌면 6V6 싱글 앰프에 응용할 수도 있겠다.

 그래서 처음 43 싱글 앰프를 만들었던 때의 조합으로 되돌아가기로 하였다. 220V:230V + 6V 전원 트랜스에 440V + 380V:220V + 110V 단권형 트랜스(30VA)를 조합하는 것. 단권형 트랜스에서는 밑줄친 탭을 사용하였다. 이 무슨 해괴한 조합이란 말인가? 사실 220V:110V 트랜스 하나면 얼추 해결될 일이었겠지만, 그러려면 또 비용이 들게 되므로 갖고 있는 트랜스 중에서 110V를 조금 넘는 출력을 얻을 수 있는 조합을 만들어 본 것이다. 

MOSFET을 사용한 리플 제거 회로에서 12V 정도를 잡아먹기 때문에, 2차에 150V가 나오는 전원 트랜스가 있었다면 초단관(6N2P)과 출력관(43) 전부를 그런대로 만족시키는 B+ 전압을 뽑을 수 있었을 것이다. 그러나 이런 트랜스는 주문제작을 하지 않으면 살 수가 없다. 진공관 오디오용으로 만들어진 표준형 전원 트랜스는 가격도 비싸고, 이렇게 낮은 전압을 출력하지는 않는다.

1940년대에 만들어진 라디오 수신기용 출력관을 응용하여 오디오 앰프를 만드느라 나도 나름대로 고생을 많이 하였다. 변변한 회로도가 없는 상태에서 출발하여 CAD로 만든 상판을 써서 제작을 하였으니 꽤 많은 공부와 경험을 하였노라고 자부해도 좋을 것이다.

6LQ8 싱글 앰프 RCA 단자의 불량을 해결하는 것을 포함하여 2022년 2월의 오디오 DIY를 마치도록 한다. 헐거운 RCA 단자는 교체 말고는 답이 없었다. 부품은 좋은 것을 쓰자! 특히 단자류의 품질에 소홀해서는 안 된다. 직접 손으로 그 품질을 느낄 수 있고, 결과는 소리에 고스란히 반영되기 때문이다.


2022년 2월 26일 토요일

Linux용 Windows 하위 시스템을 쓰게 되다

올해에는 입문자를 위한 미생물 유전체 분석 강좌를 개설하기로 하였다. 마지막으로 강사 역할을 했던 것이 벌써 2019년 11월이니 3년이라는 세월이 흘렀다. 이런 강좌를 준비할 때마다 항상 실습 환경을 마련하는 것이 고민이다. 수강생들이 리눅스 경험을 갖고 있을까? 리눅스 환경에 어느 정도는 익숙하다는 것을 전제로 준비를 해야 할까, 아니면 아예 없다고 가정하고 준비를 해야 할까? 리눅스에 대한 흥미를 유발하게 한다면 더할 나위가 없겠지만, 시커먼 화면에 글씨만 보이는 터미널에 대해서 거부감을 갖는 사람들이 적지 않다는 것이 현실이다.

2019년 강좌에서는 KOBIC에서 마련해 준 서버(우분투)에 프로그램과 실습용 데이터를 미리 설치한 다음 수업을 진행했었다. 그러나 교육 기간이 끝나면 서버를 닫아야 하니 복습 또는 심화 학습을 하기기 어려웠다. 나는 이를 대비하여 VirtualBox ova 파일과 스크립트를 별도로 배포하여 교육 기간이 끝난 뒤에도 직접 분석 과정을 경험해 보도록 나름대로 배려를 하였다.

올해 계획하고 있는 교육은 리눅스 경험이 없는 사람을 대상으로 할 것이다. 수강생들은 각자 노트북 컴퓨터를 갖고 오게 될 것이고, 윈도우 + 인터넷 환경에서 미생물 유전체 분석 과정에 대한 맛보기를 경험하게 될 것이다. 사실 아무리 초급 수준의 생명정보학이라 해도 리눅스를 배제하고 할 수 있는 일은 그렇게 많지 않다. 교육을 마치고 돌아간 뒤, 하드웨어 사양이 좋은 컴퓨터를 구입하여 리눅스를 설치한 뒤 직접 미생물 유전체 해독 자료(fastq file)를 조립하는 일이 생길 것을 기대하는 것은 어려울 것이다. 그러나 이 일이 정말로 필요함을 느끼게 되고, 그 과정이 어떻게 돌아가는지를 최소한 교육장에서는 체험해 볼 수 있게 하는 것이 나의 목표이다.

NCBI에서 직접 내려받았거나 시퀀싱 서비스 업체에서 받은 미생물 genome assembly가 있다고 가정하자. Contig는 몇 개이고, 길이는 얼마나 되고, 예측된 유전자의 수는 어떻고... 이런 것들은 전부 리눅스에서 명령행 한 줄이면 해결이 될 일이다. 상용 genomics tool을 쓰지 않아도 얼마든지 가능하다. 이런 체험을 하기 좋은 환경은 무엇일까? 교육 기간이 끝나면 쓰지 못하는 서버에 의존할 것이 아니라, 수강생이 준비해 온 노트북 컴퓨터에서 간단히 해결할 방법은 없는 것일까? 

이번에도 Oracle VirtualBox를 생각해 보았지만, 명령행 환경만 체험하기 위한 용도로는 너무 무겁고, 다소 복잡해 보이는 세부 설정 과정이 초보자에게는 부담이 될 것이다. 그러다가 대안으로 발견한 것이 바로 Linux용 Windows 하위 시스템(Windows Subsystem for Linux, WSL)이었다. 사용이 몹시 불편하던 기존의 명령 프롬프트를 대체함과 동시에 WSL과 파웨셸까지 포함하게 된 Windows Terminal을 같이 사용하면 명령행 기반으로 돌아가는 리눅스 프로그램을 실행하는데 불편함이 없다. WSL 환경에 설치한 우분투에 Conda를 이용하여 실습에 필요한 프로그램과 데이터를 받은 뒤, 이를 tar 파일로 export하여 수강생에게 배포하면 된다. 동등한 수준의 프로그램과 데이터를 받은 VirtualBox 파일보다 크기도 훨씬 작은 것 같다. GUI를 갖춘 JAVA 프로그램은 윈도우에서 직접 돌리면 된다.


VirtualBox를 통해서 리눅스를 사용할 때 느꼈던 어색함도 없고, 상호 파일을 전송하기 위해 공유폴더를 설정할 일도 없다. 그저 윈도우에서 탐색기를 열고 주소창에 '\\wsl$'라고 치면 된다. 또는 리눅스 쪽의 명령행에서 'explorer.exe .'를 쳐도 된다.

호스트 측과 게스트 간에 서로 사용할 자원(CPU나 메모리 및 디스크 공간)에 대한 경계를 확실히 세워 두고 작동하는 VirtualBox(그것이 장점이 되고 또 필요한 순간도 있을 것임)과는 달리 윈도우 안에서 작동하므로 이질감이 적고, 시스템 자원에 대한 특별한 경계가 없으니 유연한 운용이 가능할 것이다. 물론 VirtualBox는 리눅스를 호스트로 하여 쓸 수도 있다는 것이 큰 장점이다.

이러다가는 Cygwin을 쓸 사람이 하나도 없을 것 같다. Windows 11부터는 리눅스용 GUI 프로그램도 돌릴 수 있다고 하니 말이다.

어제 오후부터 WSL을 익히면서 세상이 이렇게 편해졌다는 사실에 새삼 놀라고 감탄하였다. 윈도우의 환경변수(%USERPROFILE% 또는 PowerShell 입장에서는 $env:USERPROFILE)에 대한 지식은 그동안 백지 상태였다가 이번 기회에 그 의미를 제대로 익히게 되었다.

경계가 허물어지는 세상이다. 여기에 더하여 Docker에 익숙해지면 정말 재미난 세상이 펼쳐질 것 같다.


2022년 2월 21일 월요일

FASTA file을 탭으로 구분된 파일로 펼치기(fasta2tbl 스크립트 + 줄 번호 삽입)

그렇게 중요하지 않은 일에 과도하게 집착을 하는 것은 아닌지 모르겠다. 어제 올린 글('목록을 이용하여 FASTA file에서 원하는 서열을 뽑아내기')에서 이미 awk를 이용한 방법을 소개하였었다. 이것은 특별한 문제가 없이 잘 작동하지만, description 필드를 구성하는 과정에서 사소한 오류가 있다. Description을 포함하는 서열 ID 라인을 처리할 때, awk의 필드 변수인 $1을 null 문자로 바꾸어 버린다. 그러고 나서 $0을 출력하게 만들게 하였더니 $1만 제거되고 그 뒤를 따르는 공백 문자가 description의 시작 부분에 남아 보기가 싫다. 때로는 description 없이 서열 ID만 존재하는 FASTA file도 있을 것이다.

많은 부분을 차지하지 않는 예외 사항까지 잘 처리할 수 있는 스크립트를 짜는데 의외로 많은 시간이 걸린다. 그리고 나는 이러한 마무리 과정에서 오히려 큰 희열을 느낀다. 이것이 바람직한 코딩의 자세인지는 잘 모르겠다. 보람을 상회하는 희열 추구는 자가발전의 경향이 있어서 에너지를 많이 소모하게 만드는데...

인터넷을 뒤져서 가장 완성도가 높아 보이는 awk 스크립트를 구한 다음, 기능을 확장하였다. 아래의 것을 fasta2tbl이라는 파일로 저장한 뒤 처리할 FASTA file 이름을 인수로 주어서 실행하면 된다. 결과물은 서열 ID, description(없는 경우 'NO_DESC'로 표시), 그리고 서열로 이루어진 3개의 컬럼으로 표현된다.

#!/usr/bin/awk -f
# FASTA file을 seq_ID, description, sequence의 세 컬럼으로 이루어진 tab-delimted file로 전환한다.
# description이 없는 경우 두 번째 컬럼에 'NO_DESC'을 채워 넣는다.
{
        if ($1~/^>/) {
                id = $1;
                sub(/^>/, "", id);
                if (NF == 1 )
                    desc = "NO_DESC"
                else
                    desc = $0;
                    sub(/^>[^\s]+\s/, "", desc)
                if (NR>1)
                        printf("\n%s\t%s\t", id, desc)
                else
                        printf("%s\t%s\t", id, desc)
        } else {
                printf("%s", $0)
        }
} END { printf "\n" }

# adapted from Josep Abril's Fasta2Tbl
# https://bioinformatics.stackexchange.com/questions/2649/how-to-convert-fasta-file-to-tab-delimited-file

awk는 세미콜론을 쓰는 방식이나 중괄호의 사용 등이 그렇게 엄격하지는 않은 것 같다. 문법이 헐렁하면 사용자는 게을러지는데...

다음 명령어 한 줄이면 FASTA file을 TSV file로 전환한 뒤 라인 번호(with leading zeros)를 삽입한 네 컬럼짜리 결과 파일을 얻게 될 것이다.

$ fasta2tbl input.fa | nl -n rz - > SeqWithLineNum.tab

2022년 2월 20일 일요일

목록을 이용하여 FASTA file에서 원하는 서열을 뽑아내기 - 심화편

이 주제는 나의 블로그에서도 여러 차례 다루어진 바가 있고, 구글을 검색해 보면 꽤 많은 질문과 답이 올라와 있다. 파이썬을 능숙하게 다루는 사람이라면 별로 어렵지 않게 스크립트를 짜서 원하는 바를 해결할 수 있을 것이다. 나 역시 Perl을 이용하여 이따금씩 필요한 염기서열을 multi-FASTA file에서 추출하고는 한다.

KentUtils라 불리는 Jim Kent의 유틸리티 중에서 faSomeRecords(다운로드 링크)가 이러한 목적에 딱 맞는 '이미 잘 알려진' 프로그램이다.'-exclude' 옵션을 이용하면 목록 파일(listFile)에 있는 서열을 제거하는 정반대의 동작을 한다.

$ faSomeRecords 
faSomeRecords - Extract multiple fa records
usage:
   faSomeRecords in.fa listFile out.fa
options:
   -exclude - output sequences not in the list file.

BBMap에 포함된 유틸리티 중에서 Filterbyname.sh 또한 faSomeRecords와 거의 같은 동작을 한다. 단,  fastq 파일도 다룰 수 있다는 것은 장점이다.

# By default, "filterbyname" discards reads with names in your name list, and keeps the rest. To include them and discard the others, do this:

$ filterbyname.sh in=003.fastq out=filter003.fq names=names003.txt include=t

잘 알려진 프로그램을 사용하는 방법을 알아 보았으니 이제부터는 리눅스에 기본적으로 포함된 유틸리티(awk 등)을 사용하여 다소 원시적인 방법으로 똑같은 일을 해 보자. 단, 전제 조건을 살짝 비틀어 보았다. 추출할 서열의 ID가 아니라 일련번호(1부터 시작)를 별도의 목록 파일에 갖고 있다고 가정한다.

FASTA file의 서열 ID, description 및 염기서열(줄바꿈을 제거하여 한 줄로 펼쳐야 함 - 흔히 unwrapping이라 부른다)을 탭으로 구분된 TSV 파일을 만들고, 이를 목록 파일과 비교하여 공통된 컬럼을 갖는 라인을 join으로 병합한 뒤 해당 라인만 추출하여 다시 FASTA file로 전환하는 것이 매우 일반적인 방법이다. 이러한 방법을 쓰는 경우에는 꼭 서열 일련번호가 아니라 서열 ID를 써서 원하는 서열을 뽑는 것도 무난하게 할 수 있다.

최근 알게 된 long read용 binning program인 LRBinner의 출력 파일('Bins.txt') 정보를 이용하여 입력 FASTA file에서 각 bin에 해당하는 염기서열을 별도의 파일로 뽑아내는 방법을 알아보도록 한다. Bins.txt는 입력 파일에 실린 각 염기서열이 속하는 bin 번호를 다음과 같이 보여준다. 참 건조하고 재미 없는 포맷이다. 

2
1
3
2
0
...

2번 bin에 속하는 염기서열은 1번과 4번에 해당한다. 2라는 값을 갖는 라인의 번호를 뽑아서 LineNum.txt 파일에 저장하려면 다음과 같이  awk 명령어를 실행한다.

awk '$1==2{print NR}' Bins.txt > LineNum.txt

nr이라는 유틸리티는 인수로 주어진 텍스트 파일을 라인 번호와 함께 출력한다. 당장 아무 텍스트 파일이나 가져다가 'nr file.txt'를 실행해 보라. 행 시작 부분의 공백을 제거하고 구분자를 탭으로 전환하려면 다음과 같이 약간의 수정을 거쳐야 한다. 맨 왼쪽에 라인 번호를 삽입한 텍스트 파일은 앞으로도 종종 쓸모가 있을 것이다.

nl -n ln -s $'\t' Bins.txt > BinWithLineNum.txt

이상에서 설명한 방법으로 추출해야 할 염기서열의 번호를 별도의 파일('LineNum.txt')에 저장하였다고 하자. 그러면 우선 입력물인 input.fa 파일을 TSV 파일('SeqWithNum.tab')로 펼치되, 첫 번째 컬럼에는 1에서부터 시작하는 서열 일련번호를 넣기로 한다.

awk '/^>/ { id=$1; 
            sub(/^>/,"",id); 
            $1=""; 
            printf("%s%d\t%s\t%s\t",(N>0?"\n":""),N+1,id,$0);
            N++;
            next; }
          { printf("%s", $0); } 
          END {printf("\n");}' input.fa > SeqWithNum.tab

awk  명령이 좋은 점은 따옴표로 둘러싼 긴 명령어 내부에서 가독성을 위해 줄바꿈을 해도 된다는 것. 단, END와 {..} 사이에 줄바꿈을 넣으면 에러가 남을 확인하였다. BEGIN과 {..} 사이도 마찬가지일 것이라고 생각한다. 이 awk 명령어를 여기에서 설명하지는 않겠다. 첫 번째 컬럼에 일련번호를 삽입하고,  descripton에 해당하는 여러 단어를 하나의 컬럼에 들어가게 하는 등 꽤 많은 공을 들였다. 하지만 이 awk 명령어는 나의 창작은 아니다. 또한 이것이 awk를 이용하여  FASTA file을 펼치는 유일하거나 가장 능률적인(짧은?) 방법도 아니다. 가끔씩 이 awk 명령어를 바라보면서 왜 이렇게 짜여졌는지 생각을 하는 것도 좋은 공부가 될 것이다.

LineNum.txt와 SeqWithNum.tab이라는 두 개의 파일이 만들어졌다. LineNum.txt 파일에 수록된 라인 번호에 해당하는 것을  SeqWithNum.tab에서 골라낸 뒤, 이를  FASTA file로 전환하는 것이 오늘의 목표이다. SeqWithNum.tab 파일의 첫 번째 컬럼에 1로부터 시작하는 라인 번호가 들어 있어서 점검하기는 편하다. 

오늘의 최종 목표를 달성하는 방법도 몇 가지가 있을 것이다. 우선 첫 번째 방법을 소개한다. 사실 이 방법을 쓰는 경우라면 SeqWithNum.tab 파일에 라인 번호를 별도의 컬럼으로 넣지 않아도 된다. 자세한 설명은 생략한다.

awk 'FNR==NR{a[$1];next}(FNR in a){print}' LineNum.txt SeqWithNum.tab

표준 출력으로 SeqWithNum.tab에서 조건에 맞는(즉 LineNum.txt에서 라인 번호를 지정한) 라인이 나올 것이다. 따라서 이를 FASTA file로 전환하여 저장하려면 파이프('|') 기호 뒤에 다음 명령어를 넣어야 한다(A).

awk -F"\t" '{printf(">%s %s\n%s\n",$2,$3,$4)}' | seqret fasta::stdin fasta:outfile.fa

왼쪽의 awk 명령어만을 사용해도 FASTA 기본 골격으로 출력은 된다. 그러나 서열 영역은 한 줄로 펼쳐진 형태가 되므로 이를 EMBOSS의 seqret로 처리하여 wrapping을 실시하였다.

최종 목표를 달성하는 두 번째 방법을 소개한다. 그것은 바로 sort와 join 명령어를 이용하는 것인데, 검색을 해 보면 두 텍스트 파일에서 특정 컬럼을 서로 비교하여 같은 값을 갖는 라인(혹은 선별된 컬럼)을 추출하는 일반적인 솔루션으로 소개하고 있다. 예를 들어서 두 개의 파일 file_A와 file_B가 있고, 각각의 첫 번째 컬럼이 같은 값을 갖는 경우 file_A의 1, 2번째 컬럼과 file_B의 4, 5번째 컬럼을 추출하 싶다면 다음과 같이 해야 한다. 컬럼은 탭 문자로 구분되어 있다고 가정한다.

join -t $'\t' -1 1 -2 1 -o 1.1,1.2,2.3,2.5 <(sort -t $'\t' -k1,1 file_A) <(sort -t $'\t' -k1,1 file_B)

생각보다 꽤 복잡하다. 리눅스 shell에서 컬럼 구분자를 탭문자로 설정하는 것(-t $'\t')도 그렇고, file_A와 file_B가 join field(같은 값을 갖는지 비교할 컬럼)에 대해서 이미 sort가 되어 있다 해도 여기에서 보인 process substitution에 의해서  join  명령어 내에서 sort를 다시 하지 않으면 경고문을 발생한다. '--nocheck-order' 옵션을 주면 될 것 같으나 제대로 작동을 하지 않아서 몹시 마음에 들지 않는다. sort나 join 모두 사용법을 정확히 알지 않으면 낭패를 겪기 쉬운 명령어이다.

따라서 LineNum.txt와 SeqWithNum.tab의 두 파일이 준비된 경우에 각 파일의 첫 번째 컬럼을 비교하여 같은 값을 갖는 라인을 SeqWithNum.tab에서 추출하여 FASTA file로 되돌리는 '두 번째의 복잡한' 방법은 다음과 같다.

join -t $'\t' -1 1 -2 1 <(sort -t $'\t' -k1,1 LineNum.txt) <(sort -t $'\t' -k1,1 SeqWithNum.tab)

이 명령어 뒤에 파이프 기호를 쓴 뒤 위에서 소개한 (A) 명령어를 써 넣어야 FASTA file로 결과가 저장된다. 이 방법은 아직 완벽하지 않다. process substitution 내에서 일반 문자의 순서로 소팅했기 때문에 라인 넘버로는 2, 3, 15 순서가 아니라 15, 2, 3...의 순서로 출력된다. sort 명령어의 단독 실행에서는 -k1n,1 또는 -nk1,1 옵션을 제공하여 숫자 기준의 정렬이 되지만, 이상하게도 join 명령어 안에서 <(sort ...) 형태로 실행하면 sort가 되지 않았다는 오류 메시지가 나온다. 따라서 단순하게 -k1,1  또는 -k1 옵션을 주어서 문자 순서로 sort를 해야만 한다.

오늘 학습의 교훈:

  • Don't reinvent the wheel. 창의력을 시험하고 싶다면 바퀴를 다시 발명해도 좋다.
  • awk에서 가끔은 printf() 함수를 사용할 필요가 있다.
  • sort 명령의 -k(key)는 시작과 끝을 지정하는 키 2개를 지정하는 것이 기본이 되어야 한다.
  • 예전에 쓴 글 Awk를 이용한 간단한 텍스트 파일의 조작(join)을 가끔 들추어 보면서 awk  명령어의 기본을 잊지 말도록 하자.

 

Numeric sort 문제가 없는 join 방법

라인 번호를 1, 2, 3이 아니라 00001, 00002, 00003과 같이 자릿수를 꽉 채운 0('leading zero')이 있는 것으로 바꾸면 해결이 된다. 단, seq_id, description 및 서열의 3개 컬럼으로 이루어진 TSV 파일을 먼저 만든 뒤  nl 명령을 사용하여 첫 컬럼에 라인 번호를 넣는다. nl 명령은 라인 번호를 오른쪽으로 정렬한 뒤 leading zero를 넣는 옵션('-n rz')이 있다. 전체 명령어는 다음과 같다.
 
 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
# 3개 컬럼으로 구성된 파일을 만든다.          
$ awk '/^>/ { id=$1; 
             sub(/^>/,"",id); 
             $1=""; 
         # 아래 코드는 seq_id, description, sequence의 3개 컬럼을 만든다,
             printf("%s%s\t%s\t",(N>0?"\n":""),id,$0);
         # 아래 코드는 1로부터 시작하는 번호를 첫 번째 컬럼에 넣는다(총 4개 컬럼).
         #   printf("%s%d\t%s\t%s\t",(N>0?"\n":""),N+1,id,$0);
             N++;
             next; }
           { printf("%s", $0); } 
           END {printf("\n");}' input.fa > Seq.tab
           
# 라인 번호를 첫 컬럼으로 삽입한다.
$ nl -n rz -s $'\t' Seq.tab > SeqWithNumLeadingZeros.tab

# LineNum.txt의 숫자에 leading zero를 채운다. 
# 그러려면 SeqWithNumLeadingZeros.tab에서 자릿수가 얼마나 필요한지를 알아내야 한다.       
$ for n in $(cat LineNum.txt)
> do
> printf "%06d\n" $n >> LineNumLeadingZeros.txt
> done

# join 실행(sort가 필요하지 않음)
$ join -t $'\t' LineNumLeadingZeros.txt SeqWithNumLeadingZeros.tab |\
    awk -F"\t" '{printf(">%s %s\n%s\n",$2,$3,$4)}' |\
    seqret -auto fasta::stdin fasta:outfile.fa

SeqKit의 fx2tab이라는 명령어를 쓰면 FASTA file을 TSV file로 전환할 수 있다. 물론 내가 원하는 형태와는 약간 다르다. 

어쩌면 나는 바퀴를 새로 발명하려고 애를 쓰고 있었는지도 모른다...

How to convert fasta file to tab delimited file

1
2
3
4
from Bio import SeqIO

for record in SeqIO.parse('input.fa', 'fasta'):
    print('{}\t{}\t{}'.format(record.id, record.description, record.seq))                                                                          

2022년 2월 16일 수요일

PyTorch를 이용하여 long read binner(LRBinner) 실행하기 - 일단은 실패!

Nanopore read 혹은 이를 대상으로 만들어진 조립물에 대하여 직접적으로 사용할 수 있는 metagenomics binning tool이 없을까? 지금까지 알려진 대부분의 도구는 short read를 대상으로 하였다. 이를 long read 혹은 그로부터 만들어진 assembly에 적용하기에는 무리가 있다. 왜냐하면 거의 필수 정보로 여겨지는 coverage information을 long read에 대해서 얻는 것은 - 불가능한 것은 아니지만 - 적합하지 않기 때문이다.

비교적 초창기에 개발된 소프트웨어인 MyCC를 잠시 테스트해 본 일이 있다. Coverage information을 주지 않고도 썩 잘 작동한다고 알려져 있지만, 요즘에는 사용자들의 관심 밖으로 밀려난 것만 같은 느낌이 든다. 계속 검색을 해 보니 MetaBCC-LR이라는 것이 눈에 뜨였다. 논문을 찾아서 읽어 보았다. k-mer와 trinucleotide composition 등 다양한 자료를 수집하여 bin 수립을 위한 모델을 만들어서 read를 분류한 뒤, 이를 별도로 조립하면('partitioned assembly') 샘플 속에 섞인 다양한 미생물의 유전체를 나누어서 재구성할 수 있다는 것이다.

이를 테스트하려고 소스를 컴파일하면서 잘 안되는 부분을 해결하려 애쓰다가 MetaBCC-LR의 저자 4명 중에서 3명이 모여 만들어낸 LRBinner라는 것까지 발견하게 되었다. 이는 MetaBCC-LR의 개선판으로서, read뿐만 아니라 contig도 입력물로 사용할 수 있다고 하였다. 개별 연구실에서 nanopore 장비를 직접 구입하여 응용의 폭을 점점 넓혀가는 요즘 시대에 아주 적절한 도구가 아닐 수 없다.

GitHub를 참조하여 conda에 환경을 만든 뒤 파이썬 소스를 클론하여 빌드를 하였다. 개발자가 제공하는 샘플 데이터를 이용하여 테스트 실행을 하는데 '--cuda'라는 옵션이 말썽을 부린다.

"raise AssertionError("Torch not compiled with CUDA enabled")"

그러면 이 옵션을 빼고 실행하면 되지 않겠나... 성공적으로 실행을 완료하였다. 

자, 그러면 최근 구입한 GeForce RTX 3080 장착 컴퓨터에 이를 설치하면 GPU를 이용한 계산이 되지 않을까? GitHub에서 알려준 방법 그대로 새 컴퓨터에 LRBinner를 설치하여 테스트 데이터를 분석해 보았다. 역시 같은 문제가 발생하였다. nvidia-cuda-toolkit 패키지가 설치되지 않았다는 사실을 발견하고 apt로 이를 설치한 뒤 LRBinner를 깔았으나 역시 마찬가지의 문제가 발생하였다. Guppy basecaller 실행에는 nvidia-driver-510을 설치하는 것으로 충분하였기에(이때 자동으로 cuda driver도 설치되는 것으로 알고 있음) 그때는 잘 몰랐었다.

PyTorch는 요즘 인기를 얻고 있는 오픈소스 머신 러닝 라이브러리로서, GPU 사용이 가능하다. 사실 Pytorch라는 것이 있다는 것도 오늘 처음 알았다! 윈도우에 PyTorch 설치, GPU 설정, 자세하게라는 글을 참조하여 다음과 같이 실행해 보았다.

$ conda create -n lrbinner -y python=3.7 numpy scipy seaborn h5py tabulate hdbscan gcc openmp tqdm biopython # 일단은 pytorch를 포함시키지 않음
$ conda activate lrbrenner
(lsbinner) $ conda install pytorch torchvision cudatoolkit=10.1 -c pytorch
$ python
Python 3.7.12 | packaged by conda-forge | (default, Oct 26 2021, 06:08:21) 
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import torch
>>> torch.cuda.get_device_name(0) 
'NVIDIA GeForce RTX 3090'
>>> torch.cuda.is_available() 
True
>>> torch.__version__
'1.4.0'
>>> quit()
(lrbinner) $ git clone https://github.com/anuradhawick/LRBinner.git
(lrbinner) $ cd LRBinner; python setup.py build

LRBinner가 잘 돌아갈 수 있게 PyTorch가 깔린 것 같다. 그런데 이번에는 다른 에러가 난다.

RuntimeError: cuDNN error: CUDNN_STATUS_EXECUTION_FAILED

으으으... 이건 또 뭔가? 어쩔 도리가 없이 '--cuda' 옵션을 제거한 뒤 다시 LRBinner를 실행하여 결과를 얻을 수 있었다.

기왕 장착한 GPU를 이용하여 더 많은 일을 해 보고자 하였는데 처음부터 완벽할 수는 없을 것이다. 일단 guppy는 잘 돌아가고 있고, 비록 GPU를 쓰는 상황은 못 되었지만 long read(or assembly)를 대상으로 하는 최신의 metagenomics binning tool을 쓰게 되었으니 완벽한 실패는 아닌 셈이다.


2022년 2월 13일 일요일

RTC1302를 이용한 아두이노 시계 코드

Michael Miller("Makuna")의 Rtc 라이브러리에 포함된 예제(DS1302_Simple)를 활용한 아두이노 시계의 코드를 싣는다. 택트 스위치를 누르면 인터럽트가 발동하여 12시-24시 표시를 전환하게 만드느라 이곳 저곳을 참조하여 코드 조각을 가져다가 일단은 돌아가게 만들어 놓았다.

// CONNECTIONS:
// DS1302 CLK/SCLK --> 7
// DS1302 DAT/IO --> 6
// DS1302 RST/CE --> 5
// DS1302 VCC --> 3.3v - 5v
// DS1302 GND --> GND

#include <ThreeWire.h>  
#include <RtcDS1302.h>
#include <LiquidCrystal.h> 
#define debounceTime 75 // <<- Set debounce Time (unit ms)

int interruptPin1 = 3; // 2 or 3
volatile int state = LOW;

char *dayArray[] = {
  "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};

ThreeWire myWire(6,7,5); // IO, SCLK, CE
RtcDS1302<ThreeWire> Rtc(myWire);
LiquidCrystal lcd(8, 9, 10, 11, 12, 13); //RS, EN, data(4,5,6,7)

void setup () 
{
    Serial.begin(9600);

    Serial.print("Compiled: ");
    Serial.print(__DATE__);
    Serial.print("\t");
    Serial.println(__TIME__);

    lcd.begin(16, 2);
    lcd.clear();

    pinMode(interruptPin1, INPUT_PULLUP); // pulled down using a 10K resistor
    attachInterrupt(digitalPinToInterrupt(interruptPin1), buttonPushed, FALLING);
    
    Rtc.Begin();

    RtcDateTime compiled = RtcDateTime(__DATE__, __TIME__);

    if (!Rtc.IsDateTimeValid()) 
    {
        // Common Causes:
        //    1) first time you ran and the device wasn't running yet
        //    2) the battery on the device is low or even missing

        Serial.println("RTC lost confidence in the DateTime!");
        Rtc.SetDateTime(compiled);
    }
    if (Rtc.GetIsWriteProtected())
    {
        Serial.println("RTC was write protected, enabling writing now");
        Rtc.SetIsWriteProtected(false);
    }
    if (!Rtc.GetIsRunning())
    {
        Serial.println("RTC was not actively running, starting now");
        Rtc.SetIsRunning(true);
    }

    RtcDateTime now = Rtc.GetDateTime();

    if (now < compiled) 
    {
        Serial.println("RTC is older than compile time!  (Updating DateTime)");
        Rtc.SetDateTime(compiled);
    }
    else if (now > compiled) 
    {
        Serial.println("RTC is newer than compile time. (this is expected)");
    }
    else if (now == compiled) 
    {
        Serial.println("RTC is the same as compile time! (not expected but all is fine)");
    }
}

void loop () 
{
    RtcDateTime now = Rtc.GetDateTime();
    printDateTime(now);

    if (!now.IsValid())
    {
        // Common Causes:
        //    1) the battery on the device is low or even missing and the power line was disconnected
        //Serial.println("RTC lost confidence in the DateTime!?!");
    }

    delay(1000); // one seconds
}

void printDateTime(const RtcDateTime& dt)
{
    char daystring[16];
    char timestring[16];
    String flag = "AM";
    String stateStr = "24-hr system";
    short DOW = dt.DayOfWeek();
    
    sprintf(daystring, " %4u-%02u-%02u %s", dt.Year(), dt.Month(), dt.Day(), dayArray[DOW]);

    short Hour = dt.Hour();

    if ( state ) { // 24-hr system
      lcd.clear();
      sprintf(timestring, "    %02u:%02u:%02u", dt.Hour(), dt.Minute(), dt.Second());
    } else {  // 12-hr system with AM/PM designation
       if (dt.Hour() > 12) {
         Hour = dt.Hour() - 12;
         flag = "PM";
       }     
       stateStr = "12-hr system with AM/PM designation";
       sprintf(timestring, "  %2d:%02u:%02u <%s>", Hour, dt.Minute(), dt.Second(), flag.c_str());
    }
    Serial.print("State ");
    Serial.print(state);
    Serial.print(": "); 
    Serial.println(stateStr);
    lcd.setCursor(0, 0);
    lcd.print(timestring);
    lcd.setCursor(0, 1);
    lcd.print(daystring);
}

void buttonPushed() {
    static unsigned long lastTime = 0;
    unsigned long Now = millis();
    if((Now - lastTime) > debounceTime)
    {
        state=!state;
        Serial.print("Button pressed: state changed to ");
        Serial.println(state);
        RtcDateTime now = Rtc.GetDateTime();
        printDateTime(now);
    }
    lastTime = Now;
}

C++ 함수 선언부에서 참조를 통하여 파라미터를 전송하는 방법을 겨우 이해하는 수준이다. 따라서 추가로 장착한 버튼을 눌러 시간 설정을 고치도록 개선한 버전 2 코드를 만들려면 아직 갈 길이 멀다. 그 다음 목표는 알람 기능 추가하기.

12시-24시 전환은 3번 버튼이 담당한다.

버튼을 이용한 시 변경 기능은 Accurate Clock Just Using an Arduino을 참조할 예정이다.  paulsb가 공개한 이 프로젝트에서는 그러나 RTC 모듈이라는 외부 하드웨어를 전혀 사용하지 않기 때문에 전원을 새로 연결할 때마다 시각을 새롭게 맞추어야 한다. Makuna의 Rtc 라이브러리를 가져다가 RTC 모듈에 데이터를 써 넣고 읽어오는 기능을 만들어 넣는 일이 필요하다. paulsb의 코드는 시간과 관련한 특별한 객체를 전혀 사용하지 않아서 시간 증가 및 표현을 전부 계산을 통해 구현한다. 공부 목적으로 살펴볼 가치가 있다.

LCD 또는 7-세그먼트 표시기를 이용한 시계 만들기는 아두이노 입문자의 프로젝트로 널리 인식되고 있다. 매우 단순한 목표라고 생각할 수도 있으나 시간이라는 데이터 구조가 의외로 복잡하여 공부를 할 것이 많다. 1970년 1월 1일 0시라는 유닉스 세계의 epoch time이 정해진 이유(사실 명확한 것은 없음) 등에 대해서도 흥미로운 이야깃거리가 많을 것이다.

2022년 2월 11일 금요일

숙원사업 - 드디어 GPU(GeForce RTX 3090)가 장착된 컴퓨터를 구입하여 나노포어 시퀀싱 준비를 완료하다

이제는 다른 연구팀의 장비를 빌리거나 CPU에서 극도로 느린 guppy basecalling을 하면서 며칠이고 기다릴 필요가 없게 되었다. NVIDIA GeForce RTX 3090이 장착된 데스크탑 컴퓨터를 구매하였기 때문이다. 이름은 ionic이라고 지었다. 메모리는 64 기가바이트로 맞추어서 주문하였다. 우분투 20.04의 설치용 USB 매체를 늘 갖고 다니기 때문에 아주 쉽게 리눅스부터 설치를 하였다. 

모니터 뒤편에는 같은 케이스에 만들어진 AMD Ryzen 5950X가 놓였다. AMD 머신에 비하면 말할 수 없이 조용하다! 

무슨 유흥업소 간판도 아니고 이렇게 요란하게 그래픽 카드를 만들다니...

NVIDIA 드라이버와 CUDA toolkit을 수동으로 설치하려다가 이상하게 꼬여서 고생을 하였다. 우분투를 싹 새로 설치한 다음, ubuntu-drivers devices 명령이 제안하는 드라이버를 찾아서 설치하니 아주 쉽게 CUDA까지 해결이 되었다.

$ ubuntu-drivers devices
== /sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0 ==
modalias : pci:v000010DEd00002204sv00001458sd00004043bc03sc00i00
vendor   : NVIDIA Corporation
driver   : nvidia-driver-470 - distro non-free
driver   : nvidia-driver-510 - distro non-free recommended
driver   : nvidia-driver-470-server - distro non-free
driver   : xserver-xorg-video-nouveau - distro free builtin
$ sudo ubuntu-drivers autoinstall
# or sudo apt install nvidia-driver-510
$ sudo shutdown -h reboot

설치 상태를 확인해 보았다. "NVIDIA GeForce RTX 3090"이라는 명칭이 잘 보인다.

$ nvidia-smi  --query | grep "Product Name"
    Product Name                          : NVIDIA GeForce RTX 3090
hyjeong@ionic:~$ nvidia-smi
Sat Feb 12 20:50:46 2022       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 510.47.03    Driver Version: 510.47.03    CUDA Version: 11.6     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA GeForce ...  Off  | 00000000:01:00.0 Off |                  N/A |
|  0%   32C    P8    15W / 370W |     68MiB / 24576MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|    0   N/A  N/A      1098      G   /usr/lib/xorg/Xorg                 56MiB |
|    0   N/A  N/A      1240      G   /usr/bin/gnome-shell                9MiB |
+-----------------------------------------------------------------------------+

다음으로는 나노포어 커뮤니티 사이트에 접속하여 MinKNOW와 Guppy GPU 버전을 설치하였다. 오랜만에 커뮤니티 사이트를 들어갔더니 Firefox(리눅스)와 Chrome(윈도우)에서 보이는 창이 달라서 소프트웨어 다운로드 위치를 찾느라 애를 먹었다. 더욱 혼동스러웠던 것은 'MinION Software' - 'MinIT Software' - 'MinKNOW Stand Alone GUI'라고 표현되어 있다는 점이다. 셋 중에서 가장 앞에 있는 것이 MinION Mk1B를 위한 것인데, 내 기억으로 전에는 분명히 MinKNOW라고 되어 있어서 어느 것을 골라야 하는지 주저할 필요가 없었다.

MinION Mk1B를 연결하여 인식이 됨을 확인하였다.

다음주에는 작년에 fast basecall로 생산해 둔 시퀀싱 결과를 guppy GPU version으로 다시 처리할 수 있을 것이다. 연구비에 여유가 있어서 이렇게 장비를 마련하게 된 것을 감사히 생각하며...

2022년 10월 1일 업데이트(NVIDIA 드라이버 재설치)

일린 일을 하기 위해 파견 근무 중에 주말을 이용하여 잠시 연구실에 돌아와서 컴퓨터를 켜 보았다. ionic 서버의 우분투 패키지를 업데이트 하고 났더니 NVIDIA 드라이버가 제대로 구동되지 않는 것 같았다. Ubuntu 18.04 nvidia 삭제 및 재설치라는 글을 참조하여 기존 드라이버를 전부 제거한 뒤 'sudo ubuntu-drivers install' 명령으로 다시 설치하였다. 'nvidia-smi' 명령을 입력해 보니 최신 버전으로 잘 업데이트가 되어 구동 중임을 확인할 수 있었다.

$ nvidia-smi
Sat Oct  1 11:31:00 2022       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 515.65.01    Driver Version: 515.65.01    CUDA Version: 11.7     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA GeForce ...  Off  | 00000000:01:00.0 Off |                  N/A |
|  0%   41C    P8    16W / 370W |      5MiB / 24576MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|    0   N/A  N/A      1110      G   /usr/lib/xorg/Xorg                  4MiB |
+-----------------------------------------------------------------------------+

MinKNOW나 Guppy도 업그레이드를 해야 될까? 오랜만에 Nanopore community 웹사이트에 접속을 해서 새로 나온 소프트웨어가 있는지 확인을 해 보도록 하자.