Thank you for your interest in contributing to AstroML! This document provides guidelines and instructions for contributing code, documentation, and research to the project.
- Code of Conduct
- Getting Started
- Research to Production Workflow
- Development Setup
- Code Standards
- Testing Requirements
- PR Process
- Documentation
- Questions & Support
AstroML is committed to providing a welcoming and inclusive environment. All contributors are expected to:
- Be respectful and constructive in all interactions
- Welcome feedback and criticism gracefully
- Focus on what is best for the community
- Show empathy towards other community members
# Fork the repository on GitHub, then:
git clone https://github.com/<your-username>/astroml.git
cd astroml
git remote add upstream https://github.com/Traqora/astroml.git# Sync with latest upstream
git fetch upstream
git checkout -b feature/your-feature-name upstream/main
# Or for bug fixes:
git checkout -b fix/bug-description upstream/mainSee Development Setup section below.
AstroML follows a clear data pipeline model that moves research from exploration to production. Understanding this workflow is essential for contributing effectively.
Ledger Data
↓
Ingestion & Normalization
↓
Graph Construction
↓
Feature Engineering
↓
Model Training & Evaluation
↓
Experimentation & Deployment
| Stage | Module | Purpose | Examples |
|---|---|---|---|
| Ingestion | astroml.ingestion |
Fetch ledgers from Stellar Horizon | backfill, enhanced_stream |
| Normalization | astroml.ingestion |
Validate & deduplicate data | Duplicate removal, type conversion |
| Graph Building | astroml.graph |
Construct transaction graphs | build_snapshot, windowing logic |
| Features | astroml.features |
Extract node/edge features | Asset diversity, temporal decay, node importance |
| Models | astroml.models |
GNN architectures & embeddings | GCN, GAT, GraphSAGE |
| Training | astroml.training |
Model training pipelines | Config-driven experiments, checkpoints |
When adding ingestion logic:
- Ensure idempotency (re-runs are safe)
- Handle database constraints gracefully
- Test with small ledger ranges first
- Document config requirements in
config/database.yaml
When building graph features:
- Test windowing logic thoroughly
- Ensure reproducibility (random seeds, checksums)
- Validate against edge cases (empty graphs, single nodes)
- Add unit tests before integration
When creating models:
- Use config files for hyperparameters (see
configs/) - Store checkpoints with metadata
- Log metrics consistently
- Provide examples in
examples/
- Python 3.10+
- PostgreSQL 12+ (for ingestion tests; SQLite for unit tests)
- Git
# 1. Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# 2. Install dependencies
pip install -r requirements.txt
# 3. (Optional) CPU-only PyTorch
pip install -r requirements-cpu.txt
# 4. Configure database
cp config/database.yaml.example config/database.yaml
# Edit config/database.yaml with your PostgreSQL credentials
# 5. Install package in editable mode
pip install -e .
# 6. Run tests to verify setup
pytest tests/ -v# Create a test database
createdb astroml_test
# Update config/database.yaml to point to test database
# Then run migrations:
alembic upgrade headAstroML follows PEP 8 with these conventions:
- Line length: 88 characters (Black formatter)
- Imports: Organize as (stdlib, third-party, local)
- Docstrings: Use Google-style docstrings for all public functions/classes
from datetime import datetime
from typing import Optional
import pandas as pd
from sqlalchemy import Column, String, Integer
from sqlalchemy.orm import declarative_base
from astroml.db.session import Base
def calculate_node_importance(
graph: 'nx.DiGraph',
measure: str = 'betweenness',
) -> dict:
"""Calculate node importance metrics for a transaction graph.
Args:
graph: NetworkX directed graph of transactions
measure: One of 'betweenness', 'degree', 'closeness'
Returns:
Dictionary mapping node IDs to importance scores
Raises:
ValueError: If measure is not recognized
"""
if measure not in ('betweenness', 'degree', 'closeness'):
raise ValueError(f"Unknown measure: {measure}")
# Implementation
return {}- Use type hints for all function parameters and return types
- Import from
typingmodule for complex types
from typing import List, Dict, Optional, Tuple
def process_accounts(
accounts: List[str],
filters: Optional[Dict[str, int]] = None,
) -> Tuple[int, List[str]]:
"""Process a list of account IDs."""
pass- Functions/variables:
snake_case - Classes:
PascalCase - Constants:
UPPER_SNAKE_CASE - Private members: Prefix with
_
class TransactionGraph:
DEFAULT_WINDOW_SIZE = 30 # days
def __init__(self):
self._cache = {}
def get_node_count(self) -> int:
"""Return number of nodes."""
pass- Write comments that explain why, not what
- Use docstrings for all public APIs
- Keep comments concise and up-to-date
# Good: explains reasoning
# Use cached result if available to avoid re-querying Stellar Horizon
if node_id in self._cache:
return self._cache[node_id]
# Avoid: obvious from code
# increment counter
count += 1# Run all tests
pytest tests/ -v
# Run specific test file
pytest tests/test_schema.py -v
# Run with coverage
pytest tests/ --cov=astroml --cov-report=html
# Run async tests (marked with @pytest.mark.asyncio)
pytest tests/test_stream.py -vTest file naming: test_<module_name>.py
import pytest
from astroml.features import calculate_asset_diversity
class TestAssetDiversity:
"""Tests for asset diversity feature calculation."""
def test_single_asset(self):
"""Single asset should have diversity = 1."""
result = calculate_asset_diversity(['USD'])
assert result == 1.0
def test_empty_assets(self):
"""Empty list should raise ValueError."""
with pytest.raises(ValueError):
calculate_asset_diversity([])
@pytest.mark.asyncio
async def test_async_feature_extraction(self):
"""Test async feature pipeline."""
result = await extract_features_async([...])
assert len(result) > 0
@pytest.fixture
def sample_graph():
"""Fixture providing sample transaction graph."""
import networkx as nx
G = nx.DiGraph()
G.add_edges_from([('A', 'B'), ('B', 'C')])
return GBefore submitting a PR:
- All tests pass:
pytest tests/ -v - New tests added for new functionality
- Edge cases covered (empty inputs, None values, etc.)
- Async functions tested with
@pytest.mark.asyncio - Integration tests verify database interactions
- No hardcoded test data paths (use fixtures)
| Stage | Test Type | Command |
|---|---|---|
| Ingestion | Unit + Integration | pytest tests/test_*stream*.py |
| Graph Building | Unit + Snapshot | pytest tests/test_snapshot.py |
| Features | Unit + Functional | pytest tests/test_*features*.py |
| Models | Unit + Training | pytest tests/test_*.py -k model |
## PR Checklist
### Tests
- [ ] `pytest tests/ -v` passes locally with no failures
- [ ] New functionality has unit tests covering the happy path and edge cases
- [ ] Any new async functions are tested with `@pytest.mark.asyncio`
- [ ] No hardcoded test data paths — fixtures and `test_data/` only
### Lint & Style
- [ ] `black --check astroml/ tests/` reports no formatting violations
- [ ] `flake8 astroml/ tests/` reports no errors (line length ≤ 88)
- [ ] All public functions/classes have Google-style docstrings
- [ ] Type hints are present on all new function signatures
### Changelog & Docs
- [ ] `CHANGELOG.md` entry added under `## [Unreleased]`
- [ ] `README.md` updated if new features, CLI flags, or config keys were added
- [ ] Example scripts in `examples/` updated or added where appropriateEvery pull request must pass all of the following before requesting review.
-
pytest tests/ -vpasses locally with no failures - New functionality has unit tests covering the happy path and edge cases
- Any new async functions are tested with
@pytest.mark.asyncio - Integration tests pass against a real database (not mocked) where applicable
- No hardcoded test data paths — fixtures and
test_data/only
-
black --check astroml/ tests/reports no formatting violations -
flake8 astroml/ tests/reports no errors (line length ≤ 88) -
mypy astroml/passes with no new type errors - All public functions/classes have Google-style docstrings
- Type hints are present on all new function signatures
-
CHANGELOG.mdentry added under## [Unreleased]describing the change -
README.mdupdated if new features, CLI flags, or config keys were added - Any new config fields are documented in the relevant YAML file
- Example scripts in
examples/updated or added where appropriate
- No secrets, credentials, or API keys in the diff
- No hardcoded file paths pointing to local machine directories
- Database migrations include a safe
downgradefunction - Random seeds are fixed for any reproducibility-sensitive tests
- Checksums/snapshots updated in
test_snapshots/if graph output changed - Hyperparameter changes are config-driven (not hardcoded)
-
CHANGELOG.mdnotes any model output or feature change that breaks reproducibility
-
Sync with upstream:
git fetch upstream git rebase upstream/main
-
Run linting & tests locally:
# Format check black --check astroml/ tests/ # Lint flake8 astroml/ tests/ # Type check mypy astroml/ # Full test suite pytest tests/ -v --cov=astroml
-
Ensure commits are clean:
- Meaningful commit messages (see Commit Convention)
- Logical, separated changes
- No secrets or credentials
<type>(<scope>): <subject>
<body>
<footer>
Types: feat, fix, docs, test, refactor, chore, perf
Scope: ingestion, graph, features, models, training, db
Examples:
feat(features): add temporal decay feature extractor
- Implements exponential decay based on transaction age
- Configured via decay_rate parameter
- Tested with synthetic graphs
Closes #123
fix(ingestion): handle duplicate transaction deduplication
Fixes idempotency issue when re-running backfill on same ledger range.
Fixes #456
When opening a PR, fill out:
## Description
Brief description of what this PR does.
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Related Issue
Closes #<issue_number>
## Testing
- [ ] Unit tests added/updated
- [ ] Integration tests pass
- [ ] Tested against sample data
## Checklist
- [ ] Code follows style guidelines
- [ ] Self-reviewed the code
- [ ] Updated documentation
- [ ] No new warnings generatedExpectations:
- Reviewers will provide feedback constructively
- Critical feedback focuses on the code, not the person
- Contributors should respond to all feedback (even if just acknowledging)
- Approval requires at least one maintainer sign-off
What reviewers check:
- ✅ Code correctness and logic
- ✅ Test coverage (especially for pipeline stages)
- ✅ Reproducibility (configs, seeds, checksums)
- ✅ Documentation completeness
- ✅ Alignment with "Research to Production" workflow
- ✅ Database integrity (for ingestion changes)
All public functions, classes, and modules must have docstrings:
"""Module for extracting temporal features from transaction graphs.
This module implements exponential decay and recency weighting
for node features based on transaction timestamps.
"""
def calculate_temporal_decay(
transactions: List[Transaction],
decay_rate: float = 0.1,
) -> pd.DataFrame:
"""Calculate temporal decay weights for accounts.
Uses exponential decay: weight = exp(-decay_rate * age_in_days)
Args:
transactions: List of Transaction objects (sorted by time)
decay_rate: Decay coefficient (higher = faster decay)
Returns:
DataFrame with columns: [account_id, decay_weight, timestamp]
Raises:
ValueError: If decay_rate is negative or transactions list is empty
Examples:
>>> df = calculate_temporal_decay(transactions, decay_rate=0.1)
>>> df.shape
(1000, 3)
"""When adding new features, update README.md:
- Add to feature list if it's major functionality
- Update architecture diagram if pipeline changes
- Link to new example scripts or documentation
For new features, add an example in examples/:
# examples/temporal_decay_example.py
"""Example: Extract temporal decay features."""
from astroml.features.temporal_decay import calculate_temporal_decay
from astroml.db.session import get_session
# Fetch transactions
session = get_session()
transactions = session.query(Transaction).all()
# Calculate temporal features
decay_df = calculate_temporal_decay(transactions, decay_rate=0.1)
print(f"Extracted temporal features for {len(decay_df)} accounts")
print(decay_df.head())Document YAML config fields in docstrings:
"""
Expected config (config/database.yaml):
database:
host: localhost
port: 5432
user: postgres
password: ${DB_PASSWORD} # From environment
database: astroml
"""- Bug reports: Open an issue on GitHub with reproducible example
- Feature requests: Use GitHub Discussions or open an issue with
[FEATURE]tag - Questions: Post in GitHub Discussions or tag with
[QUESTION] - Security issues: Email maintainers privately (do not open public issue)
- Check existing issues/discussions for similar questions
- Search the documentation in
docs/and README - Review example scripts in
examples/ - Run the discovery checklist from copilot-instructions.md
- README.md - Project overview and quick start
- docs/ - Full documentation
- examples/ - Example scripts for common tasks
- alembic/versions/ - Database migration history
- configs/ - Example configuration files
Your contributions make AstroML better for the entire research community. Whether you're fixing bugs, adding features, or improving documentation, every contribution matters.
Happy coding!