Contributing to MedTDA

Thank you for your interest in contributing to MedTDA! This document provides guidelines and instructions for contributing.

Ways to Contribute

There are many ways to contribute to MedTDA:

🐛 Bug Reports

Found a bug? Report it!

Feature Requests

Have an idea? Suggest it!

📝 Documentation

Improve docs, fix typos, add examples

💻 Code

Fix bugs, implement features, optimize performance

🧪 Testing

Write tests, test on different platforms

📊 Examples

Share notebooks, case studies, applications

💬 Community

Answer questions, help other users

All contributions are valued and appreciated!

Getting Started

Quick Start

  1. Fork the repository on GitHub

  2. Clone your fork locally

  3. Create a branch for your changes

  4. Make your changes

  5. Test your changes

  6. Commit with clear messages

  7. Push to your fork

  8. Submit a pull request

Detailed instructions below.

Prerequisites

Required:

  • Python 3.10 or later

  • Git

  • GitHub account

Recommended:

  • Familiarity with Git/GitHub workflow

  • Python development experience

  • Understanding of medical imaging (for some contributions)

Development Setup

1. Fork and Clone

Fork the repository:

Visit https://github.com/dashtiali/medtda and click “Fork”.

Clone your fork:

git clone https://github.com/YOUR_USERNAME/medtda.git
cd medtda

Add upstream remote:

git remote add upstream https://github.com/dashtiali/medtda.git

2. Create Environment

Using venv:

python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

Using conda:

conda create -n medtda-dev python=3.9
conda activate medtda-dev

3. Install in Development Mode

# Install package in editable mode with dev dependencies
pip install -e ".[dev]"

# Or install from requirements
pip install -r requirements-dev.txt
pip install -e .

Development dependencies include:

  • pytest - Testing framework

  • pytest-cov - Coverage reporting

  • black - Code formatting

  • flake8 - Linting

  • mypy - Type checking

  • sphinx - Documentation

  • pre-commit - Git hooks

4. Install Pre-commit Hooks

pre-commit install

This will automatically run checks before each commit.

Development Workflow

1. Create a Branch

Always create a new branch for your work:

git checkout -b feature/add-new-vectorizer
# or
git checkout -b fix/memory-leak-in-batch

Branch naming conventions:

  • feature/description - New features

  • fix/description - Bug fixes

  • docs/description - Documentation

  • refactor/description - Code refactoring

  • test/description - Adding tests

2. Make Your Changes

Follow coding standards:

  • Use Black for formatting (runs automatically with pre-commit)

  • Follow PEP 8 style guide

  • Write docstrings for all public functions/classes

  • Add type hints where appropriate

  • Keep functions focused and small

  • Write clear variable names

Example:

def compute_persistence_stats(barcodes: Dict[int, np.ndarray]) -> np.ndarray:
    """Compute summary statistics from persistence barcodes.

    Parameters
    ----------
    barcodes : Dict[int, np.ndarray]
        Dictionary mapping homology dimension to persistence barcodes.
        Each barcode is an array of shape (n_features, 2) with birth-death pairs.

    Returns
    -------
    np.ndarray
        Array of shape (13,) containing summary statistics.

    Examples
    --------
    >>> barcodes = {0: np.array([[0.0, 0.5], [0.1, 0.8]])}
    >>> stats = compute_persistence_stats(barcodes)
    >>> stats.shape
    (13,)
    """
    # Implementation
    pass

3. Write Tests

All new code should include tests!

Create test file:

# For new module medtda/newmodule.py
# Create tests/test_newmodule.py

Write tests:

import pytest
import numpy as np
from medtda.newmodule import new_function

def test_new_function_basic():
    """Test basic functionality."""
    result = new_function(input_data)
    assert result.shape == (10, 10)

def test_new_function_edge_case():
    """Test edge case handling."""
    with pytest.raises(ValueError):
        new_function(invalid_input)

def test_new_function_with_different_inputs():
    """Test with various inputs."""
    for input_val in [1, 5, 10]:
        result = new_function(input_val)
        assert result > 0

Run tests:

# Run all tests
pytest

# Run specific test file
pytest tests/test_newmodule.py

# Run with coverage
pytest --cov=medtda

# Run with verbose output
pytest -v

Test coverage:

Aim for >80% code coverage. Check coverage:

pytest --cov=medtda --cov-report=html
# Open htmlcov/index.html in browser

4. Update Documentation

Docstrings:

All public functions/classes need docstrings in NumPy style:

def my_function(param1: str, param2: int = 5) -> bool:
    """Short one-line description.

    Longer description if needed. Can span multiple lines.
    Explain what the function does, not how it does it.

    Parameters
    ----------
    param1 : str
        Description of param1.
    param2 : int, optional
        Description of param2 (default is 5).

    Returns
    -------
    bool
        Description of return value.

    Raises
    ------
    ValueError
        If param1 is empty string.

    Examples
    --------
    >>> my_function("test", 10)
    True

    Notes
    -----
    Additional information about the function.

    See Also
    --------
    related_function : Related functionality.
    """
    pass

User documentation:

If adding new features, update relevant documentation files:

  • User guide: docs/user_guide/

  • API reference: docs/api/

  • Examples: docs/examples/

Build documentation locally:

cd docs
make html
# Open docs/_build/html/index.html

5. Run Code Quality Checks

Format code:

black medtda tests

Lint:

flake8 medtda tests

Type check:

mypy medtda

All checks:

# Run all checks (like CI will)
black --check medtda tests
flake8 medtda tests
mypy medtda
pytest --cov=medtda

6. Commit Your Changes

Make focused commits:

git add medtda/newmodule.py tests/test_newmodule.py
git commit -m "Add new persistence vectorizer"

Commit message guidelines:

  • First line: Short summary (50 chars or less)

  • Body: Detailed explanation if needed

  • Reference issues: “Fixes #123” or “Relates to #456”

Good commit messages:

Add silhouette vectorization method

Implements silhouette representation for persistence diagrams.
Includes tests and documentation.

Fixes #45

Bad commit messages:

fixed stuff
WIP
asdf

7. Push and Create Pull Request

Push to your fork:

git push origin feature/add-new-vectorizer

Create pull request:

  1. Go to https://github.com/dashtiali/medtda

  2. Click “New Pull Request”

  3. Select your branch

  4. Fill in the template

  5. Submit!

Pull request template:

## Description
Brief description of changes.

## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Documentation
- [ ] Other (describe)

## Checklist
- [ ] Code follows style guidelines
- [ ] Tests added/updated
- [ ] Documentation updated
- [ ] All tests pass
- [ ] No merge conflicts

## Related Issues
Fixes #123

Code Guidelines

Style Guide

Follow PEP 8 with these specifics:

  • Line length: 88 characters (Black default)

  • Indentation: 4 spaces

  • Imports: Grouped and sorted

  • Quotes: Double quotes preferred

  • Naming:

    • snake_case for functions and variables

    • PascalCase for classes

    • UPPER_CASE for constants

Example:

"""Module docstring."""

import os
from pathlib import Path
from typing import Dict, List, Optional, Union

import numpy as np
from scipy import stats

from medtda.base import BaseVectorizer


MAX_FEATURES = 1000  # Constant


class NewVectorizer(BaseVectorizer):
    """Class docstring."""

    def __init__(self, resolution: int = 100):
        """Initialize vectorizer."""
        self.resolution = resolution

    def vectorize(self, barcodes: Dict[int, np.ndarray]) -> np.ndarray:
        """Vectorize barcodes."""
        # Implementation
        pass

Type Hints

Use type hints for all public functions:

from typing import Dict, List, Optional, Union
import numpy as np

def process_image(
    image: np.ndarray,
    mask: Optional[np.ndarray] = None,
    normalize: bool = True
) -> Dict[str, np.ndarray]:
    """Process image with optional mask."""
    pass

Error Handling

Provide informative error messages:

def load_image(path: str) -> np.ndarray:
    """Load image from file."""
    if not os.path.exists(path):
        raise FileNotFoundError(
            f"Image file not found: {path}"
        )

    if not path.endswith(('.nii', '.nii.gz')):
        raise ValueError(
            f"Unsupported file format: {path}. "
            "Supported formats: .nii, .nii.gz"
        )

    # Load image
    pass

Testing Guidelines

Test Structure

Organize tests to mirror code structure:

medtda/
    featureextractor.py
    preprocessor.py
tests/
    test_featureextractor.py
    test_preprocessor.py

Test file structure:

"""Tests for medtda.featureextractor module."""

import pytest
import numpy as np
from medtda import FeatureExtractor


@pytest.fixture
def sample_image():
    """Create sample 3D image for testing."""
    return np.random.rand(50, 50, 50)


class TestFeatureExtractor:
    """Tests for FeatureExtractor class."""

    def test_initialization(self):
        """Test extractor initialization."""
        extractor = FeatureExtractor()
        assert extractor is not None

    def test_execute_basic(self, sample_image):
        """Test basic feature extraction."""
        extractor = FeatureExtractor()
        features = extractor.execute(sample_image)
        assert len(features) > 0

    def test_execute_with_mask(self, sample_image):
        """Test extraction with mask."""
        mask = np.zeros_like(sample_image)
        mask[10:40, 10:40, 10:40] = 1

        extractor = FeatureExtractor(crop_to_roi=True)
        features = extractor.execute(sample_image, mask)
        assert features is not None

Test Types

Unit tests:

Test individual functions in isolation.

def test_normalize_minmax():
    """Test minmax normalization."""
    from medtda.utils import normalize

    data = np.array([1, 2, 3, 4, 5])
    normalized = normalize(data, method='minmax')

    assert normalized.min() == 0
    assert normalized.max() == 1

Integration tests:

Test components working together.

def test_full_pipeline():
    """Test complete feature extraction pipeline."""
    image = load_sample_image()
    mask = load_sample_mask()

    extractor = FeatureExtractor(
        normalize=True,
        crop_to_roi=True,
        vectorization_method='PersImage'
    )

    features = extractor.execute(image, mask)

    # features is a flat dict with keys like 'PersImage_H0_f0', ...
    pi_features = {k: v for k, v in features.items() if k.startswith('PersImage')}
    assert len(pi_features) > 0

Parametrized tests:

Test with multiple inputs.

@pytest.mark.parametrize("method,expected_count", [
    ('PersStats', 13),
    ('BettiCurve', 100),
    ('PersImage', 400),
])
def test_vectorization_methods(method, expected_count, sample_image):
    """Test different vectorization methods."""
    extractor = FeatureExtractor(vectorization_method=method)
    features = extractor.execute(sample_image)
    method_features = {k: v for k, v in features.items() if k.startswith(method)}
    assert len(method_features) == expected_count

Fixtures

Use fixtures for common test data:

@pytest.fixture
def sample_3d_image():
    """3D test image."""
    return np.random.rand(50, 50, 50)

@pytest.fixture
def sample_2d_image():
    """2D test image."""
    return np.random.rand(128, 128)

@pytest.fixture
def sample_mask():
    """Binary mask."""
    mask = np.zeros((50, 50, 50))
    mask[10:40, 10:40, 10:40] = 1
    return mask

Documentation Guidelines

Documentation Types

  1. Docstrings - In-code documentation (NumPy style)

  2. User Guide - Conceptual tutorials (RST files)

  3. API Reference - Auto-generated from docstrings

  4. Examples - Practical code examples

  5. Theory - Mathematical background

Writing Style

User documentation:

  • Write for your audience (beginners vs. experts)

  • Use active voice

  • Be concise but clear

  • Include examples

  • Link to related content

Good:

Use the FeatureExtractor class to extract topological features from your medical images. It handles preprocessing, persistent homology computation, and vectorization automatically.

Bad:

The FeatureExtractor is a class that can be utilized for the purpose of extracting features from images using topological data analysis methods.

Code Examples

Include runnable examples:

"""
Examples
--------
Basic usage:

>>> from medtda import FeatureExtractor
>>> extractor = FeatureExtractor()
>>> features = extractor.execute('image.nii.gz')

With custom parameters:

>>> extractor = FeatureExtractor(
...     normalize=True,
...     vectorization_method='PersImage'
... )
>>> features = extractor.execute('image.nii.gz')
"""

Pull Request Process

Checklist Before Submitting

Code:

  • [ ] Code follows style guide

  • [ ] All tests pass

  • [ ] New tests added for new code

  • [ ] No decrease in code coverage

  • [ ] Type hints added

Documentation:

  • [ ] Docstrings for all new functions/classes

  • [ ] User documentation updated (if needed)

  • [ ] Examples added (if adding features)

  • [ ] Changelog updated

Git:

  • [ ] Commits are focused and well-described

  • [ ] Branch is up to date with main

  • [ ] No merge conflicts

Review Process

  1. Automated checks run (tests, linting, coverage)

  2. Maintainers review code and documentation

  3. Feedback provided (if changes needed)

  4. Iteration - address feedback

  5. Approval - once requirements met

  6. Merge - maintainers merge to main

Tips for faster reviews:

  • Keep PRs focused and small

  • Write clear descriptions

  • Respond to feedback promptly

  • Be patient and respectful

Community Guidelines

Code of Conduct

All contributors must follow our Code of Conduct:

  • Be respectful and professional

  • Be inclusive and welcoming

  • Accept constructive criticism gracefully

  • Focus on what’s best for the community

  • Show empathy towards others

Reporting Issues

Bug reports:

Use the bug report template:

  • Clear title

  • Steps to reproduce

  • Expected vs. actual behavior

  • MedTDA version, Python version, OS

  • Minimal code example

  • Error messages/traceback

Feature requests:

Use the feature request template:

  • Clear description of feature

  • Use case / motivation

  • Proposed solution (if any)

  • Alternatives considered

Getting Help

Questions about contributing:

  • Read this guide thoroughly

  • Check existing issues and PRs

  • Ask in GitHub Discussions

  • Contact maintainers

Stuck on something:

  • Don’t hesitate to ask for help!

  • Comment on your PR

  • Open a discussion topic

  • We’re here to help you succeed!

Recognition

All contributors are recognized:

  • Listed in AUTHORS.md

  • Acknowledged in release notes

  • GitHub contributors page

  • Our gratitude and appreciation! 🎉

See Also