InĀ [1]:
import os
import time
import json
import numpy as np
import cv2
import rasterio
from rasterio.windows import from_bounds
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader, random_split
from scipy.ndimage import distance_transform_edt as distance
from huggingface_hub import hf_hub_download
import ee
import geemap
from google.colab import drive

# Mount Drive
drive.mount('/content/drive', force_remount=True)
os.system('pip install -q rasterio geopandas timm segmentation-models-pytorch huggingface_hub geedim')

# Authenticate GEE
try:
    ee.Initialize(project='[REDACTED_FOR_SECURITY]')
except:
    ee.Authenticate()
    ee.Initialize(project='[REDACTED_FOR_SECURITY]')

# Configuration
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Device: {device}")

if str(device) == 'cuda':
    print(f"GPU: {torch.cuda.get_device_name(0)}")
    torch.backends.cuda.matmul.allow_tf32 = True
    torch.backends.cudnn.allow_tf32 = True

# Paths and Hyperparameters
SAVE_DIR = '/content/drive/MyDrive/SatMAE_Advanced_Results/'
if not os.path.exists(SAVE_DIR): os.makedirs(SAVE_DIR)

BATCH_SIZE = 8
EPOCHS = 50
LR = 1e-4
PATCH_SIZE = 224
ASSET_ID = 'projects/[REDACTED_FOR_SECURITY]/assets/Punjab_Mask_2024_NEW'

TIME_WINDOWS = [
    ('2024-11-01', '2024-11-30'),
    ('2025-02-15', '2025-03-15'),
    ('2025-04-01', '2025-04-15')
]
Mounted at /content/drive
Device: cuda
GPU: Tesla T4
/usr/local/lib/python3.12/dist-packages/torch/backends/__init__.py:46: UserWarning: Please use the new API settings to control TF32 behavior, such as torch.backends.cudnn.conv.fp32_precision = 'tf32' or torch.backends.cuda.matmul.fp32_precision = 'ieee'. Old settings, e.g, torch.backends.cuda.matmul.allow_tf32 = True, torch.backends.cudnn.allow_tf32 = True, allowTF32CuDNN() and allowTF32CuBLAS() will be deprecated after Pytorch 2.9. Please see https://pytorch.org/docs/main/notes/cuda.html#tensorfloat-32-tf32-on-ampere-and-later-devices (Triggered internally at /pytorch/aten/src/ATen/Context.cpp:80.)
  self.setter(val)
InĀ [2]:
def get_satmae_data():
    print("Ingesting Asset...")
    mask_img = ee.Image(ASSET_ID)
    roi_geom = mask_img.geometry()
    mask_file = 'local_mask.tif'

    if not os.path.exists(mask_file):
        geemap.download_ee_image(mask_img, mask_file, region=roi_geom, scale=10, crs='EPSG:4326', overwrite=True)

    print("Processing Subset...")
    with rasterio.open(mask_file) as src:
        b = src.bounds
        cx, cy = (b.left + b.right)/2, (b.bottom + b.top)/2
        offset = 0.06
        window = from_bounds(cx-offset, cy-offset, cx+offset, cy+offset, src.transform)
        mask = src.read(1, window=window)
        mask = np.where(mask > 0, 1.0, 0.0).astype(np.float32)
        small_roi = ee.Geometry.Rectangle([cx-offset, cy-offset, cx+offset, cy+offset], proj=str(src.crs), geodesic=False)
        target_h, target_w = mask.shape

    stack = []
    print("Stacking 3 Time Steps...")
    for i, (start, end) in enumerate(TIME_WINDOWS):
        fname = f'time_{i}.tif'
        if not os.path.exists(fname):
            s2 = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED').filterBounds(small_roi).filterDate(start, end).median().select(['B2','B3','B4','B8','B11','B12'])
            s1 = ee.ImageCollection('COPERNICUS/S1_GRD').filterBounds(small_roi).filterDate(start, end).mean().select(['VV','VH'])
            fused = ee.Image.cat([s2, s1]).clip(small_roi)
            geemap.download_ee_image(fused, fname, region=small_roi, scale=10, crs='EPSG:4326', overwrite=True)

        with rasterio.open(fname) as src:
            arr = src.read()
            arr = np.transpose(arr, (1, 2, 0))
            if arr.shape[:2] != (target_h, target_w):
                arr = cv2.resize(arr, (target_w, target_h), interpolation=cv2.INTER_LINEAR)

            s2_n = np.clip(arr[:,:,:6] / 5000.0, 0, 1)
            s1_n = np.clip((arr[:,:,6:] - (-25.0)) / (0.0 - (-25.0)), 0, 1)
            stack.append(np.concatenate([s2_n, s1_n], axis=2))

    full_cube = np.stack(stack, axis=2)

    print("Tiling...")
    x_out, y_out = [], []
    stride = PATCH_SIZE
    for y in range(0, target_h, stride):
        for x in range(0, target_w, stride):
            img_p = full_cube[y:y+stride, x:x+stride]
            mask_p = mask[y:y+stride, x:x+stride]
            if img_p.shape[0] != PATCH_SIZE or img_p.shape[1] != PATCH_SIZE: continue
            if np.min(img_p) < 0: continue
            x_out.append(img_p)
            y_out.append(mask_p)

    if len(x_out) == 0: raise ValueError("No valid patches found.")

    X = np.array(x_out, dtype=np.float32).transpose(0, 4, 3, 1, 2)
    y = np.array(y_out, dtype=np.float32)[:, None, :, :]

    print(f"Data Ready. Shape: {X.shape}")
    return torch.tensor(X), torch.tensor(y)

X_data, y_data = get_satmae_data()
Ingesting Asset...
/usr/local/lib/python3.12/dist-packages/geemap/common.py:12471: FutureWarning: 'BaseImage' is deprecated and will be removed in a future release.  Please use the 'ee.Image.gd' accessor instead.
  img = gd.download.BaseImage(image)
...tmae-2026/assets/Punjab_Mask_2024_NEW:   0%|          |0/585 tiles [00:00<?]
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:googleapiclient.http:Sleeping 0.49 seconds before retry 1 of 5 for request: POST https://earthengine.googleapis.com/v1/projects/satmae-2026/thumbnails?fields=name&alt=json, after 429
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:googleapiclient.http:Sleeping 1.30 seconds before retry 1 of 5 for request: POST https://earthengine.googleapis.com/v1/projects/satmae-2026/thumbnails?fields=name&alt=json, after 429
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
/usr/local/lib/python3.12/dist-packages/geedim/image.py:254: RuntimeWarning: Couldn't find STAC entry for: 'projects/satmae-2026/assets/Punjab_Mask_2024_NEW'.
  return STACClient().get(self.id)
Processing Subset...
Stacking 3 Time Steps...
  0%|          |0/48 tiles [00:00<?]
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
/usr/local/lib/python3.12/dist-packages/geedim/image.py:254: RuntimeWarning: Couldn't find STAC entry for: 'None'.
  return STACClient().get(self.id)
  0%|          |0/48 tiles [00:00<?]
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:googleapiclient.http:Sleeping 1.86 seconds before retry 1 of 5 for request: POST https://earthengine.googleapis.com/v1/projects/satmae-2026/thumbnails?fields=name&alt=json, after 429
WARNING:googleapiclient.http:Sleeping 0.67 seconds before retry 1 of 5 for request: POST https://earthengine.googleapis.com/v1/projects/satmae-2026/thumbnails?fields=name&alt=json, after 429
WARNING:googleapiclient.http:Sleeping 3.84 seconds before retry 2 of 5 for request: POST https://earthengine.googleapis.com/v1/projects/satmae-2026/thumbnails?fields=name&alt=json, after 429
  0%|          |0/48 tiles [00:00<?]
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
WARNING:urllib3.connectionpool:Connection pool is full, discarding connection: earthengine.googleapis.com. Connection pool size: 10
Tiling...
Data Ready. Shape: (25, 8, 3, 224, 224)
InĀ [3]:
class DiceLoss(nn.Module):
    def __init__(self, smooth=1e-6):
        super(DiceLoss, self).__init__()
        self.smooth = smooth

    def forward(self, inputs, targets):
        inputs = torch.sigmoid(inputs)
        inputs = inputs.view(-1)
        targets = targets.view(-1)

        intersection = (inputs * targets).sum()
        dice = (2. * intersection + self.smooth) / (inputs.sum() + targets.sum() + self.smooth)

        return 1 - dice

class HausdorffDTLoss(nn.Module):
    def __init__(self, alpha=2.0):
        super().__init__()
        self.alpha = alpha

    def forward(self, pred, gt):
        with torch.no_grad():
            gt_np = gt.cpu().numpy()
            dist_map = np.zeros_like(gt_np)
            for i in range(len(gt_np)):
                mask = gt_np[i, 0]
                if mask.sum() == 0: continue
                d_in = distance(mask)
                d_out = distance(1 - mask)
                dist_map[i, 0] = (d_out - d_in)

            dist_map = torch.tensor(dist_map, device=pred.device, dtype=torch.float32)

        probs = torch.sigmoid(pred)
        loss = torch.mean((probs - gt) ** 2 * (1 + self.alpha * torch.abs(dist_map)))
        return loss

class CompositeLoss(nn.Module):
    def __init__(self):
        super().__init__()
        self.dice = DiceLoss()
        self.hd = HausdorffDTLoss(alpha=2.0)
        self.bce = nn.BCEWithLogitsLoss()

    def forward(self, preds, targets):
        return 0.4*self.dice(preds, targets) + 0.4*self.bce(preds, targets) + 0.2*self.hd(preds, targets)
InĀ [4]:
class FlashAttentionBlock(nn.Module):
    def __init__(self, dim, num_heads):
        super().__init__()
        self.num_heads = num_heads
        self.head_dim = dim // num_heads
        self.scale = self.head_dim ** -0.5

        self.qkv = nn.Linear(dim, dim * 3, bias=True)
        self.proj = nn.Linear(dim, dim)
        self.norm1 = nn.LayerNorm(dim)
        self.norm2 = nn.LayerNorm(dim)
        self.mlp = nn.Sequential(
            nn.Linear(dim, dim * 4),
            nn.GELU(),
            nn.Linear(dim * 4, dim)
        )

    def forward(self, x):
        B, N, C = x.shape
        qkv = self.qkv(self.norm1(x)).reshape(B, N, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)
        q, k, v = qkv[0], qkv[1], qkv[2]

        with torch.backends.cuda.sdp_kernel(enable_flash=True, enable_math=False, enable_mem_efficient=True):
             attn = F.scaled_dot_product_attention(q, k, v)

        attn = attn.transpose(1, 2).reshape(B, N, C)
        x = x + self.proj(attn)
        x = x + self.mlp(self.norm2(x))
        return x

class SatMAEPlusPlus_Encoder(nn.Module):
    def __init__(self, num_frames=3, in_chans=8, embed_dim=768, depth=12, num_heads=12):
        super().__init__()
        self.patch_embed = nn.Conv2d(in_chans, embed_dim, kernel_size=16, stride=16)
        self.pos_embed = nn.Parameter(torch.zeros(1, 1, 196+1, embed_dim))
        self.time_embed = nn.Parameter(torch.zeros(1, num_frames, 1, embed_dim))
        self.cls_token = nn.Parameter(torch.zeros(1, 1, 1, embed_dim))

        self.blocks = nn.ModuleList([
            FlashAttentionBlock(embed_dim, num_heads) for _ in range(depth)
        ])
        self.norm = nn.LayerNorm(embed_dim)

    def forward(self, x):
        B, C, T, H, W = x.shape
        x = x.permute(0, 2, 1, 3, 4).reshape(B * T, C, H, W)

        x = self.patch_embed(x).flatten(2).transpose(1, 2)
        x = x.reshape(B, T, -1, 768)

        x = x + self.time_embed
        x = x.reshape(B, T*196, 768)
        pos = self.pos_embed[:, :, 1:, :].expand(B, T, -1, -1).reshape(B, T*196, 768)
        x = x + pos

        cls = self.cls_token.expand(B, -1, -1, -1).reshape(B, 1, 768) + self.pos_embed[:, :, 0, :].expand(B, 1, 768)
        x = torch.cat((cls, x), dim=1)

        for blk in self.blocks:
            x = blk(x)
        x = self.norm(x)

        return x

class RefinementBlock(nn.Module):
    def __init__(self, in_c, out_c):
        super().__init__()
        self.block = nn.Sequential(
            nn.Upsample(scale_factor=2, mode='bilinear', align_corners=False),
            nn.Conv2d(in_c, out_c, kernel_size=3, padding=1),
            nn.BatchNorm2d(out_c),
            nn.GELU(),
            nn.Conv2d(out_c, out_c, kernel_size=3, padding=1),
            nn.BatchNorm2d(out_c),
            nn.GELU()
        )
    def forward(self, x):
        return self.block(x)

class FeaturePyramidDecoder(nn.Module):
    def __init__(self, embed_dim, num_frames):
        super().__init__()
        self.temporal_fuse = nn.Conv2d(embed_dim * num_frames, embed_dim, kernel_size=1)

        self.stage1 = RefinementBlock(embed_dim, 256)
        self.stage2 = RefinementBlock(256, 128)
        self.stage3 = RefinementBlock(128, 64)
        self.stage4 = RefinementBlock(64, 32)

        self.final_head = nn.Conv2d(32, 1, kernel_size=1)

    def forward(self, x, B, T):
        x = x[:, 1:, :]
        x = x.reshape(B, T, 14, 14, 768).permute(0, 1, 4, 2, 3).reshape(B, T*768, 14, 14)
        x = self.temporal_fuse(x)
        x = self.stage1(x)
        x = self.stage2(x)
        x = self.stage3(x)
        x = self.stage4(x)
        return self.final_head(x)

class SatMAE_PlusPlus(nn.Module):
    def __init__(self):
        super().__init__()
        print("Assembling SatMAE++")
        self.encoder = SatMAEPlusPlus_Encoder()
        self.decoder = FeaturePyramidDecoder(embed_dim=768, num_frames=3)

        try:
            print("Loading Google ViT Weights...")
            p = hf_hub_download("google/vit-base-patch16-224", "pytorch_model.bin")
            sd = torch.load(p, map_location='cpu')

            w = sd['vit.embeddings.patch_embeddings.projection.weight']
            new_w = torch.zeros(768, 8, 16, 16)
            new_w[:, :3] = w
            new_w[:, 3:] = w.mean(1, keepdim=True).repeat(1, 5, 1, 1)
            self.encoder.patch_embed.weight.data = new_w
            self.encoder.patch_embed.bias.data = sd['vit.embeddings.patch_embeddings.projection.bias']

            for i in range(12):
                prefix = f'vit.encoder.layer.{i}.'
                blk = self.encoder.blocks[i]

                q = sd[prefix + 'attention.attention.query.weight']
                k = sd[prefix + 'attention.attention.key.weight']
                v = sd[prefix + 'attention.attention.value.weight']
                qb = sd[prefix + 'attention.attention.query.bias']
                kb = sd[prefix + 'attention.attention.key.bias']
                vb = sd[prefix + 'attention.attention.value.bias']

                blk.qkv.weight.data = torch.cat([q, k, v], dim=0)
                blk.qkv.bias.data = torch.cat([qb, kb, vb], dim=0)

                blk.proj.weight.data = sd[prefix + 'attention.output.dense.weight']
                blk.proj.bias.data = sd[prefix + 'attention.output.dense.bias']
                blk.norm1.weight.data = sd[prefix + 'layernorm_before.weight']
                blk.norm1.bias.data = sd[prefix + 'layernorm_before.bias']
                blk.norm2.weight.data = sd[prefix + 'layernorm_after.weight']
                blk.norm2.bias.data = sd[prefix + 'layernorm_after.bias']
                blk.mlp[0].weight.data = sd[prefix + 'intermediate.dense.weight']
                blk.mlp[0].bias.data = sd[prefix + 'intermediate.dense.bias']
                blk.mlp[2].weight.data = sd[prefix + 'output.dense.weight']
                blk.mlp[2].bias.data = sd[prefix + 'output.dense.bias']

            print("Weights Adapted and Loaded Successfully.")
        except Exception as e:
            print(f"Weight loading failed: {e}. Using Random Init.")

        for p in self.encoder.parameters(): p.requires_grad = False
        self.encoder.patch_embed.weight.requires_grad = True
        self.encoder.time_embed.requires_grad = True
        for p in self.encoder.blocks[-2:].parameters(): p.requires_grad = True

    def forward(self, x):
        B, C, T, H, W = x.shape
        features = self.encoder(x)
        return self.decoder(features, B, T)
InĀ [5]:
model = SatMAE_PlusPlus().to(device)
optimizer = optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=LR)
criterion = CompositeLoss()

ds = TensorDataset(X_data, y_data)
tr_sz = int(0.85 * len(ds))
train_ds, val_ds = random_split(ds, [tr_sz, len(ds)-tr_sz])
train_loader = DataLoader(train_ds, BATCH_SIZE, shuffle=True)
val_loader = DataLoader(val_ds, BATCH_SIZE, shuffle=False)

print(f"Starting Training ({EPOCHS} Epochs)...")
history = []

for ep in range(EPOCHS):
    model.train()
    train_loss = 0

    for x, y in train_loader:
        x, y = x.to(device), y.to(device)
        optimizer.zero_grad()

        with torch.cuda.amp.autocast():
            preds = model(x)
            loss = criterion(preds, y)

        loss.backward()
        optimizer.step()
        train_loss += loss.item()

    model.eval()
    val_loss = 0
    with torch.no_grad():
        for x, y in val_loader:
            x, y = x.to(device), y.to(device)
            preds = model(x)
            val_loss += criterion(preds, y).item()

    avg_t = train_loss / len(train_loader)
    avg_v = val_loss / len(val_loader)
    history.append((avg_t, avg_v))

    if (ep+1) % 5 == 0:
        print(f"Ep {ep+1} | Train Loss: {avg_t:.4f} | Val Loss: {avg_v:.4f}")

torch.save(model.state_dict(), SAVE_DIR + "SatMAE_PlusPlus_Final.pth")
print("Training Complete and Model Saved.")
Assembling SatMAE++
Loading Google ViT Weights...
/usr/local/lib/python3.12/dist-packages/huggingface_hub/utils/_auth.py:94: UserWarning: 
The secret `HF_TOKEN` does not exist in your Colab secrets.
To authenticate with the Hugging Face Hub, create a token in your settings tab (https://huggingface.co/settings/tokens), set it as secret in your Google Colab and restart your session.
You will be able to reuse this secret in all of your notebooks.
Please note that authentication is recommended but still optional to access public models or datasets.
  warnings.warn(
pytorch_model.bin:   0%|          | 0.00/346M [00:00<?, ?B/s]
Weights Adapted and Loaded Successfully.
Starting Training (50 Epochs)...
/tmp/ipython-input-3025469134.py:22: FutureWarning: `torch.cuda.amp.autocast(args...)` is deprecated. Please use `torch.amp.autocast('cuda', args...)` instead.
  with torch.cuda.amp.autocast():
/usr/lib/python3.12/contextlib.py:105: FutureWarning: `torch.backends.cuda.sdp_kernel()` is deprecated. In the future, this context manager will be removed. Please see `torch.nn.attention.sdpa_kernel()` for the new context manager, with updated signature.
  self.gen = func(*args, **kwds)
Ep 5 | Train Loss: 0.8669 | Val Loss: 1.0325
Ep 10 | Train Loss: 0.6625 | Val Loss: 0.7839
Ep 15 | Train Loss: 0.5679 | Val Loss: 0.6473
Ep 20 | Train Loss: 0.5182 | Val Loss: 0.5999
Ep 25 | Train Loss: 0.4840 | Val Loss: 0.5945
Ep 30 | Train Loss: 0.4643 | Val Loss: 0.5735
Ep 35 | Train Loss: 0.4482 | Val Loss: 0.5551
Ep 40 | Train Loss: 0.4300 | Val Loss: 0.5676
Ep 45 | Train Loss: 0.4242 | Val Loss: 0.5430
Ep 50 | Train Loss: 0.3946 | Val Loss: 0.5571
Training Complete and Model Saved.
InĀ [6]:
import matplotlib.pyplot as plt

def plot_training_history(history):
    train_loss = [h[0] for h in history]
    val_loss = [h[1] for h in history]
    epochs = range(1, len(history) + 1)

    plt.figure(figsize=(10, 5))
    plt.plot(epochs, train_loss, label='Training Loss', color='blue')
    plt.plot(epochs, val_loss, label='Validation Loss', color='orange', linestyle='--')
    plt.title('SatMAE++ Training Progress')
    plt.xlabel('Epochs')
    plt.ylabel('Composite Loss')
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.show()

def visualize_predictions(model, loader, device, num_samples=3):
    model.eval()
    x_batch, y_batch = next(iter(loader))
    x_batch, y_batch = x_batch.to(device), y_batch.to(device)

    with torch.no_grad():
        logits = model(x_batch)
        preds = (torch.sigmoid(logits) > 0.5).float().cpu()

    # Move inputs/targets to CPU for plotting
    x_batch = x_batch.cpu()
    y_batch = y_batch.cpu()

    fig, axes = plt.subplots(num_samples, 3, figsize=(12, 4 * num_samples))
    cols = ["Input (RGB - Peak Season)", "Ground Truth", "Prediction"]

    # Set column titles
    for ax, col in zip(axes[0], cols):
        ax.set_title(col, fontweight='bold')

    for i in range(num_samples):
        # Extract RGB from Time Step 1 (Peak Growth)
        # Data is (C, T, H, W) -> (8, 3, 224, 224)
        # Indices: 0=B2(Blue), 1=B3(Green), 2=B4(Red)
        # We need [2, 1, 0] for RGB
        rgb = x_batch[i, [2, 1, 0], 1, :, :].permute(1, 2, 0).numpy()

        # Brighten image for display (Inputs were normalized by 5000)
        rgb = np.clip(rgb * 3.5, 0, 1)

        # Plot Input
        axes[i, 0].imshow(rgb)
        axes[i, 0].axis('off')

        # Plot Ground Truth
        axes[i, 1].imshow(y_batch[i, 0], cmap='gray')
        axes[i, 1].axis('off')

        # Plot Prediction
        axes[i, 2].imshow(preds[i, 0], cmap='gray')
        axes[i, 2].axis('off')

        # Calculate Sample IoU
        intersection = (preds[i] * y_batch[i]).sum()
        union = preds[i].sum() + y_batch[i].sum() - intersection
        iou = intersection / (union + 1e-6)
        axes[i, 2].text(5, 20, f"IoU: {iou:.2f}", color='white', fontweight='bold', bbox=dict(facecolor='black', alpha=0.5))

    plt.tight_layout()
    plt.show()

# Execute Visualization
if len(history) > 0:
    plot_training_history(history)
    visualize_predictions(model, val_loader, device)
else:
    print("No training history found. Run training cell first.")
No description has been provided for this image
No description has been provided for this image
InĀ [7]:
from sklearn.metrics import accuracy_score, f1_score, jaccard_score, precision_score, recall_score, confusion_matrix
import pandas as pd

def evaluate_and_save_metrics(model, loader, device, save_dir, model_name="SatMAE_PlusPlus"):
    print(f"Calculating Metrics for {model_name}...")
    model.eval()

    all_preds = []
    all_targets = []

    # Inference Loop
    start_time = time.time()
    with torch.no_grad():
        for x, y in loader:
            x = x.to(device)
            logits = model(x)
            probs = torch.sigmoid(logits)

            # Threshold at 0.5
            preds = (probs > 0.5).float().cpu().numpy().flatten()
            targets = y.cpu().numpy().flatten()

            all_preds.extend(preds)
            all_targets.extend(targets)

    end_time = time.time()
    total_time = end_time - start_time
    fps = len(loader.dataset) / (total_time + 1e-6)

    # Convert to integer arrays for metric calculation
    y_pred = np.array(all_preds).astype(int)
    y_true = np.array(all_targets).astype(int)

    # Calculate Metrics
    metrics = {
        "IoU": round(jaccard_score(y_true, y_pred, average='binary'), 4),
        "F1_Score": round(f1_score(y_true, y_pred, average='binary'), 4),
        "Pixel_Accuracy": round(accuracy_score(y_true, y_pred), 4),
        "Precision": round(precision_score(y_true, y_pred, average='binary'), 4),
        "Recall": round(recall_score(y_true, y_pred, average='binary'), 4),
        "FPS": round(fps, 2),
        "Inference_Time_Sec": round(total_time, 2)
    }

    # Confusion Matrix
    tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
    metrics["Confusion_Matrix"] = {
        "TP": int(tp), "FP": int(fp),
        "FN": int(fn), "TN": int(tn)
    }

    # Print Report
    print("\n--- FINAL PERFORMANCE REPORT ---")
    print(f"IoU Score:        {metrics['IoU']}")
    print(f"F1 Score:         {metrics['F1_Score']}")
    print(f"Precision:        {metrics['Precision']}")
    print(f"Recall:           {metrics['Recall']}")
    print(f"Inference Speed:  {metrics['FPS']} FPS")
    print("-" * 30)

    # Save Metrics to JSON
    json_path = f"{save_dir}{model_name}_Metrics.json"
    with open(json_path, 'w') as f:
        json.dump(metrics, f, indent=4)
    print(f"Metrics saved to: {json_path}")

    # Save History to CSV
    if 'history' in globals() and len(history) > 0:
        csv_path = f"{save_dir}{model_name}_History.csv"
        df = pd.DataFrame(history, columns=['Train_Loss', 'Val_Loss'])
        df['Epoch'] = range(1, len(history) + 1)
        df.to_csv(csv_path, index=False)
        print(f"History saved to: {csv_path}")

# Run Evaluation
evaluate_and_save_metrics(model, val_loader, device, SAVE_DIR)
Calculating Metrics for SatMAE_PlusPlus...
/usr/lib/python3.12/contextlib.py:105: FutureWarning: `torch.backends.cuda.sdp_kernel()` is deprecated. In the future, this context manager will be removed. Please see `torch.nn.attention.sdpa_kernel()` for the new context manager, with updated signature.
  self.gen = func(*args, **kwds)
--- FINAL PERFORMANCE REPORT ---
IoU Score:        0.7165
F1 Score:         0.8348
Precision:        0.8437
Recall:           0.8262
Inference Speed:  15.84 FPS
------------------------------
Metrics saved to: /content/drive/MyDrive/SatMAE_Advanced_Results/SatMAE_PlusPlus_Metrics.json
History saved to: /content/drive/MyDrive/SatMAE_Advanced_Results/SatMAE_PlusPlus_History.csv