Siddhant Sharma commited on
Commit
4e9fa0a
·
1 Parent(s): 3dfc96f

Added multi satellite based fetching for training

Browse files
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.s3_manager import S3Manager
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
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
14
-
15
- logger.info("Starting Autonomous Satellite Interpolation Pipeline...")
16
-
17
- s3_manager = S3Manager(settings)
 
 
 
 
 
 
 
 
18
  model = IFNet()
19
-
20
- # Load previous checkpoint if it exists
21
  checkpoint_path = settings.training.load_model_path
22
-
23
  if os.path.exists(checkpoint_path):
24
- model.load_state_dict(torch.load(checkpoint_path, map_location=device))
25
- logger.info(f"✅ Loaded EXISTING model from Kaggle Input. Resuming training!")
 
 
 
 
 
 
 
 
26
  else:
27
- logger.warning(f"⚠️ Checkpoint not found at {checkpoint_path}. Starting from scratch.")
28
- trainer = Trainer(settings, model, device)
29
-
30
- # 🤖 100% Config-Driven Chunk Generation
 
 
 
 
 
 
 
 
 
31
  year = settings.data.year
32
- start_day = settings.data.start_day
33
- end_day = settings.data.end_day
34
- prefix_type = settings.data.prefix_type # 🚨 Config se prefix uthaya
35
-
36
- # 🚨 Hardcoded RadC hata diya!
37
- chunks = [f"{prefix_type}/{year}/{day:03d}/" for day in range(start_day, end_day + 1)]
38
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  for chunk_idx, chunk in enumerate(chunks):
40
- logger.info(f"=== Fetching and Processing Chunk {chunk_idx + 1}/{len(chunks)}: {chunk} ===")
41
-
42
- s3_manager.download_chunk(chunk)
43
-
44
- for epoch in range(1, settings.training.epochs + 1):
45
- logger.info(f"--- Chunk {chunk_idx + 1} | Starting Epoch {epoch}/{settings.training.epochs} ---")
46
- trainer.train_chunk(settings.data.download_dir, epoch)
47
- trainer.save_checkpoint("latest_model.pth")
48
-
49
- s3_manager.purge_chunk()
50
- trainer.shutdown()
51
- logger.info("🔥 Config-Driven Multi-Month Training Complete!")
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
- s3_bucket: "noaa-goes19" # Agar GOES-19 chahiye toh yahan update kar lena
9
- download_dir: "/kaggle/working/goes_data"
10
- prefix_type: "ABI-L1b-RadF" # 🚨 Naya prefix variable
 
 
 
 
 
11
  year: 2024
12
- start_day: 264
13
- end_day: 294
 
 
 
14
  frame_step: 3
15
- crop_size: 512
 
 
 
 
 
 
 
 
 
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 # 🚨 Yahan prefix_type add kar diya
 
17
  year: int
18
- start_day: int
19
- end_day: int
20
- frame_step: int
21
- crop_size: int
 
 
 
 
 
 
 
 
 
 
22
 
23
  @dataclass
24
  class Settings:
25
  training: TrainingConfig
26
  data: DataConfig
27
 
28
- def load_settings(config_path: str = "src/config/config.yaml") -> Settings:
29
- with open(config_path, 'r') as file:
 
 
 
 
30
  raw_config = yaml.safe_load(file)
31
-
32
  return Settings(
33
- training=TrainingConfig(**raw_config.get("training", {})),
34
- data=DataConfig(**raw_config.get("data", {}))
 
 
 
 
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 GOESTripletDataset(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],
 
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 GOESTripletDataset
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=1e-3)
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 = GOESTripletDataset(data_dir=data_dir, augment=True)
74
  if len(dataset) == 0:
75
  logger.warning(f"No data found in {data_dir}. Skipping training chunk.")
76
  return
77
 
78
- dataloader = DataLoader(dataset, batch_size=self.settings.training.batch_size, shuffle=True, num_workers=2)
 
 
 
 
 
 
 
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.optimizer.step()
 
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
- from src.utils import setup_logging
6
 
7
  def test_load_settings(tmp_path):
8
  yaml_content = """
9
- training:
10
- epochs: 100
11
- batch_size: 4
12
- learning_rate: 0.0001
13
- checkpoints_dir: "/kaggle/working/checkpoints"
14
- load_model_path: "/kaggle/input/models/1week_model.pth"
15
-
16
- data:
17
- s3_bucket: "noaa-goes16"
18
- download_dir: "goes_data"
19
- year: 2024
20
- start_day: 264
21
- end_day: 294
22
- frame_step: 3
23
- crop_size: 512
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
- def test_setup_logging():
34
- logger = setup_logging(level=logging.DEBUG)
35
- assert logger.level == logging.DEBUG
36
- assert len(logger.handlers) > 0
 
 
 
 
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 pytest
3
- from src.data.transforms import augment_triplet
4
- from src.data.dataset import GOESTripletDataset
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 = GOESTripletDataset(str(tmp_path), augment=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  assert len(dataset) == 1
13
-
14
  img0, img1, gt = dataset[0]
15
- assert img0.shape == (1, 512, 512)
16
- assert gt.shape == (1, 512, 512)
17
-
18
- def test_augment_triplet():
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, batch_size=2, learning_rate=1e-4,
12
- checkpoints_dir="chkpt", load_model_path=""
 
 
 
 
13
  ),
14
  data=DataConfig(
15
- s3_bucket="b", download_dir="d", year=2024,
16
- start_day=200, end_day=201, frame_step=3, crop_size=256
 
 
 
 
 
 
 
17
  )
18
  )
19
- model = IFNet()
20
- device = torch.device("cpu")
21
- trainer = Trainer(settings, model, device)
 
 
 
 
 
22
 
23
- assert trainer.global_step == 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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