feat: modernize codebase for Python 3.9+ with pydantic validation#148
feat: modernize codebase for Python 3.9+ with pydantic validation#148
Conversation
tcoroller
commented
Mar 17, 2026
- Drop Python 3.8 support (EOL Oct 2024), require Python >=3.9
- Add Python 3.12 classifier
- Add pydantic>=2.0 as required dependency
- Bump torch requirement to >=2.8
- Replace validate_data.py with new pydantic-based validators.py
- Add tools/init.py module
- Refactor all metrics and loss modules to use new validators
- Add hypothesis-based property tests in tests/strategies.py
- Add shared test fixtures in tests/conftest.py
- Add tests/test_validators.py for new validation logic
- Update dev/environment.yml and run-unittests.sh
- Update ruff target-version to py39
- Add docs/package_overview.md
- Add .hypothesis/ to .gitignore
- Drop Python 3.8 support (EOL Oct 2024), require Python >=3.9 - Add Python 3.12 classifier - Add pydantic>=2.0 as required dependency - Bump torch requirement to >=2.8 - Replace validate_data.py with new pydantic-based validators.py - Add tools/__init__.py module - Refactor all metrics and loss modules to use new validators - Add hypothesis-based property tests in tests/strategies.py - Add shared test fixtures in tests/conftest.py - Add tests/test_validators.py for new validation logic - Update dev/environment.yml and run-unittests.sh - Update ruff target-version to py39 - Add docs/package_overview.md - Add .hypothesis/ to .gitignore Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
These packages are required by the modernized codebase but were missing from dev/environment.yml, causing ModuleNotFoundError in CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the optional 'checks' / 'check' boolean parameter from all loss functions, metric classes, and stats functions. Validation via pydantic validators is now always performed (guarded only by the existing torch.jit.is_scripting/is_tracing check for JIT compatibility). Affected: - loss/cox.py: neg_partial_log_likelihood, baseline_survival_function - loss/weibull.py: neg_log_likelihood_weibull - loss/survival.py: neg_log_likelihood, survival_function - metrics/auc.py: Auc.__init__ - metrics/brier_score.py: BrierScore.__init__ - metrics/cindex.py: ConcordanceIndex.__init__ - stats/ipcw.py: get_ipcw - stats/kaplan_meier.py: KaplanMeierEstimator.__call__ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The tests directory is not a package (no __init__.py), so pytest adds it to sys.path directly. Use 'from strategies import' consistent with how all other test files import from 'utils'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ions Replace O(K*N) Python loops with fully vectorized tensor operations for the standard (non-time-varying) Cox path: - H/R list comprehensions → scatter_add (events per time) + reverse cumsum (suffix sum of exp(log_hz) for risk sets), reducing cost from O(K*N) loop-based torch.where calls to O(N) tensor ops - Nested Efron for-loop (k, r) → padded (K, max_ties) tensor: compute all Efron denominator terms in parallel, mask padding with log(1)=0 - Breslow log_denominator stack → log(reverse_cumsum[first_idx]) - _cumulative_baseline_hazard: same reverse-cumsum trick The time-varying extended Cox path retains the existing list-comprehension approach (irregular 2-D indexing prevents general vectorization). Strata loops in the outer functions are kept as-is (structurally required; typically <=5 strata). Numerical equivalence verified against the reference implementation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1. test_brier_score_simulated_data (flaky): sksurv requires eval times strictly within [test_min, test_max) — filter new_time to exclude test_time.max() before passing to sksurv; skip IBS comparison when time grids differ to avoid integrating over mismatched intervals. 2. test_cox_equivalence (TorchScript): torch.jit.script rejects PEP 604 'X | Y' union syntax — replace 'torch.Tensor | None' with Optional[torch.Tensor] in all three function signatures; add 'from typing import Optional'. Suppress ruff UP045 since Optional is required for TorchScript compatibility. 3. test_training (MPS device): torch.searchsorted is not supported on Apple MPS — add _searchsorted() helper that runs on CPU and moves the result back to the original device; use it everywhere in cox.py. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Each public class and function now has runnable Examples sections covering: - happy-path usage with exact tensor output - auto-coercion behaviour (e.g. int event → bool, model_type normalisation) - error cases showing the ValidationError / ValueError message Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rict, ruff clean - validators.py: Pydantic v2 models for all public inputs; TimeVaryingCoxInputs, impute_missing_log_shape in ModelInputs, helpers, base class; log_params/log_hz allow inf (legitimate model outputs) - cox.py: Optional → X | None; TimeVaryingCoxInputs at call sites; fix baseline_survival return type; typed pll_tensor variable - momentum.py: module-level _SurvivalTuple; Callable[..., Any]; deque[Any]; type: ignore[misc] on torch decorator/subclass lines - metrics/auc.py, cindex.py, brier_score.py: __init__ return None, instance attrs typed as X | None, assert guards in private methods, full method signatures - stats/kaplan_meier.py: device: str | None, self.device: Any, return types, Iterator fix - __init__.py files: __all__ + re-exports for root, loss, metrics, stats, tools - conftest.py: collections.abc.Callable - tests: ruff-clean (B017 specific exceptions, E712 bool idioms, UP006/UP035, F401/F841) - pyproject.toml: follow_imports=skip for third-party; pandas-stubs; ruff test exclusion removed - dev/codeqc.sh: now runs ruff format + ruff check + mypy - dev/environment.yml: pandas-stubs via pip Quality gates: mypy strict 0 errors, ruff clean, 87 tests pass, 85% coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Migrate development tooling from conda to uv - Make pyproject.toml the single source of truth for all dependencies - Add missing test deps (lifelines, scikit-survival, lightning, loguru, torchvision, scikit-learn, pandas) to dev dependency group - Add missing doc deps (nbsphinx, sphinxcontrib-bibtex, sphinx-math-dollar, ipython, matplotlib, pandas, lightning) to docs optional extra - Add publish dependency group (build, twine) - Migrate all CI workflows to use astral-sh/setup-uv + uv sync - Update codeqc.yml, docs.yml, build-release.yml, torch-compatibility.yml - Update CLAUDE.md, CONTRIBUTING.md, README.md, docs/devnotes.md - Deprecate dev/environment.yml with notice pointing to uv - Regenerate uv.lock with all new dependency groups Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix codeqc CI: run dev script via uv run The shell script calls ruff/mypy which live in .venv and need uv run to be on PATH. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix mypy type-ignore comments and pre-commit hook - Update type: ignore from [misc] to [untyped-decorator] to match actual mypy error codes from newer mypy + torch versions - Update pre-commit mypy hook: use 'uv run --no-sync mypy' with explicit 'src/' target. --no-sync prevents uv from rebuilding the editable install mid-hook (which confuses pre-commit). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * uv issue * Add missing sphinx-book-theme to docs dependencies The docs conf.py uses sphinx_book_theme but it was only in the legacy environment.yml, not in pyproject.toml docs extras. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add pandoc to CI workflows that build docs pandoc is a system dependency required by nbsphinx to convert notebooks. Previously installed implicitly via conda; now needs explicit apt-get install. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PR Review Summary: Modernize Codebase + Migrate to uvThis PR has two major themes: (1) codebase modernization (Python 3.9+, Pydantic v2, typing, mypy strict) and (2) migration from conda to uv as the development toolchain. Here's a guided tour for reviewers. 🔴 High Attention — Review Carefully1. Pydantic v2 Validators (
|
| Decision | Rationale |
|---|---|
| Pydantic v2 as runtime dependency | Adds ~2MB to install; enables structured validation |
| Drop Python 3.8 support | 3.8 reached EOL Oct 2024 |
| uv as sole dev toolchain | Eliminates dep duplication between pyproject.toml and environment.yml |
| Keep environment.yml (deprecated) | Soft transition for existing conda users |
torch>=2.8 minimum |
Required for newer torch features used in vectorized cox |
| "\n", | ||
| "def make_loaders(X, event, time, test_size=0.2, val_size=0.1, batch_size=64):\n", | ||
| " \"\"\"Split into train/val/test and return DataLoaders.\"\"\"\n", | ||
| " X_tv, X_test, e_tv, e_test, t_tv, t_test = train_test_split(X, event, time, test_size=test_size, random_state=42)\n", |
There was a problem hiding this comment.
what does "tv" stand for?
| " auc_metric = Auc()\n", | ||
| " auc_val = auc_metric(estimate=log_hz, event=event_test, time=time_test, new_time=median_t)\n", | ||
| "\n", | ||
| " # Brier Score\n", |
There was a problem hiding this comment.
what not add the brier score?
| "\n", | ||
| " # C-index (use negative log_scale as risk score — higher scale = longer survival)\n", | ||
| " ci = ConcordanceIndex()\n", | ||
| " c = ci(-log_scale, event_test, time_test)\n", |
There was a problem hiding this comment.
i disagree with this. we should pass the parameters to get the survival function and use this as risk score. like in the main notebook. happy to do it just let me know if there was a particular other reason to use - log_scale
| "name": "stdout", | ||
| "output_type": "stream", | ||
| "text": [ | ||
| "Failure rate: 94.0% | Median observed hours: 118\n" |
There was a problem hiding this comment.
the failure rate seems very high and the median observed hours super low (less than 5 days of use). I propose to increase the baseline time, like i do in the commit
| "source": [ | ||
| "# ── Synthetic Extreme Weather Data ───────────────────────────────────────────\n", | ||
| "rng = np.random.default_rng(4)\n", | ||
| "N = 600\n", |
There was a problem hiding this comment.
why N = 600 and not 800 like all other examples?
| "Each section:\n", | ||
| "- Generates **synthetic data** with realistic distributions (fixed seeds for reproducibility)\n", | ||
| "- Points to a **real public dataset** you can use as a drop-in replacement\n", | ||
| "- Trains both a **Cox** or **Weibull AFT** model using a simple MLP backbone\n", |
There was a problem hiding this comment.
this is not true , you only train one of them
| @@ -11,87 +11,110 @@ pre-commit install | |||
|
|
|||
There was a problem hiding this comment.
i've tried the new steps with uv and it all works
| return log_params | ||
|
|
||
|
|
||
| def validate_time_varying_log_hz(time_sorted: torch.Tensor, log_hz_sorted: torch.Tensor) -> None: |
There was a problem hiding this comment.
I think we can delete this function. It has been replaced by TimeVaryingCoxInputs.
|
|
||
| # denominator_ties[k] = sum of exp(log_hz) for event subjects at time_unique[k] | ||
| denominator_ties = torch.zeros(K, device=device) | ||
| denominator_ties.scatter_add_(0, event_idx, exp_hz[event_sorted]) |
There was a problem hiding this comment.
I believe the code was lighter and clearer before. in the if else statements. Why the change?
|
|
||
| ### Contributors | ||
|
|
||
| * Yao Chen (**Novartis**) |
There was a problem hiding this comment.
should we make the changes with the acknowledgments as we discussed?