-
Notifications
You must be signed in to change notification settings - Fork 4
feat: 프라이머 설계 알고리즘 최적화 및 좌표계 관련 수정 #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -72,7 +72,6 @@ def __init__(self, genome_fasta: str, annotation_db: str): | |||||||||||||||||||||||||||||
| self.genome = pysam.FastaFile(genome_fasta) | ||||||||||||||||||||||||||||||
| self.db = sqlite3.connect(annotation_db) | ||||||||||||||||||||||||||||||
| self.cur = self.db.cursor() | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def generate_candidates( | ||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||
| template: str, | ||||||||||||||||||||||||||||||
|
|
@@ -83,7 +82,7 @@ def generate_candidates( | |||||||||||||||||||||||||||||
| max_poly_x=4, | ||||||||||||||||||||||||||||||
| gc_clamp=True, | ||||||||||||||||||||||||||||||
| ) -> List[Dict]: | ||||||||||||||||||||||||||||||
| """Stage 2.1: 물성 기반 후보군 생성""" | ||||||||||||||||||||||||||||||
| """Stage 2.1: 물성 기반 후보군 생성 (1-based 반영)""" | ||||||||||||||||||||||||||||||
| candidates = [] | ||||||||||||||||||||||||||||||
| for k in range(k_min, k_max + 1): | ||||||||||||||||||||||||||||||
| for i in range(len(template) - k + 1): | ||||||||||||||||||||||||||||||
|
|
@@ -92,14 +91,16 @@ def generate_candidates( | |||||||||||||||||||||||||||||
| tm = calc_tm_nn(s) | ||||||||||||||||||||||||||||||
| if not (tm_range[0] <= tm <= tm_range[1]): | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
| # 1. Poly-X 필터 반영 | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # 1. Poly-X 필터 | ||||||||||||||||||||||||||||||
| if any(base * max_poly_x in s for base in "ATCG"): | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # 2. GC Content 필터 반영 | ||||||||||||||||||||||||||||||
| # 2. GC Content 필터 | ||||||||||||||||||||||||||||||
| gc_content = (s.count("G") + s.count("C")) / len(s) | ||||||||||||||||||||||||||||||
| if not (gc_range[0] <= gc_content <= gc_range[1]): | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # 3. 3' 말단 안정성 및 GC Clamp | ||||||||||||||||||||||||||||||
| dg3 = sum(NN_PARAMS.get(s[-5:][j : j + 2], (0, 0))[0] for j in range(4)) | ||||||||||||||||||||||||||||||
| if dg3 <= -10.0: | ||||||||||||||||||||||||||||||
|
|
@@ -108,18 +109,63 @@ def generate_candidates( | |||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
| if s[-5:].count("G") + s[-5:].count("C") > 4: | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| candidates.append( | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| "seq": s, | ||||||||||||||||||||||||||||||
| "start": i, | ||||||||||||||||||||||||||||||
| "end": i + k - 1, | ||||||||||||||||||||||||||||||
| "start": i + 1, # [수정] 1-based 시작점 | ||||||||||||||||||||||||||||||
| "end": i + k, # [수정] 1-based 종료점 (inclusive) | ||||||||||||||||||||||||||||||
| "strand": strand, | ||||||||||||||||||||||||||||||
| "tm": tm, | ||||||||||||||||||||||||||||||
| "dg3": dg3, | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| return candidates | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # ========================================== | ||||||||||||||||||||||||||||||
| # 좌표 변환 및 매핑 유틸리티 추가 | ||||||||||||||||||||||||||||||
| # ========================================== | ||||||||||||||||||||||||||||||
| def locate_template_in_genome(self, template_seq: str) -> Optional[Dict]: | ||||||||||||||||||||||||||||||
| """Stage 1: 입력된 템플릿 서열의 1-based 게놈 좌표 탐색""" | ||||||||||||||||||||||||||||||
| for ref in self.genome.references: | ||||||||||||||||||||||||||||||
| full_seq = self.genome.fetch(ref) | ||||||||||||||||||||||||||||||
| pos = full_seq.find(template_seq) | ||||||||||||||||||||||||||||||
| if pos != -1: | ||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||
| "chrom": ref, | ||||||||||||||||||||||||||||||
| "genomic_start": pos + 1, # 1-based 변환 | ||||||||||||||||||||||||||||||
| "strand": "+", | ||||||||||||||||||||||||||||||
| "template_length": len(template_seq) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| rev_seq = reverse_complement(template_seq) | ||||||||||||||||||||||||||||||
| pos = full_seq.find(rev_seq) | ||||||||||||||||||||||||||||||
| if pos != -1: | ||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||
| "chrom": ref, | ||||||||||||||||||||||||||||||
| "genomic_start": pos + 1, # 1-based 변환 | ||||||||||||||||||||||||||||||
| "strand": "-", | ||||||||||||||||||||||||||||||
| "template_length": len(template_seq) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def map_to_genomic_coords(self, primer: Dict, template_info: Dict) -> Dict: | ||||||||||||||||||||||||||||||
| """Stage 1.5: 1-based 로컬 좌표를 1-based 게놈 절대 좌표로 변환""" | ||||||||||||||||||||||||||||||
| p_start = primer["start"] | ||||||||||||||||||||||||||||||
| p_end = primer["end"] | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if template_info["strand"] == "+": | ||||||||||||||||||||||||||||||
| primer["genomic_start"] = template_info["genomic_start"] + p_start - 1 | ||||||||||||||||||||||||||||||
| primer["genomic_end"] = template_info["genomic_start"] + p_end - 1 | ||||||||||||||||||||||||||||||
| primer["genomic_strand"] = primer["strand"] | ||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||
| t_len = template_info["template_length"] | ||||||||||||||||||||||||||||||
| primer["genomic_start"] = template_info["genomic_start"] + (t_len - p_end) | ||||||||||||||||||||||||||||||
| primer["genomic_end"] = template_info["genomic_start"] + (t_len - p_start) | ||||||||||||||||||||||||||||||
| primer["genomic_strand"] = "-" if primer["strand"] == "+" else "+" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| primer["chrom"] = template_info["chrom"] | ||||||||||||||||||||||||||||||
| return primer | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def local_db_filter( | ||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||
| chrom: str, | ||||||||||||||||||||||||||||||
|
|
@@ -129,31 +175,35 @@ def local_db_filter( | |||||||||||||||||||||||||||||
| intron_inclusion: bool = True, | ||||||||||||||||||||||||||||||
| intron_size_range: Optional[Tuple[int, int]] = None, | ||||||||||||||||||||||||||||||
| ) -> bool: | ||||||||||||||||||||||||||||||
| """Stage 2.2: 위치 및 구조 기반 필터링""" | ||||||||||||||||||||||||||||||
| """Stage 2.2: 위치 및 구조 기반 필터링 (게놈 절대 좌표 사용)""" | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
| """Stage 2.2: 위치 및 구조 기반 필터링 (게놈 절대 좌표 사용)""" | |
| """ | |
| Stage 2.2: 위치 및 구조 기반 필터링 (게놈 절대 좌표 사용) | |
| primer에는 보통 map_to_genomic_coords()를 통해 생성된 | |
| "genomic_start", "genomic_end", "genomic_strand" 필드가 포함되어야 한다. | |
| 만약 이 필드들이 없다면 게놈 기반 로컬 DB 필터링을 수행할 수 없으므로, | |
| 이 함수는 해당 primer에 대해 필터링을 건너뛰고 True를 반환한다. | |
| """ | |
| # 게놈 좌표 정보가 없으면 KeyError 대신 필터링을 건너뛰고 통과시킨다. | |
| required_keys = ("genomic_start", "genomic_end", "genomic_strand") | |
| if not all(k in primer for k in required_keys): | |
| # 기본 사용 흐름에서 map_to_genomic_coords()가 선행되지 않은 경우를 안전하게 처리 | |
| return True |
Copilot
AI
Mar 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
filter_specific_primers()에서 primer_pool = {p["seq"]: p ...}로 primer를 서열만으로 키잉하면 동일 서열이 다른 위치/스트랜드에서 생성된 경우가 덮어써져 후보가 유실됩니다(특히 짧은 k-mer에서는 중복이 흔합니다). 키를 (seq, genomic_start, genomic_end, genomic_strand) 같은 유니크 튜플로 바꾸거나, seq별로 리스트를 유지하도록 수정해 주세요.
Copilot
AI
Mar 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
filter_specific_primers()에서도 full_seq = self.genome.fetch(ref)로 매 염색체 전체를 한 번에 메모리에 올립니다. 그 위에 primer 수만큼 .find()를 반복 호출하는 구조라서, PR 설명의 "I/O 최적화"와 달리 실제로는 CPU/메모리 병목이 커질 수 있습니다. 최소한 reference 서열을 청크 단위로 처리하거나, primer마다 필요한 후보 위치를 인덱싱/검색할 수 있는 방식(예: k-mer 인덱스, Aho–Corasick, 외부 매퍼)으로 변경을 검토해 주세요.
Copilot
AI
Mar 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reverse-complement 매치를 검사할 때 off_target = full_seq[pos:pos+len(p_seq)]를 그대로 needleman_wunsch_mismatch(p_seq[-10:], off_target[-10:])에 넣으면, search_seq가 reverse_complement(p_seq)인 케이스에서 mm 값이 비정상적으로 커져 3' 말단 정밀 검사로 "치명적 off-target"을 제대로 탐지하지 못합니다. search_seq 방향을 고려해 off_target을 primer 서열과 동일한 방향으로 정규화(예: search_seq != p_seq이면 off_target을 reverse_complement)한 뒤 mismatch를 계산하도록 수정해 주세요.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # 프라이머 디자인 알고리즘 최적화 및 좌표 보정 | ||
|
|
||
| ## 1. 배경 및 목적 | ||
|
|
||
| - 사용자가 입력한 템플릿 서열(Local 좌표)과 게놈 DB(Genomic 절대 좌표) 간의 좌표 불일치 문제 해결 | ||
| - 1-based 기준인 생물학적 데이터를 0-based 파이썬 인덱스로 처리하며 발생하는 오차 수정 | ||
| - 전수 게놈 스캔 시 발생하는 심각한 I/O 병목 현상을 해결하여 발표 시연이 가능한 수준으로 성능 개선 | ||
|
|
||
| ## 2. 프라이머 (User Input) | ||
|
|
||
| ```text | ||
| (좌표계 문제 제기) | ||
| 이 코드를 보면 primer design 알고리즘을 설계한 것을 알 수 있어. 문제는 여기서 UI에서 입력값으로 fasta 파일이나 txt 파일을 넣을거야. 이때, template sequence가 해당 organism species의 genome을 읽어들여서 template sequence의 genome 상에서의 좌표를 파악해서 아래 코파일럿이 제기한 문제를 해결할려고 해. 기존 BLAST designer 등에서 사용하는 방법들을 사용해서 로직 코드를 보완해줘. | ||
| -제기된 문제: PrimerDesigner.py는 게놈 좌표/DB 좌표 기반으로 동작하는 전제인데, 현재 입력(템플릿 서열/검색 범위)이 템플릿 좌표로 들어오면 필터가 어긋납니다. | ||
|
|
||
| (성능 문제 제기) | ||
| 근데 genome 상 좌표를 그렇게 찾으면 시간이 너무 오래 걸릴 거 같은데? 로직 자체를 효율적으로 수정해줬으면 해. API한테 전달 받는 것도, 전달 하는 것도 변함은 없어야 해. | ||
|
|
||
| ``` | ||
|
|
||
| ## 3. AI 응답 요약 (AI Output) | ||
|
|
||
| * **좌표계 동기화**: `locate_template_in_genome` 메서드를 추가하여 템플릿의 게놈 내 위치를 찾고, 모든 로컬 좌표를 1-based 게놈 절대 좌표로 매핑하는 로직 구현. | ||
| * **I/O 최적화 (Batch Scan)**: 기존에 프라이머마다 염색체 파일을 반복 호출하던 방식에서, 염색체를 한 번 로드할 때 모든 프라이머 후보를 일괄 검사하는 루프 역전(Loop Inversion) 기법 적용. | ||
| * **정밀도 향상**: SNP, Exon 필터링 시 3' 말단(Inclusive range) 처리를 1-based 기준으로 정확히 계산하도록 수정. | ||
|
|
||
| ## 4. 결과 및 적용 (Result) | ||
|
|
||
| * **적용 사항**: `app/algorithms/PrimerDesigner.py` 내 `generate_candidates`, `local_db_filter`, `pair_primers` 등 핵심 로직 전면 수정. | ||
| * **성능 변화**: 수백 개의 후보군에 대한 특이성 검사 시간이 디스크 I/O 최적화를 통해 획기적으로 단축됨. | ||
| * **영향**: API 엔드포인트의 구조 변경 없이 알고리즘 내부 수정만으로 요구사항을 충족하여 프론트엔드/API 파트와의 정합성 유지. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
locate_template_in_genome()에서 각 reference마다self.genome.fetch(ref)로 염색체 전체 서열을 문자열로 로드한 뒤.find()를 수행하고 있어, 실제 게놈(예: hg38)에서는 메모리/시간 측면에서 운영상 문제가 될 가능성이 큽니다. 전체 서열 로드를 피하도록(예: 외부 aligner 사용, 인덱스 기반 탐색, 또는 구간 단위 스트리밍/청크 스캔 등) 구현을 조정하는 편이 안전합니다.