diff --git a/oop1/backend.py b/oop1/backend.py new file mode 100644 index 0000000..7aa19f7 --- /dev/null +++ b/oop1/backend.py @@ -0,0 +1,112 @@ +# backend.py +# Adapter Pattern for plotting backends + +from __future__ import annotations +from abc import ABC, abstractmethod +from typing import Any, Optional, Sequence +import matplotlib.pyplot as plt + + +class PlotBackend(ABC): + """ + UMAP ve diğer görseller için soyut backend arayüzü. + Adapter Pattern burada uygulanıyor. + Strategy katmanı backend hakkında bilgi sahibi değil. + """ + + @abstractmethod + def init_canvas(self, width: float, height: float, dpi: int) -> None: + ... + + @abstractmethod + def draw_umap( + self, + embedding, + labels: Optional[Sequence] = None, + point_size: float = 5.0, + alpha: float = 0.8, + cmap: str = "viridis", + color_labels: bool = True, + ) -> None: + ... + + @abstractmethod + def save(self, path: str) -> None: + ... + + @abstractmethod + def show(self) -> None: + ... + + @abstractmethod + def get_figure(self) -> Any: + ... + + @abstractmethod + def get_axes(self) -> Any: + ... + + +class MatplotlibBackend(PlotBackend): + """ + Matplotlib için adapter backend. + VisualizationService → Strategy → Backend hiyerarşisi + sayesinde her şey OOP uyumlu çalışır. + PlotlyBackend + BokehBackend katmanları buraya eklenebilir. + """ + + def __init__(self) -> None: + self._fig: Any = None + self._ax: Any = None + + def init_canvas(self, width: float, height: float, dpi: int) -> None: + self._fig, self._ax = plt.subplots(figsize=(width, height), dpi=dpi) + + def draw_umap( + self, + embedding, + labels: Optional[Sequence] = None, + point_size: float = 5.0, + alpha: float = 0.8, + cmap: str = "viridis", + color_labels: bool = True, + ) -> None: + if self._ax is None: + self.init_canvas(8, 6, 120) + + x = embedding[:, 0] + y = embedding[:, 1] + + if labels is None: + sc = self._ax.scatter(x, y, s=point_size, alpha=alpha, cmap=cmap) + else: + sc = self._ax.scatter( + x, + y, + c=labels if color_labels else None, + s=point_size, + alpha=alpha, + cmap=cmap if color_labels else None, + ) + + self._ax.set_xlabel("UMAP-1") + self._ax.set_ylabel("UMAP-2") + self._ax.set_title("UMAP Projection") + + if labels is not None and color_labels: + self._fig.colorbar(sc, ax=self._ax, label="labels") + + def save(self, path: str) -> None: + if self._fig is not None: + self._fig.savefig(path, bbox_inches="tight") + + def show(self) -> None: + if self._fig is not None: + plt.show() + + def get_figure(self) -> Any: + return self._fig + + def get_axes(self) -> Any: + return self._ax diff --git a/oop1/core.py b/oop1/core.py new file mode 100644 index 0000000..04c286b --- /dev/null +++ b/oop1/core.py @@ -0,0 +1,112 @@ +# core.py +# Veri akışı ve konfigürasyonun standartlaştırılması + +from __future__ import annotations + +from dataclasses import dataclass, field, asdict +from typing import Any, Optional, Sequence, Dict, List + + +@dataclass +class VizParams: + """ + UMAP + plot + kayıt + canvas ayarlarını tutan config sınıfı. + Strategy, backend, service hep bu obje üzerinden konuşur. + """ + + # ---------- UMAP ayarları ---------- + n_neighbors: int = 15 + min_dist: float = 0.1 + metric: str = "euclidean" + random_state: Optional[int] = 42 + n_components: int = 2 # genelde 2D UMAP + + # ---------- Canvas / figür boyutları ---------- + width: float = 8.0 # inches (matplotlib) + height: float = 6.0 + dpi: int = 120 + + # ---------- Nokta / scatter ayarları ---------- + point_size: float = 5.0 + alpha: float = 0.8 + + # ---------- Renk / etiket ayarları ---------- + cmap: str = "viridis" # Eğer labels sayısal ise + color_labels: bool = True # Kategorik/etiket bazlı renklendirme + label_name: Optional[str] = None # Örn: "cluster", "louvain" vs. + + # ---------- Kayıt / output ayarları ---------- + save_dir: str = "figures" + save_name: str = "umap" + save_format: str = "png" + save_enabled: bool = True # SaveStrategy kullanırken işine yarar + + # ---------- Eksen / başlık ---------- + title: Optional[str] = "UMAP Projection" + x_label: str = "UMAP-1" + y_label: str = "UMAP-2" + show_axis: bool = True + + def update(self, **kwargs: Any) -> None: + """ + **** + UMAP parametrelerini sonradan değiştirme imkanı. + Parametreleri runtime'da değiştirmek için: + ctx.params.update(n_neighbors=30, min_dist=0.05, title="Deneme UMAP") + Yanlış isim verilirse AttributeError fırlatır ki hatayı erken görelim. + """ + for key, value in kwargs.items(): + if not hasattr(self, key): + raise AttributeError(f"VizParams has no attribute '{key}'") + setattr(self, key, value) + + +@dataclass +class VizContext: + """ + Farklı strategy'ler arasında ortak kullanılacak context yapısı. + Data, parametreler, embedding, backend ve hataları burada tutar. + """ + # Girdi tarafı + data: Any + labels: Optional[Sequence] = None + params: VizParams = field(default_factory=VizParams) + + # Hesaplama sonrası doldurulacak + embedding: Optional[Any] = None + + # Backend tarafının dolduracağı alanlar + figure: Any = None + ax: Any = None + backend: Any = None # Örn: MatplotlibBackend instance + + # Hata ve ek artefakt yönetimi + errors: List[str] = field(default_factory=list) + artifacts: Dict[str, Any] = field(default_factory=dict) + + def add_error(self, msg: str) -> None: + """Context içinde hata mesajı biriktir.""" + self.errors.append(msg) + + @property + def has_errors(self) -> bool: + return len(self.errors) > 0 + + def attach_backend(self, backend: Any) -> None: + """Backend instance'ını context'e kaydet. + service backend'i context ile ilişkilendirir.**** + """ + self.backend = backend + + def to_dict(self) -> Dict[str, Any]: + """ + Debug / logging için context özetini sözlük formatında döner. + AnnData veya büyük objeleri komple dump etmiyoruz, sadece boyut / key bilgisi. + """ + return { + "params": asdict(self.params), + "has_errors": self.has_errors, + "errors": list(self.errors), + "embedding_shape": getattr(self.embedding, "shape", None), + "artifacts": list(self.artifacts.keys()), + } diff --git a/oop1/figures/umap_pbmc3k_leiden.png b/oop1/figures/umap_pbmc3k_leiden.png new file mode 100644 index 0000000..4625e86 Binary files /dev/null and b/oop1/figures/umap_pbmc3k_leiden.png differ diff --git a/oop1/pbmc3k_raw.h5ad b/oop1/pbmc3k_raw.h5ad new file mode 100644 index 0000000..460ee0b Binary files /dev/null and b/oop1/pbmc3k_raw.h5ad differ diff --git a/oop1/save_strategy.py b/oop1/save_strategy.py new file mode 100644 index 0000000..b4be9de --- /dev/null +++ b/oop1/save_strategy.py @@ -0,0 +1,71 @@ +# save_strategy.py +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Optional +import os + +from core import VizContext +from backend import PlotBackend + + +class SaveStrategy(ABC): + """ + Kaydetme davranışını soyutlayan Strategy. + Kaydetme politikası ayrı bir Strategy + İster hiç kaydetme, ister otomatik dosya adı üret, ister özel path kullan. + """ + + @abstractmethod + def save( + self, + ctx: VizContext, + backend: PlotBackend, + save_path: Optional[str] = None, + ) -> None: + ... + + +class NoSaveStrategy(SaveStrategy): + """Hiçbir şey kaydetmez, sadece figürü context'te bırakır.""" + + def save( + self, + ctx: VizContext, + backend: PlotBackend, + save_path: Optional[str] = None, + ) -> None: + # Sadece figür referansını artifacts içine atalım + ctx.artifacts["fig"] = backend.get_figure() + + +class AutoPathSaveStrategy(SaveStrategy): + """ + Kaydetme için otomatik path üretir: + - Eğer save_path verilmişse onu kullanır + - Verilmemişse ctx.params içindeki save_dir, save_name, save_format üzerinden üretir + """ + + def save( + self, + ctx: VizContext, + backend: PlotBackend, + save_path: Optional[str] = None, + ) -> None: + params = ctx.params + + # Kullanıcı kaydetmek istemiyorsa çık + if not params.save_enabled: + return + + # Dışarıdan özel path verilmişse onu kullan + if save_path is None: + os.makedirs(params.save_dir, exist_ok=True) + filename = f"{params.save_name}.{params.save_format}" + save_path = os.path.join(params.save_dir, filename) + + try: + backend.save(save_path) + ctx.artifacts["fig_path"] = save_path + except Exception as e: + ctx.add_error(f"Save error: {e}") diff --git a/oop1/service.py b/oop1/service.py new file mode 100644 index 0000000..f465319 --- /dev/null +++ b/oop1/service.py @@ -0,0 +1,131 @@ +# service.py +from __future__ import annotations + +from typing import Any, Optional, Sequence, Any as AnyType + +from core import VizContext, VizParams +from backend import MatplotlibBackend, PlotBackend +from umap_strategy import UMAPStrategy +from save_strategy import SaveStrategy, AutoPathSaveStrategy, NoSaveStrategy + + +class VisualizationService: + # FACADE PATTERN – kullanıcının gördüğü yüz + """ + Facade: + Kullanıcı sadece run_umap(...) çağırır, gerisi içeride halledilir. + - Modülerlik + - Genişletilebilirlik + - Ayarları değiştirme kolaylığı + """ + + def __init__( + self, + backend: Optional[PlotBackend] = None, + umap_strategy: Optional[UMAPStrategy] = None, + save_strategy: Optional[SaveStrategy] = None, + ) -> None: + # Varsayılan backend ve stratejiler + self.backend: PlotBackend = backend or MatplotlibBackend() + self.umap_strategy: UMAPStrategy = umap_strategy or UMAPStrategy() + # Varsayılan save strategy: parametrelere göre otomatik path + self.save_strategy: SaveStrategy = save_strategy or AutoPathSaveStrategy() + + def _build_context( + self, + data: Any, + labels: Optional[Sequence] = None, + params: Optional[VizParams] = None, + param_overrides: Optional[dict[str, AnyType]] = None, + ) -> VizContext: + """ + ****Context oluşturma ve parametre override işini ayrı fonksiyona aldık. + run_umap çağrısında ek parametre verdiğinde (n_neighbors=30 gibi), + onları tek tek VizParams’a elinle aktarmak zorunda değilsin → otomatik update. + """ + ctx = VizContext( + data=data, + labels=labels, + params=params or VizParams(), + ) + + # UMAP / plot parametrelerini runtime'da değiştirmek için + if param_overrides: + ctx.params.update(**param_overrides) + + return ctx + + def run_umap( + self, + data: Any, + labels: Optional[Sequence] = None, + params: Optional[VizParams] = None, + save_path: Optional[str] = None, + show: bool = True, + save_strategy: Optional[SaveStrategy] = None, + **param_overrides: AnyType, + ) -> VizContext: + """ + Uçtan uca UMAP akışı: + - Context oluştur + - Parametre override (n_neighbors, min_dist, title, save_name vs.) + - UMAPStrategy.prepare → embedding hesapla + - Backend.init_canvas → çizim alanı + - UMAPStrategy.render → scatter çizimi + - SaveStrategy.save → dosyaya kaydet / kaydetme / context'e sadece fig koy + - İstenirse show() + + Örnek kullanım: + service = VisualizationService() + ctx = service.run_umap( + data=X, + labels=clusters, + n_neighbors=30, + min_dist=0.05, + title="High-res UMAP", + save_name="umap_highres" + ) + """ + # 1) Context + parametre override + ctx = self._build_context( + data=data, + labels=labels, + params=params, + param_overrides=param_overrides or None, + ) + + # Backend'i context'e iliştir (debug vs. için) + ctx.attach_backend(self.backend) + + # 2) UMAP embedding hazırla + self.umap_strategy.prepare(ctx) + + # Hesaplama sırasında hata olduysa çizime geçme + if ctx.has_errors: + # Burada istersen loglama vs. de yapabilirsin + return ctx + + # 3) Canvas oluştur + p = ctx.params + self.backend.init_canvas(p.width, p.height, p.dpi) + + # 4) Çizim + self.umap_strategy.render(ctx, self.backend) + + # 5) Kaydetme (SaveStrategy ile) + effective_save_strategy = save_strategy or self.save_strategy + effective_save_strategy.save(ctx, self.backend, save_path=save_path) + + # 6) Gösterme + if show: + try: + self.backend.show() + except Exception as e: + ctx.add_error(f"Show error: {e}") + + # 7) Figure ve axes referanslarını context'e koy + ctx.figure = self.backend.get_figure() + ctx.ax = self.backend.get_axes() + ctx.artifacts.setdefault("backend", self.backend) + + return ctx diff --git a/oop1/test_oop_umap.py b/oop1/test_oop_umap.py new file mode 100644 index 0000000..99f78bc --- /dev/null +++ b/oop1/test_oop_umap.py @@ -0,0 +1,161 @@ +# test_oop_umap.py + +import scanpy as sc +import matplotlib.pyplot as plt + +from core import VizParams +from service import VisualizationService +from backend import MatplotlibBackend +from save_strategy import NoSaveStrategy + + +# ------------------------------------------------- +# 0) Opsiyonel: Özel backend (sağ panel için aks paylaşımı) +# ------------------------------------------------- +class AxSharingBackend(MatplotlibBackend): + """ + Bu backend, yeni bir figure/axes oluşturmak yerine + dışarıdan verilen bir axes üzerine çizer. + + Amaç: Solda Scanpy, sağda OOP UMAP olacak şekilde + aynı figürde 1x2 subplot içinde karşılaştırma yapmak. + """ + + def __init__(self, ax): + super().__init__() + self._ax = ax + self._fig = ax.figure + + def init_canvas(self, width: float, height: float, dpi: int) -> None: + """ + VisualizationService init_canvas çağırdığı için + bu metot override edilip 'hiçbir şey yapmıyor'. + Fig/Ax zaten dışarıdan geldi. + """ + # Canvas zaten var, hiçbir şey yapmıyoruz. + return + + +# ------------------------------------------------- +# 1) Veriyi oku +# ------------------------------------------------- +# Gerekirse bu path'i kendi dosya yoluna göre değiştir: +adata = sc.read_h5ad("/Users/birsen/Desktop/oop1/pbmc3k_raw.h5ad") + +print(adata) +print(adata.obs.head()) + + +# ------------------------------------------------- +# 2) Preprocessing (Scanpy klasik PBMC3k pipeline) +# ------------------------------------------------- + +# Hücre ve gen filtreleme +sc.pp.filter_cells(adata, min_genes=200) +sc.pp.filter_genes(adata, min_cells=3) + +# Normalize +sc.pp.normalize_total(adata, target_sum=1e4) + +# log1p +sc.pp.log1p(adata) + +# Highly variable genler +sc.pp.highly_variable_genes( + adata, + min_mean=0.0125, + max_mean=3, + min_disp=0.5, +) + +# Sadece HVG'ler +adata = adata[:, adata.var["highly_variable"]].copy() + +# Ölçekleme +sc.pp.scale(adata, max_value=10) + +# PCA +sc.tl.pca(adata, svd_solver="arpack") + +# Komşuluk grafiği +sc.pp.neighbors(adata, n_neighbors=15, n_pcs=40) + + +# ------------------------------------------------- +# 3) Scanpy ile UMAP + Leiden cluster +# ------------------------------------------------- +sc.tl.umap(adata) +sc.tl.leiden(adata, resolution=0.5) + +# Scanpy'nin hesapladığı UMAP embedding +scanpy_umap = adata.obsm["X_umap"] +leiden = adata.obs["leiden"] + + +# ------------------------------------------------- +# 4) Karşılaştırma figürü: 1x2 subplot +# ------------------------------------------------- +fig, (ax_left, ax_right) = plt.subplots( + 1, 2, + figsize=(12, 6), + dpi=120 +) + +# --- Sol panel: Scanpy UMAP --- +sc_left = ax_left.scatter( + scanpy_umap[:, 0], + scanpy_umap[:, 1], + c=leiden.astype("category").cat.codes, + s=5, + alpha=0.8, + cmap="viridis", +) +ax_left.set_title("Scanpy UMAP (sc.tl.umap)") +ax_left.set_xlabel("UMAP-1") +ax_left.set_ylabel("UMAP-2") +fig.colorbar(sc_left, ax=ax_left, label="leiden (Scanpy)") + + +# --- Sağ panel: Senin OOP UMAP + VisualizationService --- +# Backend olarak AxSharingBackend kullanıyoruz ki sağ panelin ax'ine çizsin +backend = AxSharingBackend(ax_right) + +# İstersen burada UMAP parametrelerini de oynayabilirsin: +params = VizParams( + n_neighbors=15, + min_dist=0.1, + metric="euclidean", + random_state=42, + point_size=5.0, + alpha=0.8, + cmap="viridis", + color_labels=True, + title="OOP UMAP (VisualizationService + UMAPStrategy)", + save_enabled=False, # bu testte dosya kaydetme +) + +# Varsayılan kayıt davranışı yerine hiçbir şey kaydetmeyen strategy +service = VisualizationService( + backend=backend, + save_strategy=NoSaveStrategy(), +) + +ctx = service.run_umap( + data=adata.obsm["X_pca"], # 👈 UMAP PCA uzayında + labels=leiden.cat.codes, + params=params, + show=False, # figürün tamamını en sonda plt.show ile açıyoruz +) + +# Sağ panel başlığı (params.title zaten backend'de de kullanılabilir) +ax_right.set_title(params.title or "OOP UMAP") + + +# ------------------------------------------------- +# 5) Sonuçları göster +# ------------------------------------------------- +plt.tight_layout() +plt.show() + +# Eğer istersen figürü dışarı kaydet: +# fig.savefig("compare_scanpy_vs_oop_umap.png", dpi=150, bbox_inches="tight") diff --git a/oop1/umap_strategy.py b/oop1/umap_strategy.py new file mode 100644 index 0000000..40d6db9 --- /dev/null +++ b/oop1/umap_strategy.py @@ -0,0 +1,116 @@ +# umap_strategy.py +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +import umap # pip install umap-learn + +from core import VizContext, VizParams +from backend import PlotBackend + + +""" +Katmanlar: +- transform → sadece sayısal UMAP hesaplama (embedding) +- strategy → embedding + backend ile çizim (UMAP'e özel) +core → parametre & context +backend → çizim layer (Matplotlib, Plotly vs.) +service → pipeline / facade +""" + + +@dataclass +class UMAPTransform: + """ + sadece matematik/algoritma*** + UMAP dönüşümünü yapan sınıf (SRP – sadece embedding). + - model ayarlama + - fit_transform() ile hesaplama + """ + + n_neighbors: int = 15 + min_dist: float = 0.1 + metric: str = "euclidean" + random_state: int | None = 42 + n_components: int = 2 + + def fit_transform(self, X: Any): + reducer = umap.UMAP( + n_neighbors=self.n_neighbors, + min_dist=self.min_dist, + metric=self.metric, + random_state=self.random_state, + n_components=self.n_components, + ) + embedding = reducer.fit_transform(X) + return embedding + + @classmethod + def from_params(cls, params: VizParams) -> "UMAPTransform": + """ + bu embedding ile ne yapıyoruz backend’le çizim, context yönetimi. + VizParams içinden UMAPTransform objesi üretmek için yardımcı constructor. + Parametreleri sonradan değiştirdiğinde (params.update(...)) buraya otomatik yansır. + """ + return cls( + n_neighbors=params.n_neighbors, + min_dist=params.min_dist, + metric=params.metric, + random_state=params.random_state, + n_components=params.n_components, + ) + + +class UMAPStrategy: + """ + Sadece UMAP çizimi yapan strateji. + İleride PacMAP / TriMAP / PHATE gibi başka transform'larla da değiştirilebilir. + """ + + def __init__(self, transform: UMAPTransform | None = None) -> None: + self.transform = transform + + def prepare(self, ctx: VizContext) -> None: + """ + UMAP embedding'i hesapla ve ctx.embedding içine koy. + Hata olursa context'e error ekler, embedding None kalır. + """ + # Transform objesi yoksa, her çağrıda güncel VizParams üzerinden oluştur. + if self.transform is None: + self.transform = UMAPTransform.from_params(ctx.params) + else: + # Dışarıdan inject edilen transform varsa ama params güncellendiyse, + # senkronize etmek istiyorsan burada da update edebilirsin (opsiyonel). + self.transform.n_neighbors = ctx.params.n_neighbors + self.transform.min_dist = ctx.params.min_dist + self.transform.metric = ctx.params.metric + self.transform.random_state = ctx.params.random_state + self.transform.n_components = ctx.params.n_components + + try: + embedding = self.transform.fit_transform(ctx.data) + ctx.embedding = embedding + ctx.artifacts["umap_embedding"] = embedding + except Exception as e: + ctx.add_error(f"UMAP computation error: {e}") + ctx.embedding = None + + def render(self, ctx: VizContext, backend: PlotBackend) -> None: + """ + + Backend-agnostic render: sadece backend.draw_umap çağırıyoruz. + """ + if ctx.embedding is None: + ctx.add_error("[UMAPStrategy] Embedding yok, çizim yapılmadı.") + return + + p = ctx.params + backend.draw_umap( + embedding=ctx.embedding, + labels=ctx.labels, + point_size=p.point_size, + alpha=p.alpha, + cmap=p.cmap, + color_labels=p.color_labels, + )