Skip to content

Commit 7d79504

Browse files
authored
Merge pull request #591 from NRCan/notebook_n_base
Add notebooks, tests, and environment setup with improved project metadata for refactor initial release
2 parents 2c7495f + e2ea30e commit 7d79504

File tree

13 files changed

+1374
-34
lines changed

13 files changed

+1374
-34
lines changed
Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
name: tests
2+
23
on: [push, pull_request]
4+
35
jobs:
46
run-tests:
57
runs-on: ubuntu-22.04
6-
defaults:
7-
run:
8-
shell: bash -el {0}
8+
99
steps:
10-
- name: checkout repository
11-
uses: actions/checkout@v4.1.2
10+
- uses: actions/checkout@v4
1211

13-
- name: create environment
14-
uses: conda-incubator/setup-miniconda@v3
12+
- uses: actions/setup-python@v5
1513
with:
16-
mamba-version: "*"
17-
activate-environment: geo_deep_env
18-
environment-file: environment.yml
14+
python-version: "3.10"
15+
cache: "pip"
1916

20-
- name: test with pytest
17+
- name: Install dependencies
2118
run: |
22-
pytest tests/
19+
python -m pip install --upgrade pip
20+
pip install -r requirements.txt
21+
22+
- name: Install pytest
23+
run: pip install pytest
24+
25+
- name: Run tests
26+
run: pytest tests/

.gitignore

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
*__pycache__**
22
*.idea**
33
*.vscode**
4-
*geo_deep_learning/notebooks*
4+
5+
# Specific folders name
6+
waterloo_subset_512/
7+
mlruns/
8+
.ipynb_checkpoints/
9+
10+
# Specific files
11+
environment_full_conda_bckp.yml

.pre-commit-config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,8 @@ repos:
1515
- id: check-yaml
1616
- id: check-json
1717
- id: check-added-large-files
18+
19+
exclude: |
20+
(?x)(
21+
^notebooks/.*\.ipynb$
22+
)

data/waterloo_subset_512.zip

18.3 MB
Binary file not shown.

geo_deep_learning/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""
2+
Geo Deep Learning (GDL).
3+
4+
A geospatial deep learning framework for segmentation and related tasks.
5+
Provides utilities for dataset preparation, model training, evaluation,
6+
and deployment with PyTorch Lightning.
7+
8+
Modules include:
9+
- datasets: data loading and preprocessing for geospatial sources
10+
- models: deep learning architectures for segmentation
11+
- datamodules: PyTorch Lightning DataModules for training pipelines
12+
- tasks_with_models: high-level training tasks coupled with models
13+
- tools: utilities for data handling and workflow management
14+
"""

geo_deep_learning/tasks_with_models/segmentation_unetplus.py

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
1313
from kornia.augmentation import AugmentationSequential
1414
from lightning.pytorch import LightningModule, Trainer
1515
from lightning.pytorch.cli import LRSchedulerCallable, OptimizerCallable
16-
from tools.utils import denormalization, load_weights_from_checkpoint
17-
from tools.visualization import visualize_prediction
1816
from torch import Tensor
17+
from torch.optim.lr_scheduler import _LRScheduler
1918
from torchmetrics.segmentation import MeanIoU
2019
from torchmetrics.wrappers import ClasswiseWrapper
2120

21+
from geo_deep_learning.tools.utils import denormalization, load_weights_from_checkpoint
22+
from geo_deep_learning.tools.visualization import visualize_prediction
23+
2224
# Ignore warning about default grid_sample and affine_grid behavior triggered by kornia
2325
warnings.filterwarnings(
2426
"ignore",
@@ -140,17 +142,27 @@ def configure_model(self) -> None:
140142
map_location=map_location,
141143
)
142144

143-
def configure_optimizers(self) -> list[list[dict[str, Any]]]:
144-
"""Configure optimizers."""
145+
def configure_optimizers(self) -> list:
146+
"""Configure optimizers and schedulers."""
145147
optimizer = self.optimizer(self.parameters())
146-
if (
147-
self.hparams["scheduler"]["class_path"]
148-
== "torch.optim.lr_scheduler.OneCycleLR"
149-
):
150-
max_lr = (
151-
self.hparams.get("scheduler", {}).get("init_args", {}).get("max_lr")
152-
)
148+
scheduler_cfg = self.hparams.get("scheduler", None)
149+
150+
# Initialize scheduler variable (either an LR scheduler or None)
151+
scheduler: _LRScheduler | None = None
152+
153+
# Handle non-CLI case
154+
if not scheduler_cfg or not isinstance(scheduler_cfg, dict):
155+
scheduler = self.scheduler(optimizer) if callable(self.scheduler) else None
156+
if scheduler:
157+
return [optimizer], [{"scheduler": scheduler, **self.scheduler_config}]
158+
return [optimizer]
159+
160+
# CLI-compatible config logic
161+
scheduler_class_path = scheduler_cfg.get("class_path", "")
162+
if scheduler_class_path == "torch.optim.lr_scheduler.OneCycleLR":
163+
max_lr = scheduler_cfg.get("init_args", {}).get("max_lr")
153164
stepping_batches = self.trainer.estimated_stepping_batches
165+
154166
if stepping_batches > -1:
155167
scheduler = torch.optim.lr_scheduler.OneCycleLR(
156168
optimizer,
@@ -165,31 +177,31 @@ def configure_optimizers(self) -> list[list[dict[str, Any]]]:
165177
epoch_size = self.trainer.datamodule.epoch_size
166178
accumulate_grad_batches = self.trainer.accumulate_grad_batches
167179
max_epochs = self.trainer.max_epochs
180+
168181
steps_per_epoch = math.ceil(
169182
epoch_size / (batch_size * accumulate_grad_batches),
170183
)
171184
buffer_steps = int(steps_per_epoch * accumulate_grad_batches)
185+
172186
scheduler = torch.optim.lr_scheduler.OneCycleLR(
173187
optimizer,
174188
max_lr=max_lr,
175189
steps_per_epoch=steps_per_epoch + buffer_steps,
176190
epochs=max_epochs,
177191
)
178192
else:
179-
stepping_batches = (
180-
self.hparams.get("scheduler", {})
181-
.get("init_args", {})
182-
.get("total_steps")
183-
)
193+
total_steps = scheduler_cfg.get("init_args", {}).get("total_steps")
184194
scheduler = torch.optim.lr_scheduler.OneCycleLR(
185195
optimizer,
186196
max_lr=max_lr,
187-
total_steps=stepping_batches,
197+
total_steps=total_steps,
188198
)
189199
else:
190200
scheduler = self.scheduler(optimizer)
191201

192-
return [optimizer], [{"scheduler": scheduler, **self.scheduler_config}]
202+
return [optimizer], [
203+
{"scheduler": scheduler, **self.scheduler_config},
204+
] if scheduler else [optimizer]
193205

194206
def forward(self, image: Tensor) -> Tensor:
195207
"""Forward pass."""
@@ -275,6 +287,7 @@ def test_step(
275287
y_hat = self(x)
276288
loss = self.loss(y_hat, y)
277289
y = y.squeeze(1).long()
290+
278291
if self.num_classes == 1:
279292
y_hat = (y_hat.sigmoid().squeeze(1) > self.threshold).long()
280293
else:

notebooks/00_quickstart.ipynb

Lines changed: 1067 additions & 0 deletions
Large diffs are not rendered by default.

notebooks/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Notebooks
2+
3+
This folder contains example notebooks to help you get started with **Geo Deep Learning (GDL)**.
4+
5+
## Available Notebooks
6+
7+
- **`00_quickstart.ipynb`**
8+
Minimal end-to-end demo:
9+
1. Prepare a small sample dataset
10+
2. Train a UNet++ model on CPU
11+
3. Run inference & visualize predictions
12+
13+
This version calls **GDL’s core classes directly** (no config files).
14+
It is meant as the simplest entry point to verify everything works.
15+
16+
- **`01_quickstart_config.ipynb`** *(coming soon)*
17+
Same workflow as above, but using **LightningCLI** and GDL’s config files.
18+
This is the recommended way for reproducible experiments.
19+
20+
## Requirements
21+
- GDL repository cloned locally
22+
- Environment with proper dependencies (see `requirements.txt` or `pyproject.toml`
23+
24+
## Troubleshooting
25+
`ModuleNotFoundError: No module named 'geo_deep_learning'` (or other module)
26+
27+
In general, this problem occurs when the paths are not properly defined. Make sure
28+
to add the repo to your PYTHONPATH.
29+
30+
Example inside a notebook:
31+
32+
```python
33+
import sys
34+
sys.path.append("..")
35+
```

notebooks/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Notebooks package for demo and examples."""

pyproject.toml

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,47 @@
1+
[build-system]
2+
requires = ["setuptools>=61"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "geo-deep-learning"
7+
version = "0.1.0a0"
8+
description = "Geospatial deep learning framework for segmentation tasks"
9+
readme = "README.md"
10+
authors = [
11+
{ name = "Victor Alhassan", email = "victor.alhassan@NRCan-RNCan.gc.ca" },
12+
{ name = "Luca Romanini", email = "luca.romanini@NRCan-RNCan.gc.ca" },
13+
]
14+
requires-python = ">=3.10"
15+
license = { file = "LICENSE" }
16+
classifiers = [
17+
"Development Status :: 3 - Alpha",
18+
"Intended Audience :: Science/Research",
19+
"License :: OSI Approved :: MIT License",
20+
"Programming Language :: Python :: 3.10",
21+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
22+
"Topic :: Scientific/Engineering :: GIS",
23+
]
24+
keywords = ["pytorch", "deep learning", "machine learning", "remote sensing", "satellite imagery", "earth observation", "geospatial"]
25+
26+
# Dependencies are pulled from requirements.txt
27+
dynamic = ["dependencies"]
28+
29+
[tool.setuptools.dynamic]
30+
dependencies = { file = ["requirements.txt"] }
31+
32+
[project.optional-dependencies]
33+
dev = ["pytest", "ruff", "pre-commit"]
34+
35+
[project.urls]
36+
Homepage = "https://github.com/NRCan/geo-deep-learning"
37+
Repository = "https://github.com/NRCan/geo-deep-learning"
38+
Issues = "https://github.com/NRCan/geo-deep-learning/issues"
39+
40+
41+
# --------------------------
42+
# Ruff configuration
43+
# --------------------------
44+
145
[tool.ruff]
246
exclude = [
347
".bzr", ".direnv", ".eggs", ".git", ".git-rewrite", ".hg",
@@ -6,6 +50,7 @@ exclude = [
650
".vscode", "__pypackages__", "_build", "buck-out", "build", "dist",
751
"node_modules", "site-packages", "venv"
852
]
53+
src = ["geo_deep_learning"]
954
line-length = 88
1055
indent-width = 4
1156
target-version = "py310"
@@ -18,11 +63,9 @@ ignore = [
1863
"ANN101", "ANN102", # allow skipping `self`, `cls` annotations
1964
"EXE002", # ignore missing executable bit on scripts with shebangs
2065
"ERA001", # ignore commented out code
66+
"TC002", # allow third-party imports in type annotations without TYPE_CHECKING
2167
]
2268

23-
# You can limit to specific rule groups instead of ALL:
24-
# select = ["E", "F", "W", "I", "N", "UP", "B", "C4", "SIM", "D", "PT"]
25-
2669
[tool.ruff.lint]
2770
fixable = ["ALL"]
2871
unfixable = []
@@ -34,3 +77,17 @@ quote-style = "double"
3477
indent-style = "space"
3578
skip-magic-trailing-comma = false
3679
line-ending = "auto"
80+
81+
[tool.ruff.lint.isort]
82+
# Treat both the package and legacy alias names as first-party
83+
known-first-party = [
84+
"geo_deep_learning",
85+
"tools",
86+
"models",
87+
"datasets",
88+
"datamodules",
89+
"tasks_with_models",
90+
]
91+
92+
[tool.ruff.lint.per-file-ignores]
93+
"tests/*" = ["S101"]

0 commit comments

Comments
 (0)