Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ dist/
.pytest_cache/
.mypy_cache/
# Jupyter Notebook checkpoints
.ipynb_checkpoints/
.ipynb_checkpoints/
# Test coverage artifacts
.coverage
htmlcov/
18 changes: 0 additions & 18 deletions setup.cfg

This file was deleted.

2 changes: 1 addition & 1 deletion src/simicpipeline/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

try:
from simicpipeline.core.aucprocessor import AUCProcessor
__all__.append("AUProcessor")
__all__.append("AUCProcessor")
except ImportError as e:
import warnings
warnings.warn(f"Could not import AUCprocessor: {e}")
Expand Down
1 change: 0 additions & 1 deletion src/simicpipeline/_jupyter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# src/simicpipeline/_jupyter.py
import os
import subprocess

def jupyter():
Expand Down
7 changes: 1 addition & 6 deletions src/simicpipeline/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@
Core functionality for SimiC Pipeline
"""

# This module is part of simicpipeline.core
# All imports are handled at the package level (simicpipeline.__init__.py)
# Individual modules can be imported as:
from simicpipeline.core.base import SimiCBase

__all__ = ["SimiCBase"]
# from simicpipeline.core.simicvisualization import SimiCVisualization
# from simicpipeline.core.simicpreprocess import MagicPipeline, ExperimentSetup
# from simicpipeline.core.aucprocessor import AUCprocessor

26 changes: 0 additions & 26 deletions src/simicpipeline/core/aucprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,6 @@
from simicpipeline.core import SimiCBase
from typing import Optional, Union

# class BaseProcessor(SimiCBase):
# """
# Base class for SimiC processors.
# Provides common functionality for file handling and data loading.
# """


# def __init__(self, p2df, p2res):
# """
# Initialize the base processor.

# Args:
# p2df (str): Path to dataframe pickle file
# p2res (str): Path to results pickle file
# """
# self.p2df = Path(p2df)
# self.p2res = Path(p2res)
# self.validate_files()

# def validate_files(self):
# """Validate that required files exist."""
# if not self.p2df.exists():
# raise FileNotFoundError(f"Data file not found: {self.p2df}")
# if not self.p2res.exists():
# raise FileNotFoundError(f"Results file not found: {self.p2res}")


class AUCProcessor(SimiCBase):
"""
Expand Down
25 changes: 0 additions & 25 deletions src/simicpipeline/core/simicpreprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,6 @@

from simicpipeline.core import SimiCBase

# class SimiCPreprocess(SimiCBase):
# """
# Base preprocessing pipeline for SimiC analysis.
# Provides common utilities for both MAGIC imputation and experiment setup.
# """

# def __init__(self,
# project_dir: Union[str, Path],
# ):
# """
# Initialize base preprocessing pipeline.

# Args:
# project_dir: Directory for project files
# """
# super().__init__(project_dir = project_dir)

# def _create_directory_structure(self) -> None:
# """Create standard SimiC directory structure."""

# self.input_files_dir = self.input_path
# self.output_simic_dir = self.output_path
# self.figures_dir = self.figures_path
# self.matrices_dir = self.matrices_path


class MagicPipeline(SimiCBase):
"""
Expand Down
3 changes: 3 additions & 0 deletions src/simicpipeline/data/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Data files bundled with SimiCPipeline."""

__all__: list[str] = []
6 changes: 2 additions & 4 deletions src/simicpipeline/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@
load_from_matrix_market,
load_from_anndata,
write_pickle,
install_package,
format_time,
print_tree
print_tree,
)

__all__ = [
"load_from_matrix_market",
"load_from_anndata",
"write_pickle",
"install_package",
"format_time",
"print_tree"
"print_tree",
]
35 changes: 14 additions & 21 deletions src/simicpipeline/utils/io.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
from __future__ import annotations
from typing import Optional, Tuple, Union
from typing import Optional, Union
from pathlib import Path
import pickle
import pandas as pd
import numpy as np
from scipy.sparse import coo_matrix
# Install packages

def install_package(package_name):
"""Install a package using pip3."""
import subprocess
import sys
subprocess.check_call([sys.executable, "-m", "pip", "install", package_name])

# Directories
def _install_package(package_name: str) -> None:
"""Install a package using pip (internal utility, not part of public API)."""
import subprocess
import sys
subprocess.check_call([sys.executable, "-m", "pip", "install", package_name])


def print_tree(directory: Union[str,Path] = Path('.'),
Expand All @@ -40,7 +36,7 @@ def print_tree(directory: Union[str,Path] = Path('.'),
│ └── utils/
│ └── io.py
"""
# Print the root directory name at the top
# Print the root directory name at the top
directory = Path(directory)
if current_depth == 0:
print(f"{directory.name}/")
Expand All @@ -62,7 +58,7 @@ def print_tree(directory: Union[str,Path] = Path('.'),
extension = " " if is_last else "│ "
print_tree(item, prefix + extension, max_depth, current_depth + 1)

# Loaders

def load_from_anndata(
path: Union[str, Path],
) -> object:
Expand All @@ -83,15 +79,15 @@ def load_from_anndata(

path = Path(path)
adata = ad.read_h5ad(str(path))
if adata.raw == None:
if adata.raw is None:
print("No raw attribute found in AnnData object.")

return adata

def load_from_matrix_market(
matrix_path: Union[str, Path],
genes_path: Union[str, Path] = None,
cells_path: Union[str, Path] = None,
genes_path: Optional[Union[str, Path]] = None,
cells_path: Optional[Union[str, Path]] = None,
transpose: bool = False,
cells_index_name: str = "Cell",
) -> object:
Expand Down Expand Up @@ -127,9 +123,9 @@ def load_from_matrix_market(
# Load names
genes = None
cells = None
if genes_path and genes_path.exists():
if genes_path is not None and Path(genes_path).exists():
genes = pd.read_csv(genes_path, header=None, sep="\t").iloc[:, 0].astype(str).tolist()
if cells_path and cells_path.exists():
if cells_path is not None and Path(cells_path).exists():
cells = pd.read_csv(cells_path, header=None, sep="\t").iloc[:, 0].astype(str).tolist()

# Convert to dense for simplicity; adjust if large matrices are expected
Expand All @@ -150,7 +146,7 @@ def load_from_matrix_market(

return df

# Writers

def write_pickle(obj, file_path):
file_path = Path(file_path)
file_path.parent.mkdir(parents=True, exist_ok=True)
Expand All @@ -164,7 +160,6 @@ def write_pickle(obj, file_path):
print(f"Pickle failed with error: {e}")


@staticmethod
def format_time(seconds: float) -> str:
"""
Format time duration in human-readable format.
Expand All @@ -185,5 +180,3 @@ def format_time(seconds: float) -> str:
return f"{minutes}min {secs}s"
else:
return f"{secs}s"

###########################################################################################
Empty file added tests/__init__.py
Empty file.
48 changes: 48 additions & 0 deletions tests/test_core_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Smoke tests for core base class."""

from pathlib import Path

import pytest

from simicpipeline.core.base import SimiCBase, _load_assignment_file


class TestSimiCBase:
"""Smoke tests for SimiCBase initialization and helpers."""

def test_creates_project_dir(self, tmp_path):
project_dir = tmp_path / "my_project"
assert not project_dir.exists()
base = SimiCBase(project_dir)
assert project_dir.exists()
assert base.project_dir == project_dir

def test_existing_project_dir(self, tmp_path):
base = SimiCBase(tmp_path)
assert base.project_dir == tmp_path

def test_format_time_delegates(self, tmp_path):
base = SimiCBase(tmp_path)
assert base.format_time(60) == "1min 0s"

def test_print_project_info(self, tmp_path, capsys):
base = SimiCBase(tmp_path)
base.print_project_info(max_depth=1)
captured = capsys.readouterr()
assert tmp_path.name in captured.out


class TestLoadAssignmentFile:
"""Tests for _load_assignment_file helper."""

def test_new_csv_format(self, tmp_path):
csv_file = tmp_path / "assignment.csv"
csv_file.write_text("category,label\ncell1,0\ncell2,1\ncell3,0\n")
labels = _load_assignment_file(csv_file)
assert list(labels) == [0, 1, 0]

def test_legacy_format(self, tmp_path):
tsv_file = tmp_path / "assignment.txt"
tsv_file.write_text("0\n1\n1\n0\n")
labels = _load_assignment_file(tsv_file)
assert list(labels) == [0, 1, 1, 0]
59 changes: 59 additions & 0 deletions tests/test_utils_io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Tests for simicpipeline.utils.io utilities."""

import pickle
from pathlib import Path

import pytest

from simicpipeline.utils.io import format_time, print_tree, write_pickle


class TestFormatTime:
"""Tests for the format_time utility function."""

def test_seconds_only(self):
assert format_time(45) == "45s"

def test_minutes_and_seconds(self):
assert format_time(90) == "1min 30s"

def test_hours_minutes_seconds(self):
assert format_time(3661) == "1h 1min 1s"

def test_zero(self):
assert format_time(0) == "0s"

def test_returns_string(self):
assert isinstance(format_time(100), str)


class TestWritePickle:
"""Tests for the write_pickle utility function."""

def test_writes_and_reloads(self, tmp_path):
obj = {"key": [1, 2, 3]}
out = tmp_path / "test.pkl"
write_pickle(obj, out)
assert out.exists()
with open(out, "rb") as f:
loaded = pickle.load(f)
assert loaded == obj

def test_creates_parent_directories(self, tmp_path):
obj = "hello"
out = tmp_path / "nested" / "dir" / "test.pkl"
write_pickle(obj, out)
assert out.exists()


class TestPrintTree:
"""Tests for the print_tree utility function."""

def test_runs_without_error(self, tmp_path, capsys):
(tmp_path / "a.txt").write_text("x")
sub = tmp_path / "sub"
sub.mkdir()
(sub / "b.txt").write_text("y")
print_tree(tmp_path, max_depth=2)
captured = capsys.readouterr()
assert tmp_path.name in captured.out