| |
| """Render HOMER *.motif PWMs to PNG logos (website display colors).""" |
| from __future__ import annotations |
|
|
| import argparse |
| import re |
| from pathlib import Path |
|
|
| import logomaker |
| import matplotlib.pyplot as plt |
| import numpy as np |
| import pandas as pd |
|
|
| |
| COLOR_SCHEME = { |
| "A": "#65a455", |
| "C": "#2e45a4", |
| "G": "#fda562", |
| "U": "#d54f3f", |
| } |
|
|
|
|
| def parse_homer_motif(path: Path) -> list[tuple[str, np.ndarray]]: |
| """Parse HOMER .motif file → [(label, Lx4 ACGU matrix), ...].""" |
| text = path.read_text(encoding="utf-8", errors="replace") |
| motifs: list[tuple[str, np.ndarray]] = [] |
| blocks = [b.strip() for b in text.split(">") if b.strip()] |
| for bi, block in enumerate(blocks): |
| lines = [ln.strip() for ln in block.splitlines() if ln.strip()] |
| if not lines: |
| continue |
| header = lines[0].split("\t") |
| label = header[1] if len(header) > 1 else f"{path.stem}_{bi + 1}" |
| rows: list[list[float]] = [] |
| for ln in lines[1:]: |
| parts = re.split(r"\s+", ln) |
| if len(parts) != 4: |
| continue |
| try: |
| rows.append([float(x) for x in parts]) |
| except ValueError: |
| continue |
| if rows: |
| motifs.append((label, np.asarray(rows, dtype=np.float64))) |
| return motifs |
|
|
|
|
| def plot_logo(matrix: np.ndarray, output_path: Path, dpi: int = 200) -> None: |
| df = pd.DataFrame(matrix, columns=["A", "C", "G", "U"]) |
| df = df.div(df.sum(axis=1).replace(0, np.nan), axis=0).fillna(0.25) |
| fig, ax = plt.subplots(figsize=(4, 2)) |
| logomaker.Logo( |
| df, |
| ax=ax, |
| color_scheme=COLOR_SCHEME, |
| baseline_width=0, |
| show_spines=False, |
| ) |
| ax.axis("off") |
| output_path.parent.mkdir(parents=True, exist_ok=True) |
| fig.savefig(output_path, dpi=dpi, bbox_inches="tight", format="png") |
| plt.close(fig) |
|
|
|
|
| def render_homer_dir(homer_dir: Path, logo_dir: Path, dpi: int = 200) -> list[Path]: |
| out: list[Path] = [] |
| logo_dir.mkdir(parents=True, exist_ok=True) |
| for motif_path in sorted(homer_dir.glob("*.motif")): |
| for i, (label, mat) in enumerate(parse_homer_motif(motif_path), start=1): |
| safe = re.sub(r"[^\w.\-]+", "_", label) |
| png = logo_dir / f"{motif_path.stem}_logo{i}_{safe}.png" |
| plot_logo(mat, png, dpi=dpi) |
| out.append(png) |
| print(f"saved {png.name}") |
| return out |
|
|
|
|
| def main() -> None: |
| ap = argparse.ArgumentParser(description="HOMER .motif → PNG logos") |
| ap.add_argument("homer_dir", type=Path, help="Directory of *.motif files") |
| ap.add_argument("logo_dir", type=Path, help="Output directory for PNGs") |
| ap.add_argument("--dpi", type=int, default=200) |
| args = ap.parse_args() |
| paths = render_homer_dir(args.homer_dir, args.logo_dir, dpi=args.dpi) |
| print(f"wrote {len(paths)} logos -> {args.logo_dir}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|