Skip to content
Open
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
140 changes: 140 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
name: CI Tests

on:
push:
branches: [master, main, "claude/**"]
pull_request:
branches: [master, main]

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Check Python syntax
run: |
python -m py_compile LatestPhasenetLocalTest/phasenet/detect_peaks.py
python -m py_compile LatestPhasenetLocalTest/phasenet/postprocess.py
python -m py_compile LatestPhasenetLocalTest/phasenet/util.py
python -m py_compile GaMMaTest/gamma/_base.py
python -m py_compile GaMMaTest/gamma/utils.py

unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11"]

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Cache pip packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-${{ matrix.python-version }}-

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Run unit tests
run: |
pytest tests/ -m unit -v --tb=short --junitxml=test-results-unit.xml

- name: Upload unit test results
if: always()
uses: actions/upload-artifact@v4
with:
name: unit-test-results-py${{ matrix.python-version }}
path: test-results-unit.xml

integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
needs: unit-tests

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Cache pip packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-3.10-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-3.10-

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Run integration tests
run: |
pytest tests/ -m integration -v --tb=short --junitxml=test-results-integration.xml

- name: Upload integration test results
if: always()
uses: actions/upload-artifact@v4
with:
name: integration-test-results
path: test-results-integration.xml

all-tests-with-coverage:
name: Full Test Suite with Coverage
runs-on: ubuntu-latest
needs: unit-tests

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Cache pip packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-3.10-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-3.10-

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Run all tests with coverage
run: |
pytest tests/ -v --tb=short --cov=LatestPhasenetLocalTest/phasenet --cov=GaMMaTest/gamma --cov-report=term-missing --cov-report=xml:coverage.xml

- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage.xml
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
__pycache__/
*.pyc
*.pyo
.pytest_cache/
*.egg-info/
dist/
build/
.eggs/
coverage.xml
test-results-*.xml
*.egg
9 changes: 9 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short
markers =
unit: Unit tests for individual functions
integration: Integration tests for multi-component workflows
14 changes: 14 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
numpy>=1.20
pandas>=1.3
scipy>=1.7
scikit-learn>=0.24
obspy>=1.3
matplotlib>=3.4
tqdm>=4.60
h5py>=3.0
pyproj>=3.0
contexttimer>=0.3

# Testing
pytest>=7.0
pytest-cov>=4.0
Empty file added tests/__init__.py
Empty file.
73 changes: 73 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Shared fixtures and path setup for all tests."""

import os
import sys

import numpy as np
import pytest

# Add source directories to sys.path so tests can import modules
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PHASENET_DIR = os.path.join(PROJECT_ROOT, "LatestPhasenetLocalTest", "phasenet")
GAMMA_DIR = os.path.join(PROJECT_ROOT, "GaMMaTest")

for path in [PHASENET_DIR, GAMMA_DIR]:
if path not in sys.path:
sys.path.insert(0, path)


@pytest.fixture
def project_root():
return PROJECT_ROOT


@pytest.fixture
def phasenet_dir():
return PHASENET_DIR


@pytest.fixture
def gamma_dir():
return GAMMA_DIR


@pytest.fixture
def sample_waveform_3c():
"""A synthetic 3-component seismic waveform (nt=3000, nsta=1, nch=3)."""
np.random.seed(42)
nt, nsta, nch = 3000, 1, 3
data = np.random.randn(nt, nsta, nch).astype(np.float32)
# Add a synthetic P-wave arrival at sample 500
for ch in range(nch):
data[500:520, 0, ch] += 5.0 * np.sin(np.linspace(0, 4 * np.pi, 20))
# Add a synthetic S-wave arrival at sample 1200
for ch in range(nch):
data[1200:1240, 0, ch] += 8.0 * np.sin(np.linspace(0, 6 * np.pi, 40))
return data


@pytest.fixture
def sample_predictions():
"""Synthetic model predictions with clear P and S peaks.

Shape: (Nb=1, Nt=3000, Ns=1, Nc=3) where channels are [noise, P, S].
"""
np.random.seed(42)
Nb, Nt, Ns, Nc = 1, 3000, 1, 3
preds = np.zeros((Nb, Nt, Ns, Nc), dtype=np.float32)
# Noise channel is generally high
preds[:, :, :, 0] = 0.9

# P-wave peak at index 500
p_signal = np.exp(-0.5 * ((np.arange(Nt) - 500) / 5) ** 2)
preds[0, :, 0, 1] = p_signal
preds[0, :, 0, 0] -= p_signal

# S-wave peak at index 1200
s_signal = np.exp(-0.5 * ((np.arange(Nt) - 1200) / 5) ** 2)
preds[0, :, 0, 2] = s_signal
preds[0, :, 0, 0] -= s_signal

# Clamp noise channel
preds[:, :, :, 0] = np.clip(preds[:, :, :, 0], 0, 1)
return preds
Loading