Spaces:
Running
Running
Siddhant Sharma commited on
Commit ·
4e9fa0a
1
Parent(s): 3dfc96f
Added multi satellite based fetching for training
Browse files- main.py +128 -35
- pyproject.toml +1 -0
- src/config/config.yaml +25 -6
- src/config/settings.py +35 -10
- src/data/data_manager.py +174 -0
- src/data/dataset.py +1 -1
- src/data/fetchers/__init__.py +1 -0
- src/data/fetchers/base_fetcher.py +48 -0
- src/data/fetchers/goes_fetcher.py +78 -0
- src/data/fetchers/himawari_fetcher.py +165 -0
- src/data/s3_manager.py +0 -150
- src/data/standardizer.py +37 -0
- src/data/transforms.py +4 -0
- src/training/trainer.py +17 -7
- tests/test_config.py +30 -27
- tests/test_data_manager.py +62 -0
- tests/test_dataset.py +26 -20
- tests/test_fetchers.py +86 -0
- tests/test_pipeline.py +101 -0
- tests/test_s3_manager.py +0 -46
- tests/test_standardizer.py +21 -0
- tests/test_trainer.py +64 -10
- uv.lock +0 -0
main.py
CHANGED
|
@@ -1,54 +1,147 @@
|
|
| 1 |
import os
|
| 2 |
import logging
|
| 3 |
import torch
|
|
|
|
| 4 |
from src.config.settings import load_settings
|
| 5 |
from src.utils import setup_logging
|
| 6 |
-
from src.data.
|
| 7 |
from src.model.ifnet import IFNet
|
| 8 |
from src.training.trainer import Trainer
|
| 9 |
|
|
|
|
| 10 |
def main():
|
| 11 |
logger = setup_logging()
|
| 12 |
settings = load_settings()
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
model = IFNet()
|
| 19 |
-
|
| 20 |
-
#
|
| 21 |
checkpoint_path = settings.training.load_model_path
|
| 22 |
-
|
| 23 |
if os.path.exists(checkpoint_path):
|
| 24 |
-
model.load_state_dict(
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
else:
|
| 27 |
-
logger.warning(
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
year = settings.data.year
|
| 32 |
-
start_day = settings.data.
|
| 33 |
-
end_day = settings.data.
|
| 34 |
-
prefix_type = settings.data.prefix_type
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
for chunk_idx, chunk in enumerate(chunks):
|
| 40 |
-
logger.info(
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
if __name__ == "__main__":
|
| 54 |
main()
|
|
|
|
| 1 |
import os
|
| 2 |
import logging
|
| 3 |
import torch
|
| 4 |
+
|
| 5 |
from src.config.settings import load_settings
|
| 6 |
from src.utils import setup_logging
|
| 7 |
+
from src.data.data_manager import DataManager
|
| 8 |
from src.model.ifnet import IFNet
|
| 9 |
from src.training.trainer import Trainer
|
| 10 |
|
| 11 |
+
|
| 12 |
def main():
|
| 13 |
logger = setup_logging()
|
| 14 |
settings = load_settings()
|
| 15 |
+
|
| 16 |
+
device = torch.device(
|
| 17 |
+
"cuda" if torch.cuda.is_available() else "cpu"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
logger.info(
|
| 21 |
+
"Starting Universal Satellite Interpolation Pipeline..."
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
# Universal Data Manager
|
| 25 |
+
data_manager = DataManager(settings)
|
| 26 |
+
|
| 27 |
+
# Model init
|
| 28 |
model = IFNet()
|
| 29 |
+
|
| 30 |
+
# Resume checkpoint if exists
|
| 31 |
checkpoint_path = settings.training.load_model_path
|
| 32 |
+
|
| 33 |
if os.path.exists(checkpoint_path):
|
| 34 |
+
model.load_state_dict(
|
| 35 |
+
torch.load(
|
| 36 |
+
checkpoint_path,
|
| 37 |
+
map_location=device
|
| 38 |
+
)
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
logger.info(
|
| 42 |
+
f"Loaded existing checkpoint: {checkpoint_path}"
|
| 43 |
+
)
|
| 44 |
else:
|
| 45 |
+
logger.warning(
|
| 46 |
+
f"No checkpoint found at {checkpoint_path}. "
|
| 47 |
+
f"Starting from scratch."
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
trainer = Trainer(
|
| 51 |
+
settings=settings,
|
| 52 |
+
model=model,
|
| 53 |
+
device=device
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
# Config-driven chunk generation
|
| 57 |
+
sat_type = settings.data.satellite_type.lower()
|
| 58 |
year = settings.data.year
|
| 59 |
+
start_day = settings.data.start_unit
|
| 60 |
+
end_day = settings.data.end_unit
|
| 61 |
+
prefix_type = settings.data.prefix_type
|
| 62 |
+
|
| 63 |
+
chunks = []
|
| 64 |
+
|
| 65 |
+
if sat_type == "goes":
|
| 66 |
+
# Example:
|
| 67 |
+
# ABI-L1b-RadC/2026/150/
|
| 68 |
+
chunks = [
|
| 69 |
+
f"{prefix_type}/{year}/{day:03d}/"
|
| 70 |
+
for day in range(
|
| 71 |
+
start_day,
|
| 72 |
+
end_day + 1
|
| 73 |
+
)
|
| 74 |
+
]
|
| 75 |
+
|
| 76 |
+
elif sat_type == "himawari":
|
| 77 |
+
# Example:
|
| 78 |
+
# AHI-L1b-FLDK/2026/06/18/
|
| 79 |
+
month = settings.data.month
|
| 80 |
+
|
| 81 |
+
chunks = [
|
| 82 |
+
f"{prefix_type}/{year}/{month:02d}/{day:02d}/"
|
| 83 |
+
for day in range(
|
| 84 |
+
start_day,
|
| 85 |
+
end_day + 1
|
| 86 |
+
)
|
| 87 |
+
]
|
| 88 |
+
|
| 89 |
+
else:
|
| 90 |
+
raise ValueError(
|
| 91 |
+
f"Unsupported satellite type: {sat_type}"
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
# Main chunk loop
|
| 95 |
for chunk_idx, chunk in enumerate(chunks):
|
| 96 |
+
logger.info(
|
| 97 |
+
f"=== Processing Chunk "
|
| 98 |
+
f"{chunk_idx + 1}/{len(chunks)}: {chunk} ==="
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
# Fetch -> Standardize -> Crop -> Save .pt triplets
|
| 102 |
+
data_manager.process_chunk(chunk)
|
| 103 |
+
|
| 104 |
+
# Train on generated triplets
|
| 105 |
+
for epoch in range(
|
| 106 |
+
1,
|
| 107 |
+
settings.training.epochs + 1
|
| 108 |
+
):
|
| 109 |
+
logger.info(
|
| 110 |
+
f"--- Chunk {chunk_idx + 1} | "
|
| 111 |
+
f"Epoch {epoch}/{settings.training.epochs} ---"
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
trainer.train_chunk(
|
| 115 |
+
settings.data.download_dir,
|
| 116 |
+
epoch
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
trainer.save_checkpoint(
|
| 120 |
+
"latest_model.pth"
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
# Purge .pt files after training chunk
|
| 124 |
+
logger.info(
|
| 125 |
+
"Purging processed .pt triplets..."
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
for f in os.listdir(
|
| 129 |
+
settings.data.download_dir
|
| 130 |
+
):
|
| 131 |
+
if f.endswith(".pt"):
|
| 132 |
+
os.remove(
|
| 133 |
+
os.path.join(
|
| 134 |
+
settings.data.download_dir,
|
| 135 |
+
f
|
| 136 |
+
)
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
trainer.shutdown()
|
| 140 |
+
|
| 141 |
+
logger.info(
|
| 142 |
+
"Universal multi-satellite training complete."
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
if __name__ == "__main__":
|
| 147 |
main()
|
pyproject.toml
CHANGED
|
@@ -10,6 +10,7 @@ dependencies = [
|
|
| 10 |
"numpy>=2.4.6",
|
| 11 |
"pytest>=9.1.0",
|
| 12 |
"pyyaml>=6.0.3",
|
|
|
|
| 13 |
"tensorboard>=2.20.0",
|
| 14 |
"torch>=2.12.0",
|
| 15 |
"torchvision>=0.27.0",
|
|
|
|
| 10 |
"numpy>=2.4.6",
|
| 11 |
"pytest>=9.1.0",
|
| 12 |
"pyyaml>=6.0.3",
|
| 13 |
+
"satpy>=0.60.0",
|
| 14 |
"tensorboard>=2.20.0",
|
| 15 |
"torch>=2.12.0",
|
| 16 |
"torchvision>=0.27.0",
|
src/config/config.yaml
CHANGED
|
@@ -2,14 +2,33 @@ training:
|
|
| 2 |
epochs: 100
|
| 3 |
batch_size: 4
|
| 4 |
learning_rate: 0.0001
|
|
|
|
|
|
|
| 5 |
checkpoints_dir: "/kaggle/working/checkpoints"
|
| 6 |
load_model_path: ""
|
|
|
|
| 7 |
data:
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
year: 2024
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
| 14 |
frame_step: 3
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
epochs: 100
|
| 3 |
batch_size: 4
|
| 4 |
learning_rate: 0.0001
|
| 5 |
+
weight_decay: 0.001
|
| 6 |
+
num_workers: 4
|
| 7 |
checkpoints_dir: "/kaggle/working/checkpoints"
|
| 8 |
load_model_path: ""
|
| 9 |
+
|
| 10 |
data:
|
| 11 |
+
satellite_type: "himawari"
|
| 12 |
+
s3_bucket: "noaa-himawari9"
|
| 13 |
+
download_dir: "/kaggle/working/data"
|
| 14 |
+
|
| 15 |
+
# Satellite path prefix
|
| 16 |
+
prefix_type: "AHI-L1b-FLDK"
|
| 17 |
+
|
| 18 |
+
# Universal time config
|
| 19 |
year: 2024
|
| 20 |
+
month: 9
|
| 21 |
+
start_unit: 21
|
| 22 |
+
end_unit: 30
|
| 23 |
+
|
| 24 |
+
# Temporal triplet spacing
|
| 25 |
frame_step: 3
|
| 26 |
+
|
| 27 |
+
# Crop logic
|
| 28 |
+
crop_size: 512
|
| 29 |
+
crop_stride_divisor: 4
|
| 30 |
+
static_motion_threshold: 0.005
|
| 31 |
+
|
| 32 |
+
# Brightness temperature normalization
|
| 33 |
+
min_bt: 180.0
|
| 34 |
+
max_bt: 330.0
|
src/config/settings.py
CHANGED
|
@@ -1,35 +1,60 @@
|
|
| 1 |
from dataclasses import dataclass
|
|
|
|
| 2 |
import yaml
|
| 3 |
|
|
|
|
| 4 |
@dataclass
|
| 5 |
class TrainingConfig:
|
| 6 |
epochs: int
|
| 7 |
batch_size: int
|
| 8 |
learning_rate: float
|
|
|
|
|
|
|
| 9 |
checkpoints_dir: str
|
| 10 |
load_model_path: str = ""
|
| 11 |
|
|
|
|
| 12 |
@dataclass
|
| 13 |
class DataConfig:
|
|
|
|
| 14 |
s3_bucket: str
|
| 15 |
download_dir: str
|
| 16 |
-
prefix_type: str
|
|
|
|
| 17 |
year: int
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
@dataclass
|
| 24 |
class Settings:
|
| 25 |
training: TrainingConfig
|
| 26 |
data: DataConfig
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
raw_config = yaml.safe_load(file)
|
| 31 |
-
|
| 32 |
return Settings(
|
| 33 |
-
training=TrainingConfig(
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
)
|
|
|
|
| 1 |
from dataclasses import dataclass
|
| 2 |
+
from typing import Optional
|
| 3 |
import yaml
|
| 4 |
|
| 5 |
+
|
| 6 |
@dataclass
|
| 7 |
class TrainingConfig:
|
| 8 |
epochs: int
|
| 9 |
batch_size: int
|
| 10 |
learning_rate: float
|
| 11 |
+
weight_decay: float
|
| 12 |
+
num_workers: int
|
| 13 |
checkpoints_dir: str
|
| 14 |
load_model_path: str = ""
|
| 15 |
|
| 16 |
+
|
| 17 |
@dataclass
|
| 18 |
class DataConfig:
|
| 19 |
+
satellite_type: str
|
| 20 |
s3_bucket: str
|
| 21 |
download_dir: str
|
| 22 |
+
prefix_type: str
|
| 23 |
+
|
| 24 |
year: int
|
| 25 |
+
month: Optional[int] = None
|
| 26 |
+
|
| 27 |
+
start_unit: int = 1
|
| 28 |
+
end_unit: int = 1
|
| 29 |
+
|
| 30 |
+
frame_step: int = 1
|
| 31 |
+
|
| 32 |
+
crop_size: int = 256
|
| 33 |
+
crop_stride_divisor: int = 4
|
| 34 |
+
static_motion_threshold: float = 0.005
|
| 35 |
+
|
| 36 |
+
min_bt: float = 180.0
|
| 37 |
+
max_bt: float = 330.0
|
| 38 |
+
|
| 39 |
|
| 40 |
@dataclass
|
| 41 |
class Settings:
|
| 42 |
training: TrainingConfig
|
| 43 |
data: DataConfig
|
| 44 |
|
| 45 |
+
|
| 46 |
+
def load_settings(
|
| 47 |
+
config_path: str = "src/config/config.yaml"
|
| 48 |
+
) -> Settings:
|
| 49 |
+
|
| 50 |
+
with open(config_path, "r") as file:
|
| 51 |
raw_config = yaml.safe_load(file)
|
| 52 |
+
|
| 53 |
return Settings(
|
| 54 |
+
training=TrainingConfig(
|
| 55 |
+
**raw_config["training"]
|
| 56 |
+
),
|
| 57 |
+
data=DataConfig(
|
| 58 |
+
**raw_config["data"]
|
| 59 |
+
)
|
| 60 |
)
|
src/data/data_manager.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import glob
|
| 3 |
+
import shutil
|
| 4 |
+
import logging
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
|
| 8 |
+
from src.config.settings import Settings
|
| 9 |
+
from src.data.fetchers.goes_fetcher import GOESFetcher
|
| 10 |
+
from src.data.fetchers.himawari_fetcher import HimawariFetcher
|
| 11 |
+
from src.data.standardizer import UniversalStandardizer
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class DataManager:
|
| 17 |
+
"""
|
| 18 |
+
Universal multi-satellite data pipeline manager.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
def __init__(self, settings: Settings):
|
| 22 |
+
self.settings = settings
|
| 23 |
+
self.pt_dir = settings.data.download_dir
|
| 24 |
+
self.raw_dir = os.path.join(self.pt_dir, "raw_data")
|
| 25 |
+
|
| 26 |
+
os.makedirs(self.pt_dir, exist_ok=True)
|
| 27 |
+
os.makedirs(self.raw_dir, exist_ok=True)
|
| 28 |
+
|
| 29 |
+
sat_type = getattr(settings.data, "satellite_type", "goes").lower()
|
| 30 |
+
|
| 31 |
+
if sat_type == "goes":
|
| 32 |
+
self.fetcher = GOESFetcher(
|
| 33 |
+
bucket_name=settings.data.s3_bucket
|
| 34 |
+
)
|
| 35 |
+
elif sat_type == "himawari":
|
| 36 |
+
self.fetcher = HimawariFetcher(
|
| 37 |
+
bucket_name=settings.data.s3_bucket
|
| 38 |
+
)
|
| 39 |
+
else:
|
| 40 |
+
raise ValueError(f"Unsupported satellite type: {sat_type}")
|
| 41 |
+
|
| 42 |
+
def process_chunk(self, chunk_prefix: str) -> None:
|
| 43 |
+
logger.info(
|
| 44 |
+
f"Processing chunk {chunk_prefix} "
|
| 45 |
+
f"using {self.fetcher.__class__.__name__}"
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
raw_files = self.fetcher.fetch_chunk(chunk_prefix, self.raw_dir)
|
| 49 |
+
|
| 50 |
+
if len(raw_files) < 3:
|
| 51 |
+
logger.warning("Not enough frames for triplets.")
|
| 52 |
+
return
|
| 53 |
+
|
| 54 |
+
frame_step = self.settings.data.frame_step
|
| 55 |
+
|
| 56 |
+
for i in range(len(raw_files) - 2 * frame_step):
|
| 57 |
+
try:
|
| 58 |
+
t0_path = raw_files[i]
|
| 59 |
+
t1_path = raw_files[i + frame_step]
|
| 60 |
+
t2_path = raw_files[i + 2 * frame_step]
|
| 61 |
+
|
| 62 |
+
img0_raw = self.fetcher.apply_planck_function(t0_path)
|
| 63 |
+
gt_raw = self.fetcher.apply_planck_function(t1_path)
|
| 64 |
+
img1_raw = self.fetcher.apply_planck_function(t2_path)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
img0 = UniversalStandardizer.normalize_bt(
|
| 69 |
+
img0_raw,
|
| 70 |
+
self.settings.data.min_bt,
|
| 71 |
+
self.settings.data.max_bt
|
| 72 |
+
)
|
| 73 |
+
gt = UniversalStandardizer.normalize_bt(
|
| 74 |
+
gt_raw,
|
| 75 |
+
self.settings.data.min_bt,
|
| 76 |
+
self.settings.data.max_bt
|
| 77 |
+
)
|
| 78 |
+
img1 = UniversalStandardizer.normalize_bt(
|
| 79 |
+
img1_raw,
|
| 80 |
+
self.settings.data.min_bt,
|
| 81 |
+
self.settings.data.max_bt
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
img0_crop, img1_crop, gt_crop = (
|
| 85 |
+
self._motion_guided_argmax_crop(
|
| 86 |
+
img0, img1, gt
|
| 87 |
+
)
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
safe_prefix = chunk_prefix.replace("/", "_")
|
| 91 |
+
pt_filename = os.path.join(
|
| 92 |
+
self.pt_dir,
|
| 93 |
+
f"triplet_{safe_prefix}_{i:03d}.pt"
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
triplet_tensor = torch.stack(
|
| 97 |
+
[img0_crop, gt_crop, img1_crop],
|
| 98 |
+
dim=0
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
torch.save(triplet_tensor, pt_filename)
|
| 102 |
+
|
| 103 |
+
except Exception as e:
|
| 104 |
+
logger.error(
|
| 105 |
+
f"Triplet processing failed ({i}): {e}"
|
| 106 |
+
)
|
| 107 |
+
continue
|
| 108 |
+
|
| 109 |
+
self.purge_raw_files()
|
| 110 |
+
|
| 111 |
+
def _motion_guided_argmax_crop(
|
| 112 |
+
self,
|
| 113 |
+
img0: torch.Tensor,
|
| 114 |
+
img1: torch.Tensor,
|
| 115 |
+
gt: torch.Tensor
|
| 116 |
+
):
|
| 117 |
+
crop_size = self.settings.data.crop_size
|
| 118 |
+
stride = crop_size // self.settings.data.crop_stride_divisor
|
| 119 |
+
|
| 120 |
+
_, h, w = img0.shape
|
| 121 |
+
|
| 122 |
+
if h < crop_size or w < crop_size:
|
| 123 |
+
raise ValueError(
|
| 124 |
+
f"Image smaller than crop size: {h}x{w}"
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
motion_map = torch.abs(img1 - img0)
|
| 128 |
+
|
| 129 |
+
# Mask out outer-space / invalid regions
|
| 130 |
+
space_mask = (img0 > 0.0).float()
|
| 131 |
+
motion_map = motion_map * space_mask
|
| 132 |
+
|
| 133 |
+
pooled_motion = F.avg_pool2d(
|
| 134 |
+
motion_map.unsqueeze(0),
|
| 135 |
+
kernel_size=crop_size,
|
| 136 |
+
stride=stride
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
_, _, h_out, w_out = pooled_motion.shape
|
| 140 |
+
|
| 141 |
+
flat_idx = torch.argmax(pooled_motion).item()
|
| 142 |
+
|
| 143 |
+
y_out = flat_idx // w_out
|
| 144 |
+
x_out = flat_idx % w_out
|
| 145 |
+
|
| 146 |
+
y = y_out * stride
|
| 147 |
+
x = x_out * stride
|
| 148 |
+
|
| 149 |
+
y = max(0, min(y, h - crop_size))
|
| 150 |
+
x = max(0, min(x, w - crop_size))
|
| 151 |
+
|
| 152 |
+
img0_crop = img0[:, y:y+crop_size, x:x+crop_size]
|
| 153 |
+
img1_crop = img1[:, y:y+crop_size, x:x+crop_size]
|
| 154 |
+
gt_crop = gt[:, y:y+crop_size, x:x+crop_size]
|
| 155 |
+
|
| 156 |
+
crop_motion = torch.abs(
|
| 157 |
+
img1_crop - img0_crop
|
| 158 |
+
).mean().item()
|
| 159 |
+
|
| 160 |
+
if crop_motion < self.settings.data.static_motion_threshold:
|
| 161 |
+
raise ValueError(
|
| 162 |
+
f"Static crop rejected: {crop_motion:.5f}"
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
return img0_crop, img1_crop, gt_crop
|
| 166 |
+
|
| 167 |
+
def purge_raw_files(self):
|
| 168 |
+
logger.info("Purging raw files...")
|
| 169 |
+
|
| 170 |
+
for f in glob.glob(os.path.join(self.raw_dir, "*")):
|
| 171 |
+
if os.path.isfile(f):
|
| 172 |
+
os.remove(f)
|
| 173 |
+
elif os.path.isdir(f):
|
| 174 |
+
shutil.rmtree(f)
|
src/data/dataset.py
CHANGED
|
@@ -6,7 +6,7 @@ from torch.utils.data import Dataset
|
|
| 6 |
from src.data.transforms import augment_triplet
|
| 7 |
|
| 8 |
|
| 9 |
-
class
|
| 10 |
"""PyTorch Dataset for loading pre-processed Satellite TIR triplets.
|
| 11 |
|
| 12 |
Expects data to be stored as `.pt` files containing tensors of shape [3, 1, H, W],
|
|
|
|
| 6 |
from src.data.transforms import augment_triplet
|
| 7 |
|
| 8 |
|
| 9 |
+
class SatelliteTripletDataset(Dataset):
|
| 10 |
"""PyTorch Dataset for loading pre-processed Satellite TIR triplets.
|
| 11 |
|
| 12 |
Expects data to be stored as `.pt` files containing tensors of shape [3, 1, H, W],
|
src/data/fetchers/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .base_fetcher import SatelliteFetcher
|
src/data/fetchers/base_fetcher.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from abc import ABC, abstractmethod
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
logger = logging.getLogger(__name__)
|
| 6 |
+
|
| 7 |
+
class SatelliteFetcher(ABC):
|
| 8 |
+
"""
|
| 9 |
+
Abstract base class for all satellite data fetchers (GOES, Himawari, INSAT, etc.).
|
| 10 |
+
Enforces a strict interface for fetching and standardizing physical meteorological data.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
def __init__(self, bucket_name: str):
|
| 14 |
+
"""
|
| 15 |
+
Initializes the fetcher with the specific AWS or local bucket/source.
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
bucket_name (str): The name of the data repository/bucket.
|
| 19 |
+
"""
|
| 20 |
+
self.bucket_name = bucket_name
|
| 21 |
+
|
| 22 |
+
@abstractmethod
|
| 23 |
+
def fetch_chunk(self, chunk_prefix: str, output_dir: str) -> list[str]:
|
| 24 |
+
"""
|
| 25 |
+
Downloads a chunk of raw satellite files from the source to a local directory.
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
chunk_prefix (str): The path/prefix for the specific time chunk.
|
| 29 |
+
output_dir (str): Local temporary directory to save the raw files.
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
list[str]: A list of local file paths that were downloaded.
|
| 33 |
+
"""
|
| 34 |
+
pass
|
| 35 |
+
|
| 36 |
+
@abstractmethod
|
| 37 |
+
def apply_planck_function(self, raw_data_path: str) -> torch.Tensor:
|
| 38 |
+
"""
|
| 39 |
+
Reads the raw satellite file, applies the satellite-specific inverse
|
| 40 |
+
Planck function, and returns a physical Brightness Temperature tensor in Kelvin.
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
raw_data_path (str): The local path to the downloaded raw file (.nc, .h5, etc.).
|
| 44 |
+
|
| 45 |
+
Returns:
|
| 46 |
+
torch.Tensor: The calculated Brightness Temperature in Kelvin.
|
| 47 |
+
"""
|
| 48 |
+
pass
|
src/data/fetchers/goes_fetcher.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import logging
|
| 3 |
+
import boto3
|
| 4 |
+
from botocore import UNSIGNED
|
| 5 |
+
from botocore.config import Config
|
| 6 |
+
import xarray as xr
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
from src.data.fetchers.base_fetcher import SatelliteFetcher
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class GOESFetcher(SatelliteFetcher):
|
| 16 |
+
def __init__(self, bucket_name: str = "noaa-goes16"):
|
| 17 |
+
super().__init__(bucket_name)
|
| 18 |
+
self.s3_client = boto3.client(
|
| 19 |
+
"s3",
|
| 20 |
+
config=Config(signature_version=UNSIGNED)
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
def fetch_chunk(self, chunk_prefix: str, output_dir: str) -> list[str]:
|
| 24 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 25 |
+
|
| 26 |
+
paginator = self.s3_client.get_paginator("list_objects_v2")
|
| 27 |
+
|
| 28 |
+
c13_files = []
|
| 29 |
+
|
| 30 |
+
for page in paginator.paginate(
|
| 31 |
+
Bucket=self.bucket_name,
|
| 32 |
+
Prefix=chunk_prefix
|
| 33 |
+
):
|
| 34 |
+
for obj in page.get("Contents", []):
|
| 35 |
+
key = obj["Key"]
|
| 36 |
+
if "M6C13" in key and key.endswith(".nc"):
|
| 37 |
+
c13_files.append(key)
|
| 38 |
+
|
| 39 |
+
c13_files = sorted(c13_files)
|
| 40 |
+
|
| 41 |
+
downloaded_paths = []
|
| 42 |
+
|
| 43 |
+
for file_key in c13_files:
|
| 44 |
+
filename = os.path.basename(file_key)
|
| 45 |
+
local_path = os.path.join(output_dir, filename)
|
| 46 |
+
|
| 47 |
+
if not os.path.exists(local_path):
|
| 48 |
+
self.s3_client.download_file(
|
| 49 |
+
self.bucket_name,
|
| 50 |
+
file_key,
|
| 51 |
+
local_path
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
downloaded_paths.append(local_path)
|
| 55 |
+
|
| 56 |
+
return downloaded_paths
|
| 57 |
+
|
| 58 |
+
def apply_planck_function(self, raw_data_path: str) -> torch.Tensor:
|
| 59 |
+
with xr.open_dataset(raw_data_path, mask_and_scale=True) as ds:
|
| 60 |
+
rad = ds["Rad"]
|
| 61 |
+
|
| 62 |
+
fk1 = ds["planck_fk1"].values
|
| 63 |
+
fk2 = ds["planck_fk2"].values
|
| 64 |
+
bc1 = ds["planck_bc1"].values
|
| 65 |
+
bc2 = ds["planck_bc2"].values
|
| 66 |
+
|
| 67 |
+
rad_safe = rad.where(rad > 0)
|
| 68 |
+
|
| 69 |
+
bt = (fk2 / np.log((fk1 / rad_safe) + 1) - bc1) / bc2
|
| 70 |
+
|
| 71 |
+
clean = np.nan_to_num(
|
| 72 |
+
bt.values,
|
| 73 |
+
nan=0.0,
|
| 74 |
+
posinf=330.0,
|
| 75 |
+
neginf=180.0
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
return torch.from_numpy(clean).float().unsqueeze(0)
|
src/data/fetchers/himawari_fetcher.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import glob
|
| 3 |
+
import logging
|
| 4 |
+
import boto3
|
| 5 |
+
from botocore import UNSIGNED
|
| 6 |
+
from botocore.config import Config
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
from satpy import Scene
|
| 10 |
+
|
| 11 |
+
from src.data.fetchers.base_fetcher import SatelliteFetcher
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class HimawariFetcher(SatelliteFetcher):
|
| 17 |
+
"""
|
| 18 |
+
Himawari-8/9 AHI Full Disk Fetcher.
|
| 19 |
+
Band 14 (11.2 μm IR)
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
def __init__(self, bucket_name: str = "noaa-himawari9"):
|
| 23 |
+
super().__init__(bucket_name)
|
| 24 |
+
|
| 25 |
+
self.s3_client = boto3.client(
|
| 26 |
+
"s3",
|
| 27 |
+
config=Config(signature_version=UNSIGNED)
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
def fetch_chunk(
|
| 31 |
+
self,
|
| 32 |
+
chunk_prefix: str,
|
| 33 |
+
output_dir: str
|
| 34 |
+
) -> list[str]:
|
| 35 |
+
|
| 36 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 37 |
+
|
| 38 |
+
paginator = self.s3_client.get_paginator(
|
| 39 |
+
"list_objects_v2"
|
| 40 |
+
)
|
| 41 |
+
all_b14_files = []
|
| 42 |
+
|
| 43 |
+
for page in paginator.paginate(
|
| 44 |
+
Bucket=self.bucket_name,
|
| 45 |
+
Prefix=chunk_prefix
|
| 46 |
+
):
|
| 47 |
+
for obj in page.get("Contents", []):
|
| 48 |
+
key = obj["Key"]
|
| 49 |
+
|
| 50 |
+
if (
|
| 51 |
+
"B14" in key and
|
| 52 |
+
key.endswith(".DAT.bz2")
|
| 53 |
+
):
|
| 54 |
+
all_b14_files.append(key)
|
| 55 |
+
|
| 56 |
+
if not all_b14_files:
|
| 57 |
+
raise ValueError(
|
| 58 |
+
f"No B14 files found for {chunk_prefix}"
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# Group files by timestamp
|
| 62 |
+
timestamp_groups = {}
|
| 63 |
+
|
| 64 |
+
for key in all_b14_files:
|
| 65 |
+
filename = os.path.basename(key)
|
| 66 |
+
|
| 67 |
+
# Example:
|
| 68 |
+
# HS_H09_20260618_1200_B14_FLDK_R20_S0110.DAT.bz2
|
| 69 |
+
parts = filename.split("_")
|
| 70 |
+
|
| 71 |
+
timestamp = f"{parts[2]}_{parts[3]}"
|
| 72 |
+
|
| 73 |
+
if timestamp not in timestamp_groups:
|
| 74 |
+
timestamp_groups[timestamp] = []
|
| 75 |
+
|
| 76 |
+
timestamp_groups[timestamp].append(key)
|
| 77 |
+
|
| 78 |
+
frame_dirs = []
|
| 79 |
+
|
| 80 |
+
for timestamp in sorted(timestamp_groups.keys()):
|
| 81 |
+
segment_files = sorted(
|
| 82 |
+
timestamp_groups[timestamp]
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
if len(segment_files) != 10:
|
| 86 |
+
logger.warning(
|
| 87 |
+
f"Skipping incomplete frame {timestamp} "
|
| 88 |
+
f"({len(segment_files)} segments)"
|
| 89 |
+
)
|
| 90 |
+
continue
|
| 91 |
+
|
| 92 |
+
timestamp_dir = os.path.join(
|
| 93 |
+
output_dir,
|
| 94 |
+
timestamp
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
os.makedirs(timestamp_dir, exist_ok=True)
|
| 98 |
+
|
| 99 |
+
for file_key in segment_files:
|
| 100 |
+
filename = os.path.basename(file_key)
|
| 101 |
+
local_path = os.path.join(
|
| 102 |
+
timestamp_dir,
|
| 103 |
+
filename
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
if not os.path.exists(local_path):
|
| 107 |
+
self.s3_client.download_file(
|
| 108 |
+
self.bucket_name,
|
| 109 |
+
file_key,
|
| 110 |
+
local_path
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
frame_dirs.append(timestamp_dir)
|
| 114 |
+
|
| 115 |
+
return frame_dirs
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def apply_planck_function(
|
| 119 |
+
self,
|
| 120 |
+
raw_data_path: str
|
| 121 |
+
) -> torch.Tensor:
|
| 122 |
+
|
| 123 |
+
logger.debug(
|
| 124 |
+
f"[Himawari] Loading frame from {raw_data_path}"
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
file_paths = sorted(
|
| 128 |
+
glob.glob(
|
| 129 |
+
os.path.join(
|
| 130 |
+
raw_data_path,
|
| 131 |
+
"*_B14_FLDK_R20_S*.DAT.bz2"
|
| 132 |
+
)
|
| 133 |
+
)
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
if len(file_paths) != 10:
|
| 137 |
+
raise ValueError(
|
| 138 |
+
f"Incomplete Himawari frame: "
|
| 139 |
+
f"{len(file_paths)} segments found"
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
scn = Scene(
|
| 143 |
+
reader="ahi_hsd",
|
| 144 |
+
filenames=file_paths
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
scn.load(
|
| 148 |
+
["B14"],
|
| 149 |
+
calibration="brightness_temperature"
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
bt_data = scn["B14"].values
|
| 153 |
+
|
| 154 |
+
clean_numpy_array = np.nan_to_num(
|
| 155 |
+
bt_data,
|
| 156 |
+
nan=0.0,
|
| 157 |
+
posinf=330.0,
|
| 158 |
+
neginf=180.0
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
tensor = torch.from_numpy(
|
| 162 |
+
clean_numpy_array
|
| 163 |
+
).float().unsqueeze(0)
|
| 164 |
+
|
| 165 |
+
return tensor
|
src/data/s3_manager.py
DELETED
|
@@ -1,150 +0,0 @@
|
|
| 1 |
-
import glob
|
| 2 |
-
import logging
|
| 3 |
-
import os
|
| 4 |
-
import concurrent.futures
|
| 5 |
-
|
| 6 |
-
import boto3
|
| 7 |
-
import numpy as np
|
| 8 |
-
import torch
|
| 9 |
-
import torch.nn.functional as F
|
| 10 |
-
import xarray as xr
|
| 11 |
-
from botocore import UNSIGNED
|
| 12 |
-
from botocore.config import Config
|
| 13 |
-
|
| 14 |
-
from src.config.settings import Settings
|
| 15 |
-
|
| 16 |
-
logger = logging.getLogger(__name__)
|
| 17 |
-
|
| 18 |
-
class S3Manager:
|
| 19 |
-
"""Manages downloading GOES data, Argmax Motion-Guided Cropping, and purging."""
|
| 20 |
-
|
| 21 |
-
def __init__(self, settings: Settings):
|
| 22 |
-
self.settings = settings
|
| 23 |
-
self.s3_client = boto3.client("s3", config=Config(signature_version=UNSIGNED))
|
| 24 |
-
self.bucket_name = settings.data.s3_bucket
|
| 25 |
-
|
| 26 |
-
self.pt_dir = settings.data.download_dir
|
| 27 |
-
self.raw_dir = os.path.join(self.pt_dir, "raw_nc")
|
| 28 |
-
|
| 29 |
-
os.makedirs(self.pt_dir, exist_ok=True)
|
| 30 |
-
os.makedirs(self.raw_dir, exist_ok=True)
|
| 31 |
-
|
| 32 |
-
def download_chunk(self, prefix: str) -> None:
|
| 33 |
-
logger.info(f"Fetching chunk from S3: {prefix}")
|
| 34 |
-
|
| 35 |
-
response = self.s3_client.list_objects_v2(Bucket=self.bucket_name, Prefix=prefix)
|
| 36 |
-
c13_files = [obj['Key'] for obj in response.get('Contents', []) if 'M6C13' in obj['Key'] and obj['Key'].endswith('.nc')]
|
| 37 |
-
c13_files = sorted(c13_files)
|
| 38 |
-
|
| 39 |
-
if len(c13_files) < 3:
|
| 40 |
-
logger.warning(f"Not enough files in prefix {prefix} to form a triplet.")
|
| 41 |
-
return
|
| 42 |
-
|
| 43 |
-
step = self.settings.data.frame_step
|
| 44 |
-
crop_size = self.settings.data.crop_size
|
| 45 |
-
|
| 46 |
-
def fetch_file(key):
|
| 47 |
-
filename = key.split('/')[-1]
|
| 48 |
-
local_path = os.path.join(self.raw_dir, filename)
|
| 49 |
-
if not os.path.exists(local_path):
|
| 50 |
-
logger.info(f"Downloading {filename}...")
|
| 51 |
-
thread_s3 = boto3.client("s3", config=Config(signature_version=UNSIGNED))
|
| 52 |
-
thread_s3.download_file(self.bucket_name, key, local_path)
|
| 53 |
-
return local_path
|
| 54 |
-
|
| 55 |
-
# Concurrent downloading (Fixes network bottleneck)
|
| 56 |
-
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
|
| 57 |
-
local_nc_paths = list(executor.map(fetch_file, c13_files))
|
| 58 |
-
|
| 59 |
-
# 3. Create Triplets using Argmax Motion Map (Solves Problem 3 & 4)
|
| 60 |
-
for i in range(len(local_nc_paths) - 2 * step):
|
| 61 |
-
try:
|
| 62 |
-
# SOLVING PROBLEM 3: Load full array into RAM exactly ONCE per file.
|
| 63 |
-
t0_full = self._load_full_tensor(local_nc_paths[i])
|
| 64 |
-
t2_full = self._load_full_tensor(local_nc_paths[i + 2 * step])
|
| 65 |
-
|
| 66 |
-
# SOLVING PROBLEM 4: Create full motion map abs(t2 - t0)
|
| 67 |
-
motion_map = torch.abs(t2_full - t0_full) # Shape: [1, H, W]
|
| 68 |
-
|
| 69 |
-
# Ignore empty space (Outer space padding = 0.0)
|
| 70 |
-
space_mask = (t0_full > 0.0).float()
|
| 71 |
-
motion_map = motion_map * space_mask
|
| 72 |
-
|
| 73 |
-
# Add batch dimension for pooling: [1, 1, H, W]
|
| 74 |
-
motion_map_4d = motion_map.unsqueeze(0)
|
| 75 |
-
|
| 76 |
-
# Use PyTorch AvgPool2d to find the 256x256 window with maximum average motion
|
| 77 |
-
stride = 64
|
| 78 |
-
pooled_motion = F.avg_pool2d(motion_map_4d, kernel_size=crop_size, stride=stride)
|
| 79 |
-
|
| 80 |
-
# Find argmax (highest motion density) - BUG FIXED WITH .item()
|
| 81 |
-
b, c, h_out, w_out = pooled_motion.shape
|
| 82 |
-
flat_idx = torch.argmax(pooled_motion).item() # <--- Added .item() here
|
| 83 |
-
y_out = flat_idx // w_out
|
| 84 |
-
x_out = flat_idx % w_out
|
| 85 |
-
|
| 86 |
-
# Map pooled coordinates back to actual image coordinates
|
| 87 |
-
best_start_y = y_out * stride
|
| 88 |
-
best_start_x = x_out * stride
|
| 89 |
-
|
| 90 |
-
# SOLVING PROBLEM 1: Log the exact motion score to study distribution instead of magic numbers
|
| 91 |
-
best_motion = pooled_motion.flatten()[flat_idx].item()
|
| 92 |
-
logger.info(f"Motion-Guided Argmax Crop mapped at Y:{best_start_y}, X:{best_start_x} | Motion Score: {best_motion:.5f}")
|
| 93 |
-
|
| 94 |
-
# Slice the exact best crop directly from RAM
|
| 95 |
-
t0_crop = t0_full[:, best_start_y:best_start_y+crop_size, best_start_x:best_start_x+crop_size]
|
| 96 |
-
t2_crop = t2_full[:, best_start_y:best_start_y+crop_size, best_start_x:best_start_x+crop_size]
|
| 97 |
-
|
| 98 |
-
# SAFETY FILTER: Agar argmax ne bhi low-motion kachra uthaya hai, toh reject karo
|
| 99 |
-
crop_motion = torch.abs(t2_crop - t0_crop).mean().item()
|
| 100 |
-
if crop_motion < 0.005:
|
| 101 |
-
logger.warning(f"⏩ Argmax crop too static (Score: {crop_motion:.5f}). Skipping.")
|
| 102 |
-
continue
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
# Load T1 (Present) and slice using the exact same coordinates
|
| 106 |
-
t1_full = self._load_full_tensor(local_nc_paths[i + step])
|
| 107 |
-
t1_crop = t1_full[:, best_start_y:best_start_y+crop_size, best_start_x:best_start_x+crop_size]
|
| 108 |
-
# Save Triplet
|
| 109 |
-
triplet_tensor = torch.stack([t0_crop, t1_crop, t2_crop], dim=0)
|
| 110 |
-
safe_prefix = prefix.replace("/", "_")
|
| 111 |
-
pt_filename = os.path.join(self.pt_dir, f'triplet_{safe_prefix}_{i:03d}.pt')
|
| 112 |
-
|
| 113 |
-
torch.save(triplet_tensor, pt_filename)
|
| 114 |
-
|
| 115 |
-
except Exception as e:
|
| 116 |
-
logger.error(f"Error processing argmax triplet {i}: {e}")
|
| 117 |
-
continue
|
| 118 |
-
|
| 119 |
-
self.purge_raw_files()
|
| 120 |
-
logger.info(f"Chunk converted to .pt triplets. Raw .nc files purged.")
|
| 121 |
-
|
| 122 |
-
def _load_full_tensor(self, nc_path: str) -> torch.Tensor:
|
| 123 |
-
"""Loads the ENTIRE NetCDF image into memory ONCE to prevent 30x disk reads."""
|
| 124 |
-
with xr.open_dataset(nc_path) as ds:
|
| 125 |
-
rad = ds["Rad"]
|
| 126 |
-
fk1 = ds["planck_fk1"].values
|
| 127 |
-
fk2 = ds["planck_fk2"].values
|
| 128 |
-
bc1 = ds["planck_bc1"].values
|
| 129 |
-
bc2 = ds["planck_bc2"].values
|
| 130 |
-
|
| 131 |
-
rad_safe = rad.where(rad > 0)
|
| 132 |
-
bt = (fk2 / np.log((fk1 / rad_safe) + 1) - bc1) / bc2
|
| 133 |
-
|
| 134 |
-
min_bt, max_bt = 180.0, 330.0
|
| 135 |
-
bt_norm = (bt - min_bt) / (max_bt - min_bt)
|
| 136 |
-
bt_norm = bt_norm.clip(0, 1)
|
| 137 |
-
|
| 138 |
-
clean_numpy_array = np.nan_to_num(bt_norm.values, nan=0.0, posinf=1.0, neginf=0.0)
|
| 139 |
-
tensor = torch.from_numpy(clean_numpy_array).float().unsqueeze(0)
|
| 140 |
-
|
| 141 |
-
return tensor
|
| 142 |
-
|
| 143 |
-
def purge_raw_files(self) -> None:
|
| 144 |
-
for f in glob.glob(os.path.join(self.raw_dir, "*.nc")):
|
| 145 |
-
os.remove(f)
|
| 146 |
-
|
| 147 |
-
def purge_chunk(self) -> None:
|
| 148 |
-
for f in glob.glob(os.path.join(self.pt_dir, "*.pt")):
|
| 149 |
-
os.remove(f)
|
| 150 |
-
logger.info("Purged trained .pt chunk from disk.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/data/standardizer.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
logger = logging.getLogger(__name__)
|
| 5 |
+
|
| 6 |
+
class UniversalStandardizer:
|
| 7 |
+
"""
|
| 8 |
+
Standardizes physical radiometric satellite data into unified AI-ready tensors.
|
| 9 |
+
Ensures that irrespective of the satellite source, the model receives uniformly
|
| 10 |
+
scaled inputs.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
@staticmethod
|
| 14 |
+
def normalize_bt(bt_tensor: torch.Tensor, min_bt: float = 180.0, max_bt: float = 330.0) -> torch.Tensor:
|
| 15 |
+
"""
|
| 16 |
+
Clips Brightness Temperature (K) and normalizes it to a [0, 1] range.
|
| 17 |
+
|
| 18 |
+
Args:
|
| 19 |
+
bt_tensor (torch.Tensor): The physical brightness temperature tensor in Kelvin.
|
| 20 |
+
min_bt (float, optional): Lower bound for clipping (typically overshooting tops). Defaults to 180.0.
|
| 21 |
+
max_bt (float, optional): Upper bound for clipping (typically hot desert). Defaults to 330.0.
|
| 22 |
+
|
| 23 |
+
Returns:
|
| 24 |
+
torch.Tensor: Normalized tensor bounded strictly between [0, 1].
|
| 25 |
+
"""
|
| 26 |
+
logger.debug(f"Normalizing tensor of shape {bt_tensor.shape} with bounds [{min_bt}K, {max_bt}K]")
|
| 27 |
+
|
| 28 |
+
# Apply min-max normalization
|
| 29 |
+
bt_norm = (bt_tensor - min_bt) / (max_bt - min_bt)
|
| 30 |
+
|
| 31 |
+
# Clip values strictly between 0 and 1
|
| 32 |
+
bt_norm = torch.clamp(bt_norm, 0.0, 1.0)
|
| 33 |
+
|
| 34 |
+
# Clean any remaining NaNs or Infs that might have survived the math
|
| 35 |
+
bt_norm = torch.nan_to_num(bt_norm, nan=0.0, posinf=1.0, neginf=0.0)
|
| 36 |
+
|
| 37 |
+
return bt_norm
|
src/data/transforms.py
CHANGED
|
@@ -19,6 +19,10 @@ def augment_triplet(img0: torch.Tensor, img1: torch.Tensor, gt: torch.Tensor, cr
|
|
| 19 |
Tuple of augmented tensors (img0, img1, gt).
|
| 20 |
"""
|
| 21 |
_, h, w = img0.shape
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
# Random Spatial Cropping
|
| 24 |
top = random.randint(0, h - crop_size)
|
|
|
|
| 19 |
Tuple of augmented tensors (img0, img1, gt).
|
| 20 |
"""
|
| 21 |
_, h, w = img0.shape
|
| 22 |
+
if h < crop_size or w < crop_size:
|
| 23 |
+
raise ValueError(
|
| 24 |
+
f"Input smaller than crop size: {h}x{w}"
|
| 25 |
+
)
|
| 26 |
|
| 27 |
# Random Spatial Cropping
|
| 28 |
top = random.randint(0, h - crop_size)
|
src/training/trainer.py
CHANGED
|
@@ -9,7 +9,7 @@ from torch.utils.tensorboard import SummaryWriter
|
|
| 9 |
from src.config.settings import Settings
|
| 10 |
from src.model.ifnet import IFNet
|
| 11 |
from src.model.loss import CompositeLoss
|
| 12 |
-
from src.data.dataset import
|
| 13 |
|
| 14 |
logger = logging.getLogger(__name__)
|
| 15 |
|
|
@@ -58,9 +58,11 @@ class Trainer:
|
|
| 58 |
self.settings = settings
|
| 59 |
self.device = device
|
| 60 |
self.model = model.to(device)
|
| 61 |
-
self.optimizer = AdamW(self.model.parameters(), lr=settings.training.learning_rate, weight_decay=
|
| 62 |
self.criterion = CompositeLoss()
|
| 63 |
-
|
|
|
|
|
|
|
| 64 |
self.writer = SummaryWriter(log_dir=os.path.join(settings.training.checkpoints_dir, 'logs'))
|
| 65 |
os.makedirs(settings.training.checkpoints_dir, exist_ok=True)
|
| 66 |
self.global_step = 0
|
|
@@ -70,12 +72,19 @@ class Trainer:
|
|
| 70 |
|
| 71 |
def train_chunk(self, data_dir: str, epoch: int) -> None:
|
| 72 |
"""Trains the model for one epoch on the current data chunk."""
|
| 73 |
-
dataset =
|
| 74 |
if len(dataset) == 0:
|
| 75 |
logger.warning(f"No data found in {data_dir}. Skipping training chunk.")
|
| 76 |
return
|
| 77 |
|
| 78 |
-
dataloader = DataLoader(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
self.model.train()
|
| 81 |
for batch_idx, (img0, img1, gt) in enumerate(dataloader):
|
|
@@ -102,8 +111,9 @@ class Trainer:
|
|
| 102 |
|
| 103 |
loss = loss_student + loss_teacher + (loss_distill * 0.01)
|
| 104 |
|
| 105 |
-
loss.backward()
|
| 106 |
-
self.
|
|
|
|
| 107 |
if batch_idx % 10 == 0:
|
| 108 |
logger.info(f"Epoch {epoch} | Batch {batch_idx}/{len(dataloader)} | Loss: {loss.item():.4f}")
|
| 109 |
self.writer.add_scalar('Loss/train', loss.item(), self.global_step)
|
|
|
|
| 9 |
from src.config.settings import Settings
|
| 10 |
from src.model.ifnet import IFNet
|
| 11 |
from src.model.loss import CompositeLoss
|
| 12 |
+
from src.data.dataset import SatelliteTripletDataset
|
| 13 |
|
| 14 |
logger = logging.getLogger(__name__)
|
| 15 |
|
|
|
|
| 58 |
self.settings = settings
|
| 59 |
self.device = device
|
| 60 |
self.model = model.to(device)
|
| 61 |
+
self.optimizer = AdamW(self.model.parameters(), lr=settings.training.learning_rate, weight_decay=settings.training.weight_decay)
|
| 62 |
self.criterion = CompositeLoss()
|
| 63 |
+
self.scaler = torch.amp.GradScaler(
|
| 64 |
+
enabled=self.device.type == "cuda"
|
| 65 |
+
)
|
| 66 |
self.writer = SummaryWriter(log_dir=os.path.join(settings.training.checkpoints_dir, 'logs'))
|
| 67 |
os.makedirs(settings.training.checkpoints_dir, exist_ok=True)
|
| 68 |
self.global_step = 0
|
|
|
|
| 72 |
|
| 73 |
def train_chunk(self, data_dir: str, epoch: int) -> None:
|
| 74 |
"""Trains the model for one epoch on the current data chunk."""
|
| 75 |
+
dataset = SatelliteTripletDataset(data_dir=data_dir, augment=True)
|
| 76 |
if len(dataset) == 0:
|
| 77 |
logger.warning(f"No data found in {data_dir}. Skipping training chunk.")
|
| 78 |
return
|
| 79 |
|
| 80 |
+
dataloader = DataLoader(
|
| 81 |
+
dataset,
|
| 82 |
+
batch_size=self.settings.training.batch_size,
|
| 83 |
+
shuffle=True,
|
| 84 |
+
num_workers=self.settings.training.num_workers,
|
| 85 |
+
pin_memory=True,
|
| 86 |
+
persistent_workers=True
|
| 87 |
+
)
|
| 88 |
|
| 89 |
self.model.train()
|
| 90 |
for batch_idx, (img0, img1, gt) in enumerate(dataloader):
|
|
|
|
| 111 |
|
| 112 |
loss = loss_student + loss_teacher + (loss_distill * 0.01)
|
| 113 |
|
| 114 |
+
self.scaler.scale(loss).backward()
|
| 115 |
+
self.scaler.step(self.optimizer)
|
| 116 |
+
self.scaler.update()
|
| 117 |
if batch_idx % 10 == 0:
|
| 118 |
logger.info(f"Epoch {epoch} | Batch {batch_idx}/{len(dataloader)} | Loss: {loss.item():.4f}")
|
| 119 |
self.writer.add_scalar('Loss/train', loss.item(), self.global_step)
|
tests/test_config.py
CHANGED
|
@@ -1,36 +1,39 @@
|
|
| 1 |
-
import logging
|
| 2 |
-
import os
|
| 3 |
-
|
| 4 |
from src.config.settings import load_settings
|
| 5 |
-
|
| 6 |
|
| 7 |
def test_load_settings(tmp_path):
|
| 8 |
yaml_content = """
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
config_file = tmp_path / "config.yaml"
|
| 26 |
config_file.write_text(yaml_content)
|
| 27 |
|
| 28 |
settings = load_settings(str(config_file))
|
| 29 |
-
assert settings.training.epochs == 100
|
| 30 |
-
assert settings.data.year == 2024
|
| 31 |
-
assert settings.training.load_model_path == "/kaggle/input/models/1week_model.pth"
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
assert
|
| 36 |
-
assert
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from src.config.settings import load_settings
|
| 2 |
+
|
| 3 |
|
| 4 |
def test_load_settings(tmp_path):
|
| 5 |
yaml_content = """
|
| 6 |
+
training:
|
| 7 |
+
epochs: 100
|
| 8 |
+
batch_size: 4
|
| 9 |
+
learning_rate: 0.0001
|
| 10 |
+
weight_decay: 0.001
|
| 11 |
+
num_workers: 4
|
| 12 |
+
checkpoints_dir: "/tmp/checkpoints"
|
| 13 |
+
load_model_path: ""
|
| 14 |
+
|
| 15 |
+
data:
|
| 16 |
+
satellite_type: "goes"
|
| 17 |
+
s3_bucket: "noaa-goes16"
|
| 18 |
+
download_dir: "/tmp/data"
|
| 19 |
+
prefix_type: "ABI-L1b-RadF"
|
| 20 |
+
year: 2024
|
| 21 |
+
start_unit: 264
|
| 22 |
+
end_unit: 294
|
| 23 |
+
frame_step: 3
|
| 24 |
+
crop_size: 512
|
| 25 |
+
crop_stride_divisor: 4
|
| 26 |
+
static_motion_threshold: 0.005
|
| 27 |
+
min_bt: 180.0
|
| 28 |
+
max_bt: 330.0
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
config_file = tmp_path / "config.yaml"
|
| 32 |
config_file.write_text(yaml_content)
|
| 33 |
|
| 34 |
settings = load_settings(str(config_file))
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
assert settings.training.weight_decay == 0.001
|
| 37 |
+
assert settings.training.num_workers == 4
|
| 38 |
+
assert settings.data.satellite_type == "goes"
|
| 39 |
+
assert settings.data.crop_stride_divisor == 4
|
tests/test_data_manager.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import pytest
|
| 3 |
+
|
| 4 |
+
from src.data.data_manager import DataManager
|
| 5 |
+
from src.config.settings import Settings, TrainingConfig, DataConfig
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@pytest.fixture
|
| 9 |
+
def settings():
|
| 10 |
+
return Settings(
|
| 11 |
+
training=TrainingConfig(
|
| 12 |
+
epochs=1,
|
| 13 |
+
batch_size=1,
|
| 14 |
+
learning_rate=1e-4,
|
| 15 |
+
weight_decay=0.001,
|
| 16 |
+
num_workers=2,
|
| 17 |
+
checkpoints_dir="chkpt"
|
| 18 |
+
),
|
| 19 |
+
data=DataConfig(
|
| 20 |
+
satellite_type="goes",
|
| 21 |
+
s3_bucket="test",
|
| 22 |
+
download_dir="tmp",
|
| 23 |
+
prefix_type="ABI",
|
| 24 |
+
year=2024,
|
| 25 |
+
start_unit=1,
|
| 26 |
+
end_unit=2,
|
| 27 |
+
frame_step=1,
|
| 28 |
+
crop_size=64,
|
| 29 |
+
crop_stride_divisor=4,
|
| 30 |
+
static_motion_threshold=0.005
|
| 31 |
+
)
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def test_motion_crop_detects_motion(settings):
|
| 36 |
+
manager = DataManager(settings)
|
| 37 |
+
|
| 38 |
+
img0 = torch.ones((1, 256, 256)) * 0.5
|
| 39 |
+
img1 = img0.clone()
|
| 40 |
+
gt = img0.clone()
|
| 41 |
+
|
| 42 |
+
img1[:, 100:164, 100:164] = 1.0
|
| 43 |
+
|
| 44 |
+
crop0, crop1, crop_gt = manager._motion_guided_argmax_crop(
|
| 45 |
+
img0, img1, gt
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
motion = torch.abs(crop1 - crop0).mean().item()
|
| 49 |
+
|
| 50 |
+
assert crop0.shape == (1, 64, 64)
|
| 51 |
+
assert motion > settings.data.static_motion_threshold
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def test_static_crop_rejection(settings):
|
| 55 |
+
manager = DataManager(settings)
|
| 56 |
+
|
| 57 |
+
img0 = torch.zeros((1, 256, 256))
|
| 58 |
+
img1 = torch.zeros((1, 256, 256))
|
| 59 |
+
gt = torch.zeros((1, 256, 256))
|
| 60 |
+
|
| 61 |
+
with pytest.raises(ValueError):
|
| 62 |
+
manager._motion_guided_argmax_crop(img0, img1, gt)
|
tests/test_dataset.py
CHANGED
|
@@ -1,24 +1,30 @@
|
|
| 1 |
import torch
|
| 2 |
-
import
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
def test_dataset(tmp_path):
|
| 7 |
-
# Setup mock data
|
| 8 |
-
tensor_data = torch.zeros((3, 1, 512, 512))
|
| 9 |
torch.save(tensor_data, tmp_path / "triplet_001.pt")
|
| 10 |
-
|
| 11 |
-
dataset =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
assert len(dataset) == 1
|
| 13 |
-
|
| 14 |
img0, img1, gt = dataset[0]
|
| 15 |
-
|
| 16 |
-
assert
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
img0 = torch.zeros((1, 512, 512))
|
| 20 |
-
img1 = torch.zeros((1, 512, 512))
|
| 21 |
-
gt = torch.zeros((1, 512, 512))
|
| 22 |
-
|
| 23 |
-
a_img0, a_img1, a_gt = augment_triplet(img0, img1, gt, crop_size=256)
|
| 24 |
-
assert a_img0.shape == (1, 256, 256)
|
|
|
|
| 1 |
import torch
|
| 2 |
+
from src.data.dataset import SatelliteTripletDataset
|
| 3 |
+
def test_dataset_augment(tmp_path):
|
| 4 |
+
tensor_data = torch.ones((3, 1, 512, 512))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
torch.save(tensor_data, tmp_path / "triplet_001.pt")
|
| 6 |
+
|
| 7 |
+
dataset = SatelliteTripletDataset(str(tmp_path), augment=True)
|
| 8 |
+
|
| 9 |
+
img0, img1, gt = dataset[0]
|
| 10 |
+
|
| 11 |
+
assert img0.shape == (1, 256, 256)
|
| 12 |
+
assert img1.shape == (1, 256, 256)
|
| 13 |
+
assert gt.shape == (1, 256, 256)
|
| 14 |
+
|
| 15 |
+
def test_dataset_loading(tmp_path):
|
| 16 |
+
triplet = torch.zeros((3, 1, 256, 256))
|
| 17 |
+
torch.save(triplet, tmp_path / "triplet_001.pt")
|
| 18 |
+
|
| 19 |
+
dataset = SatelliteTripletDataset(
|
| 20 |
+
str(tmp_path),
|
| 21 |
+
augment=False
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
assert len(dataset) == 1
|
| 25 |
+
|
| 26 |
img0, img1, gt = dataset[0]
|
| 27 |
+
|
| 28 |
+
assert img0.shape == (1, 256, 256)
|
| 29 |
+
assert img1.shape == (1, 256, 256)
|
| 30 |
+
assert gt.shape == (1, 256, 256)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_fetchers.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import xarray as xr
|
| 3 |
+
import torch
|
| 4 |
+
from src.data.fetchers.goes_fetcher import GOESFetcher
|
| 5 |
+
import pytest
|
| 6 |
+
from unittest.mock import MagicMock
|
| 7 |
+
|
| 8 |
+
from src.data.fetchers.himawari_fetcher import HimawariFetcher
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def test_goes_planck(tmp_path):
|
| 13 |
+
ds = xr.Dataset(
|
| 14 |
+
{
|
| 15 |
+
"Rad": (
|
| 16 |
+
("y", "x"),
|
| 17 |
+
np.ones((32, 32)) * 10
|
| 18 |
+
)
|
| 19 |
+
}
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
ds["planck_fk1"] = 1.0
|
| 23 |
+
ds["planck_fk2"] = 100.0
|
| 24 |
+
ds["planck_bc1"] = 0.1
|
| 25 |
+
ds["planck_bc2"] = 1.0
|
| 26 |
+
|
| 27 |
+
nc_path = tmp_path / "test.nc"
|
| 28 |
+
ds.to_netcdf(nc_path)
|
| 29 |
+
|
| 30 |
+
fetcher = GOESFetcher()
|
| 31 |
+
|
| 32 |
+
tensor = fetcher.apply_planck_function(
|
| 33 |
+
str(nc_path)
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
assert isinstance(tensor, torch.Tensor)
|
| 37 |
+
assert tensor.shape == (1, 32, 32)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def test_himawari_missing_segments(tmp_path):
|
| 43 |
+
fetcher = HimawariFetcher("dummy")
|
| 44 |
+
|
| 45 |
+
fetcher.s3_client.get_paginator = MagicMock()
|
| 46 |
+
fetcher.s3_client.get_paginator.return_value.paginate.return_value = [
|
| 47 |
+
{
|
| 48 |
+
"Contents": [
|
| 49 |
+
{
|
| 50 |
+
"Key": f"HS_H09_20260618_1200_B14_FLDK_R20_S{i:04d}.DAT.bz2"
|
| 51 |
+
}
|
| 52 |
+
for i in range(8)
|
| 53 |
+
]
|
| 54 |
+
}
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
result = fetcher.fetch_chunk("prefix", str(tmp_path))
|
| 58 |
+
|
| 59 |
+
assert result == []
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def test_goes_corrupt_file(tmp_path):
|
| 65 |
+
corrupt_file = tmp_path / "bad.nc"
|
| 66 |
+
corrupt_file.write_text("corrupted")
|
| 67 |
+
|
| 68 |
+
fetcher = GOESFetcher("dummy")
|
| 69 |
+
|
| 70 |
+
with pytest.raises(Exception):
|
| 71 |
+
fetcher.apply_planck_function(str(corrupt_file))
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def test_himawari_corrupt_frame(tmp_path):
|
| 77 |
+
frame_dir = tmp_path / "frame"
|
| 78 |
+
frame_dir.mkdir()
|
| 79 |
+
|
| 80 |
+
for i in range(5):
|
| 81 |
+
(frame_dir / f"seg_{i}.DAT.bz2").write_text("bad")
|
| 82 |
+
|
| 83 |
+
fetcher = HimawariFetcher("dummy")
|
| 84 |
+
|
| 85 |
+
with pytest.raises(ValueError):
|
| 86 |
+
fetcher.apply_planck_function(str(frame_dir))
|
tests/test_pipeline.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from unittest.mock import MagicMock
|
| 3 |
+
|
| 4 |
+
from src.data.data_manager import DataManager
|
| 5 |
+
from src.config.settings import Settings, TrainingConfig, DataConfig
|
| 6 |
+
import pytest
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@pytest.fixture
|
| 11 |
+
def settings():
|
| 12 |
+
return Settings(
|
| 13 |
+
training=TrainingConfig(
|
| 14 |
+
epochs=1,
|
| 15 |
+
batch_size=1,
|
| 16 |
+
learning_rate=1e-4,
|
| 17 |
+
weight_decay=0.001,
|
| 18 |
+
num_workers=2,
|
| 19 |
+
checkpoints_dir="chkpt"
|
| 20 |
+
),
|
| 21 |
+
data=DataConfig(
|
| 22 |
+
satellite_type="goes",
|
| 23 |
+
s3_bucket="dummy",
|
| 24 |
+
download_dir="tmp",
|
| 25 |
+
prefix_type="ABI",
|
| 26 |
+
year=2024,
|
| 27 |
+
start_unit=1,
|
| 28 |
+
end_unit=2,
|
| 29 |
+
frame_step=1,
|
| 30 |
+
crop_size=64,
|
| 31 |
+
crop_stride_divisor=4,
|
| 32 |
+
static_motion_threshold=0.005
|
| 33 |
+
)
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
def test_pipeline_integration(tmp_path):
|
| 37 |
+
settings = Settings(
|
| 38 |
+
training=TrainingConfig(
|
| 39 |
+
epochs=1,
|
| 40 |
+
batch_size=1,
|
| 41 |
+
learning_rate=1e-4,
|
| 42 |
+
weight_decay=0.001,
|
| 43 |
+
num_workers=2,
|
| 44 |
+
checkpoints_dir="chkpt"
|
| 45 |
+
),
|
| 46 |
+
data=DataConfig(
|
| 47 |
+
satellite_type="goes",
|
| 48 |
+
s3_bucket="dummy",
|
| 49 |
+
download_dir=str(tmp_path),
|
| 50 |
+
prefix_type="ABI",
|
| 51 |
+
year=2024,
|
| 52 |
+
start_unit=1,
|
| 53 |
+
end_unit=2,
|
| 54 |
+
frame_step=1,
|
| 55 |
+
crop_size=64,
|
| 56 |
+
crop_stride_divisor=4,
|
| 57 |
+
static_motion_threshold=0.005
|
| 58 |
+
)
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
manager = DataManager(settings)
|
| 62 |
+
|
| 63 |
+
manager.fetcher.fetch_chunk = MagicMock(
|
| 64 |
+
return_value=["a.nc", "b.nc", "c.nc"]
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
img0 = torch.ones((1, 256, 256)) * 250.0
|
| 68 |
+
gt = img0.clone()
|
| 69 |
+
img1 = img0.clone()
|
| 70 |
+
img1[:, 100:164, 100:164] = 300.0
|
| 71 |
+
|
| 72 |
+
fake_tensors = [img0, gt, img1]
|
| 73 |
+
|
| 74 |
+
manager.fetcher.apply_planck_function = MagicMock(
|
| 75 |
+
side_effect=fake_tensors
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
manager.process_chunk("dummy_prefix")
|
| 79 |
+
|
| 80 |
+
saved_files = list(tmp_path.glob("*.pt"))
|
| 81 |
+
|
| 82 |
+
assert len(saved_files) == 1
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def test_factory_goes(settings):
|
| 89 |
+
settings.data.satellite_type = "goes"
|
| 90 |
+
|
| 91 |
+
manager = DataManager(settings)
|
| 92 |
+
|
| 93 |
+
assert manager.fetcher.__class__.__name__ == "GOESFetcher"
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def test_factory_himawari(settings):
|
| 97 |
+
settings.data.satellite_type = "himawari"
|
| 98 |
+
|
| 99 |
+
manager = DataManager(settings)
|
| 100 |
+
|
| 101 |
+
assert manager.fetcher.__class__.__name__ == "HimawariFetcher"
|
tests/test_s3_manager.py
DELETED
|
@@ -1,46 +0,0 @@
|
|
| 1 |
-
from unittest.mock import MagicMock, patch
|
| 2 |
-
import numpy as np
|
| 3 |
-
import pytest
|
| 4 |
-
import xarray as xr
|
| 5 |
-
import torch
|
| 6 |
-
|
| 7 |
-
from src.config.settings import DataConfig, Settings, TrainingConfig
|
| 8 |
-
from src.data.s3_manager import S3Manager
|
| 9 |
-
|
| 10 |
-
@pytest.fixture
|
| 11 |
-
def mock_settings():
|
| 12 |
-
# Updated to match the exact schema
|
| 13 |
-
return Settings(
|
| 14 |
-
training=TrainingConfig(
|
| 15 |
-
epochs=1, batch_size=1, learning_rate=0.001,
|
| 16 |
-
checkpoints_dir="chkpt", load_model_path=""
|
| 17 |
-
),
|
| 18 |
-
data=DataConfig(
|
| 19 |
-
s3_bucket="test-bucket", download_dir="test_dir",
|
| 20 |
-
year=2024, start_day=200, end_day=201, frame_step=3, crop_size=256
|
| 21 |
-
),
|
| 22 |
-
)
|
| 23 |
-
|
| 24 |
-
def test_load_full_tensor(mock_settings, tmp_path):
|
| 25 |
-
manager = S3Manager(mock_settings)
|
| 26 |
-
|
| 27 |
-
# Create mock NetCDF dataset simulating GOES-16/19 data
|
| 28 |
-
ds = xr.Dataset(
|
| 29 |
-
{"Rad": (("y", "x"), np.ones((512, 512)) * 10)},
|
| 30 |
-
coords={"y": np.arange(512), "x": np.arange(512)},
|
| 31 |
-
)
|
| 32 |
-
ds["planck_fk1"] = 1.0
|
| 33 |
-
ds["planck_fk2"] = 100.0
|
| 34 |
-
ds["planck_bc1"] = 0.1
|
| 35 |
-
ds["planck_bc2"] = 1.0
|
| 36 |
-
|
| 37 |
-
nc_path = tmp_path / "test.nc"
|
| 38 |
-
ds.to_netcdf(nc_path)
|
| 39 |
-
|
| 40 |
-
# 🚨 FIX: Call the ACTUAL method that exists in your s3_manager.py
|
| 41 |
-
tensor = manager._load_full_tensor(str(nc_path))
|
| 42 |
-
|
| 43 |
-
# Assertions to ensure it returns a valid PyTorch tensor with the correct shape
|
| 44 |
-
assert isinstance(tensor, torch.Tensor)
|
| 45 |
-
assert tensor.shape == (1, 512, 512)
|
| 46 |
-
assert not torch.isnan(tensor).any() # Ensure no NaN values exist in the output
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_standardizer.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from src.data.standardizer import UniversalStandardizer
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def test_normalize_bt():
|
| 6 |
+
bt = torch.tensor([180.0, 255.0, 330.0])
|
| 7 |
+
|
| 8 |
+
out = UniversalStandardizer.normalize_bt(bt)
|
| 9 |
+
|
| 10 |
+
assert out[0] == 0.0
|
| 11 |
+
assert out[2] == 1.0
|
| 12 |
+
assert 0.0 <= out[1] <= 1.0
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_normalize_bt_clipping():
|
| 16 |
+
bt = torch.tensor([100.0, 400.0])
|
| 17 |
+
|
| 18 |
+
out = UniversalStandardizer.normalize_bt(bt)
|
| 19 |
+
|
| 20 |
+
assert out[0] == 0.0
|
| 21 |
+
assert out[1] == 1.0
|
tests/test_trainer.py
CHANGED
|
@@ -1,23 +1,77 @@
|
|
| 1 |
import torch
|
| 2 |
-
import pytest
|
| 3 |
from src.training.trainer import Trainer
|
| 4 |
from src.model.ifnet import IFNet
|
| 5 |
from src.config.settings import Settings, TrainingConfig, DataConfig
|
|
|
|
|
|
|
| 6 |
|
| 7 |
def test_trainer_init():
|
| 8 |
-
# 🚨 FIX: Updated DataConfig and TrainingConfig parameters
|
| 9 |
settings = Settings(
|
| 10 |
training=TrainingConfig(
|
| 11 |
-
epochs=1,
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
),
|
| 14 |
data=DataConfig(
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
)
|
| 18 |
)
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import torch
|
|
|
|
| 2 |
from src.training.trainer import Trainer
|
| 3 |
from src.model.ifnet import IFNet
|
| 4 |
from src.config.settings import Settings, TrainingConfig, DataConfig
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
|
| 8 |
def test_trainer_init():
|
|
|
|
| 9 |
settings = Settings(
|
| 10 |
training=TrainingConfig(
|
| 11 |
+
epochs=1,
|
| 12 |
+
batch_size=2,
|
| 13 |
+
learning_rate=1e-4,
|
| 14 |
+
weight_decay=0.001,
|
| 15 |
+
num_workers=2,
|
| 16 |
+
checkpoints_dir="chkpt"
|
| 17 |
),
|
| 18 |
data=DataConfig(
|
| 19 |
+
satellite_type="goes",
|
| 20 |
+
s3_bucket="bucket",
|
| 21 |
+
download_dir="data",
|
| 22 |
+
prefix_type="ABI",
|
| 23 |
+
year=2024,
|
| 24 |
+
start_unit=1,
|
| 25 |
+
end_unit=2,
|
| 26 |
+
frame_step=1,
|
| 27 |
+
crop_size=256
|
| 28 |
)
|
| 29 |
)
|
| 30 |
+
|
| 31 |
+
trainer = Trainer(
|
| 32 |
+
settings,
|
| 33 |
+
IFNet(),
|
| 34 |
+
torch.device("cpu")
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
assert trainer.global_step == 0
|
| 38 |
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def test_checkpoint_save(tmp_path):
|
| 42 |
+
settings = Settings(
|
| 43 |
+
training=TrainingConfig(
|
| 44 |
+
epochs=1,
|
| 45 |
+
batch_size=2,
|
| 46 |
+
learning_rate=1e-4,
|
| 47 |
+
weight_decay=0.001,
|
| 48 |
+
num_workers=2,
|
| 49 |
+
checkpoints_dir=str(tmp_path)
|
| 50 |
+
),
|
| 51 |
+
data=DataConfig(
|
| 52 |
+
satellite_type="goes",
|
| 53 |
+
s3_bucket="b",
|
| 54 |
+
download_dir="d",
|
| 55 |
+
prefix_type="ABI",
|
| 56 |
+
year=2024,
|
| 57 |
+
start_unit=1,
|
| 58 |
+
end_unit=2,
|
| 59 |
+
frame_step=1,
|
| 60 |
+
crop_size=256,
|
| 61 |
+
crop_stride_divisor=4,
|
| 62 |
+
static_motion_threshold=0.005
|
| 63 |
+
)
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
trainer = Trainer(
|
| 67 |
+
settings,
|
| 68 |
+
model=IFNet(),
|
| 69 |
+
device=torch.device("cpu")
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
trainer.save_checkpoint("test_model.pth")
|
| 73 |
+
trainer.shutdown()
|
| 74 |
+
|
| 75 |
+
assert os.path.exists(
|
| 76 |
+
os.path.join(tmp_path, "test_model.pth")
|
| 77 |
+
)
|
uv.lock
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|