File size: 24,163 Bytes
80e070e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
# RockGlacierNet β€” Data Preprocessing Specification (Shapefile β†’ Training-Ready Dataset)

This is a **complete, step-by-step recipe** for turning one rock-glacier Shapefile (`.shp`)
into a machine-learning-ready dataset of aligned 12-band feature tensors and binary
segmentation masks.

It is written so that **someone with no prior knowledge of the project** can follow it
end to end. Every stage has:

1. **What it does** (objective)
2. **Inputs / Outputs** (exact files and folders)
3. **How to run it** (the reference script)
4. **βœ… TEST GATE** β€” a *mandatory* check that must pass before moving on.

> **Golden rule:** Never skip a TEST GATE. Each gate exists to catch a specific failure
> that would otherwise silently corrupt model training. If a gate fails, **stop and fix
> the previous stage** β€” do not proceed.

---

## 0. Setup (do this first)

### 0.1 Inputs you must have
- **One Shapefile** of rock-glacier polygons. A `.shp` never travels alone β€” make sure the
  sidecar files are in the same folder: `.shx`, `.dbf`, and ideally `.prj` and `.cpg`.
  - Example used to build the original dataset: `Final_merged01.shp` (626 polygons,
    Sikkim Himalayas, CRS `EPSG:4326`).
- **A Google Earth Engine (GEE) account** with a cloud project ID (needed in Phase 2 to
  download satellite + terrain features).

### 0.2 Software environment
Install these Python packages (the original project used a conda env; any env works):

```bash
pip install geopandas matplotlib rasterio earthengine-api geemap shapely numpy pandas
```

| Package | Used for |
| :--- | :--- |
| `geopandas`, `shapely` | reading/splitting the shapefile, reprojection |
| `rasterio` | rasterizing polygons β†’ masks, reading/writing GeoTIFFs |
| `earthengine-api`, `geemap` | downloading Sentinel-2 + DEM features from GEE |
| `numpy`, `pandas` | array math, patch extraction, reports |
| `matplotlib` | EDA plots and patch visualizations |

### 0.3 ⚠️ Fix the hard-coded paths first (critical)
The reference scripts were written on Windows and contain **hard-coded paths** like
`r"D:\RockGlacier_Project\..."`. Before running anything, open each script and change the
path constants at the top to **your** project root.

On this machine the project root is:
```
/data1/nasiruddink/rockglacier/RockGlacierNet
```

| Script | Constants to update |
| :--- | :--- |
| `EDA/explore_shapefile.py` | `SHAPEFILE_PATH`, `OUTPUT_DIR` |
| `EDA/separate_polygons.py` | `SHAPEFILE_PATH`, `OUTPUT_DIR`, `PLOTS_DIR` |
| `preprocess/generate_masks.py` | `INPUT_DIR`, `OUTPUT_MASK_DIR` |
| `preprocess/gee_feature_extraction.py` | `INPUT_DIR`, `OUTPUT_DIR`, GEE `project=` |
| `preprocess/harmonize_dataset.py` | `FEATURE_DIR`, `MASK_DIR`, `H_FEATURE_DIR`, `H_MASK_DIR` |
| `preprocess/split_dataset.py` | `H_FEATURE_DIR`, `H_MASK_DIR`, `DATASET_DIR` |
| `check_separability.py` | `H_FEATURE_DIR`, `H_MASK_DIR` |

> Tip: pick ONE root folder and keep the sub-folder names exactly as below so the scripts
> chain together without surprises.

### 0.4 Folder layout produced by this pipeline
```
RockGlacierNet/
β”œβ”€β”€ EDA/
β”‚   β”œβ”€β”€ individual_polygons/      # polygon_0.geojson … polygon_625.geojson
β”‚   └── individual_plots/         # first-5 sanity plots
β”œβ”€β”€ masks/                        # mask_0000.tif … (rasterized polygons) + mask_report.csv
β”œβ”€β”€ features/                     # feature_0000.tif … (12-band GEE stacks)
β”œβ”€β”€ preprocess/
β”‚   β”œβ”€β”€ harmonized_features/      # cropped to match masks pixel-for-pixel
β”‚   └── harmonized_masks/
β”œβ”€β”€ dataset/                      # train/val/test split of harmonized .tif pairs
β”‚   β”œβ”€β”€ train/{features,masks}/
β”‚   β”œβ”€β”€ val/{features,masks}/
β”‚   └── test/{features,masks}/
└── patches/                      # final 64Γ—64 .npy tensors fed to the model
    β”œβ”€β”€ train/{features,masks}/
    β”œβ”€β”€ val/{features,masks}/
    β”œβ”€β”€ test/{features,masks}/
    └── metadata/                 # config.json, patch_metadata.csv, dataset_summary.csv, diagnostics_report.txt
```

### 0.5 The 12 features (memorize this band order β€” everything depends on it)
Every `feature_*.tif` and every feature `.npy` has **exactly 12 channels in this order**:

| Band | Name | Source | Notes |
| :---: | :--- | :--- | :--- |
| 1 | `B2` Blue | Sentinel-2 SR | reflectance 0–1 |
| 2 | `B3` Green | Sentinel-2 SR | reflectance 0–1 |
| 3 | `B4` Red | Sentinel-2 SR | reflectance 0–1 |
| 4 | `B8` NIR | Sentinel-2 SR | reflectance 0–1 |
| 5 | `B11` SWIR-1 | Sentinel-2 SR | reflectance 0–1 |
| 6 | `B12` SWIR-2 | Sentinel-2 SR | reflectance 0–1 |
| 7 | `NDVI` | derived | `(B8βˆ’B4)/(B8+B4)`, range βˆ’1…1 |
| 8 | `NDWI` | derived | `(B3βˆ’B8)/(B3+B8)`, range βˆ’1…1 |
| 9 | `NDSI` | derived | `(B3βˆ’B11)/(B3+B11)`, range βˆ’1…1 |
| 10 | `Elevation` | Copernicus GLO-30 DEM | metres |
| 11 | `Slope` | derived from DEM | degrees 0–90 |
| 12 | `Aspect` | derived from DEM | degrees 0–360 |

**Mask convention:** `1 = rock glacier`, `0 = background`.

---

## Phase 1 β€” EDA & Polygon Separation

### Step 1. Inspect the Shapefile
- **Objective:** Confirm the file loads, see its CRS, feature count, and attributes before
  trusting it.
- **Run:** `EDA/explore_shapefile.py`
- **Outputs:** console summary + `rock_glaciers_plot.png` (overview map).

#### βœ… TEST GATE 1 β€” Shapefile sanity
Run this quick check (paste into a Python shell, edit the path):

```python
import geopandas as gpd
gdf = gpd.read_file("PATH/TO/Final_merged01.shp")
print("features:", len(gdf))
print("CRS:", gdf.crs)
print("geom types:", gdf.geom_type.value_counts().to_dict())
print("empty geoms:", gdf.geometry.is_empty.sum())
print("null geoms:", gdf.geometry.isna().sum())
print("invalid geoms:", (~gdf.is_valid).sum())
print("bbox:", gdf.total_bounds)
```

**PASS criteria:**
- [ ] `features` > 0 (original dataset: **626**).
- [ ] `CRS` is defined (original: `EPSG:4326`). If `None`, the file is unusable β€” get a
      version with a `.prj`.
- [ ] Geometry type is `Polygon` / `MultiPolygon`.
- [ ] `bbox` looks geographically plausible for your study area (original Sikkim bbox:
      `[88.07, 27.32] β†’ [88.93, 28.08]`).
- [ ] Note any empty/null/invalid geometries β€” these will be **skipped** later (expected and OK).

---

### Step 2. Split into individual polygons
- **Objective:** Break the one big shapefile into one GeoJSON per polygon so each can be
  processed independently. GeoJSON is used (not `.shp`) to avoid creating thousands of
  sidecar files.
- **Run:** `EDA/separate_polygons.py`
- **Output:** `EDA/individual_polygons/polygon_{idx}.geojson` (e.g. `polygon_0.geojson`).
  The numeric `idx` is the row index and becomes the **ID that links masks ↔ features**
  for the rest of the pipeline.

#### βœ… TEST GATE 2 β€” One GeoJSON per polygon
```python
import glob, geopandas as gpd
files = glob.glob("EDA/individual_polygons/polygon_*.geojson")
print("geojson files:", len(files))
g = gpd.read_file(files[0])
print("rows in one file:", len(g), "| CRS:", g.crs)   # expect 1 row, same CRS as source
```
**PASS criteria:**
- [ ] Number of GeoJSON files == number of features from Gate 1 (original: 626).
- [ ] Each file contains exactly **1** polygon and keeps the source CRS.

---

## Phase 2 β€” Mask & Feature Generation

### Step 3. Generate ground-truth masks
- **Objective:** Rasterize each polygon into a binary mask aligned to a fixed 10 m grid.
- **Run:** `preprocess/generate_masks.py`
- **Key parameters (do not change unless you also change them in Step 4):**
  - `TARGET_CRS = EPSG:32645` (UTM 45N β€” a metre-based CRS so padding is in real metres).
  - `RESOLUTION = 10.0` m/pixel (matches Sentinel-2 native resolution).
  - `PADDING = 200.0` m added on every side of the polygon's bounding box (gives the model
    surrounding background context, not just the glacier).
- **Outputs:** `masks/mask_{idx:04d}.tif` (single-band `uint8`, values {0,1}) and
  `masks/mask_report.csv` (per-mask width/height, foreground/background pixel counts, area).
- **Behavior to expect:** polygons with empty/NaN geometry are **skipped** (logged). This is
  why the final pair count (β‰ˆ607) can be lower than the polygon count (626).

#### βœ… TEST GATE 3 β€” Masks are valid binary rasters
```python
import glob, rasterio, numpy as np
bad = []
for f in sorted(glob.glob("masks/mask_*.tif")):
    with rasterio.open(f) as src:
        m = src.read(1)
        vals = set(np.unique(m).tolist())
        if not vals.issubset({0, 1}):        bad.append((f, "values", vals))
        if str(src.crs) != "EPSG:32645":     bad.append((f, "crs", str(src.crs)))
        if m.sum() == 0:                      bad.append((f, "empty-foreground", 0))
print("masks:", len(glob.glob('masks/mask_*.tif')), "| problems:", len(bad))
for b in bad[:10]: print("  βœ—", b)
```
**PASS criteria:**
- [ ] Every mask contains **only** values `{0, 1}`.
- [ ] Every mask CRS is `EPSG:32645`.
- [ ] No mask is entirely background (`m.sum() == 0`) β€” a fully-empty mask means the
      rasterization missed the polygon (CRS or geometry problem).
- [ ] Open `masks/mask_report.csv`: `Foreground_Pixels` > 0 for every row, and
      `Rasterized_Area_m2` should be in the same ballpark as `Polygon_Area_m2`.

---

### Step 4. Extract the 12-band feature stacks from Google Earth Engine
- **Objective:** For each polygon, download Sentinel-2 + terrain features over the **same
  padded bounding box** used for the mask, stacked into one 12-band GeoTIFF.
- **Run:** `preprocess/gee_feature_extraction.py` (set your GEE `project=` ID first).
- **How the features are built (must match the band table in Β§0.5):**
  - **Sentinel-2:** collection `COPERNICUS/S2_SR_HARMONIZED`, date range
    `2021-01-01 β†’ 2024-01-01`, pixel-level cloud mask via the `QA60` band (drops opaque +
    cirrus), reflectance scaled by `Γ·10000`, then a **median** composite (clear, summer-biased,
    cloud-free surface). Bands taken: `B2,B3,B4,B8,B11,B12`; indices computed: `NDVI,NDWI,NDSI`.
  - **Terrain:** `COPERNICUS/DEM/GLO30`. Slope & aspect are computed **before** mosaicking so
    the native 30 m projection is preserved, then `Elevation, Slope, Aspect` are added.
  - **Export:** `scale=10`, `crs=EPSG:32645`, `file_per_band=False` β†’ one 12-band
    `features/feature_{idx:04d}.tif`.
- **Behavior to expect:** the script **skips** indices whose `.tif` already exists (safe to
  re-run after interruptions) and **skips** empty geometries. GEE downloads can fail
  transiently β€” failures are logged; re-run to fill gaps.

#### βœ… TEST GATE 4 β€” Feature stacks are complete & well-formed
```python
import glob, rasterio, numpy as np
bad = []
for f in sorted(glob.glob("features/feature_*.tif")):
    with rasterio.open(f) as src:
        if src.count != 12:                 bad.append((f, "bands", src.count))
        if str(src.crs) != "EPSG:32645":    bad.append((f, "crs", str(src.crs)))
        if round(src.res[0]) != 10:         bad.append((f, "res", src.res))
        arr = src.read()
        # a band that is entirely no-data means GEE returned nothing here
        if np.all(~np.isfinite(arr)) or np.all(arr == 0): bad.append((f, "all-nodata", None))
print("features:", len(glob.glob('features/feature_*.tif')), "| problems:", len(bad))
for b in bad[:10]: print("  βœ—", b)
```
**PASS criteria:**
- [ ] Each feature file has **exactly 12 bands**, CRS `EPSG:32645`, ~10 m resolution.
- [ ] No feature stack is entirely no-data/zero.
- [ ] **Every mask has a matching feature** (same `idx`). Run the coverage check:
```python
import glob, os
m = {os.path.basename(p).split('_')[1].split('.')[0] for p in glob.glob('masks/mask_*.tif')}
f = {os.path.basename(p).split('_')[1].split('.')[0] for p in glob.glob('features/feature_*.tif')}
print("masks without features:", sorted(m - f)[:20])   # must be empty (or re-run GEE for these)
```
  - [ ] `masks - features` is empty (re-run the GEE script for any missing IDs).

> **Note on no-data:** GEE may write a sentinel value of `-9999` (or `NaN`) outside valid
> coverage. That is tolerated here β€” it is cleaned to `0` automatically during patch
> extraction (Step 7). The gate above only fails when a **whole** stack is no-data.

---

## Phase 3 β€” Harmonize, Split, and Patch

### Step 5. Harmonize feature/mask pixel grids
- **Objective:** GEE export and `rasterio` rasterization can differ by **one pixel** in
  height/width. This step crops each feature and its mask to their **common (min H, min W)**
  and copies the feature's geo-transform onto the mask so the pair is **pixel-perfect aligned**.
- **Run:** `preprocess/harmonize_dataset.py`
- **Outputs:** `preprocess/harmonized_features/feature_{idx}.tif`,
  `preprocess/harmonized_masks/mask_{idx}.tif`, and `harmonization_summary.csv`
  (records original shapes, harmonized shape, and CRS-match per pair).

#### βœ… TEST GATE 5 β€” Every pair is identically shaped & georeferenced
```python
import glob, os, rasterio
fdir, mdir = "preprocess/harmonized_features", "preprocess/harmonized_masks"
bad = []
for fp in sorted(glob.glob(f"{fdir}/feature_*.tif")):
    idx = os.path.basename(fp).split('_')[1].split('.')[0]
    mp = f"{mdir}/mask_{idx}.tif"
    if not os.path.exists(mp): bad.append((idx, "no-mask")); continue
    with rasterio.open(fp) as fs, rasterio.open(mp) as ms:
        if fs.shape != ms.shape:           bad.append((idx, "shape", fs.shape, ms.shape))
        if fs.crs != ms.crs:               bad.append((idx, "crs"))
        if fs.transform != ms.transform:   bad.append((idx, "transform"))
print("pairs:", len(glob.glob(f'{fdir}/feature_*.tif')), "| problems:", len(bad))
for b in bad[:10]: print("  βœ—", b)
```
**PASS criteria:**
- [ ] For **every** pair: identical `shape`, identical `crs`, identical `transform`.
- [ ] `0` problems printed. (Original harmonized set: **607** valid pairs.)

---

### Step 6. Split into train / val / test
- **Objective:** Randomly partition the harmonized pairs into **70 % train / 15 % val /
  15 % test**, copying matched feature+mask pairs into split folders.
- **Run:** `preprocess/split_dataset.py`
- **Reproducibility:** uses `random.seed(42)` β€” the split is deterministic. Re-running gives
  the same partition.
- **Outputs:** `dataset/{train,val,test}/{features,masks}/`.
  (Original split: train **424**, val **91**, test **92** = 607.)

#### βœ… TEST GATE 6 β€” Splits are matched and leak-free
```python
import glob, os
def ids(split, kind):
    return {os.path.basename(p).split('_')[1].split('.')[0]
            for p in glob.glob(f"dataset/{split}/{kind}/*.tif")}
sets = {s: (ids(s,'features'), ids(s,'masks')) for s in ['train','val','test']}
for s,(f,m) in sets.items():
    print(f"{s}: feat={len(f)} mask={len(m)} | feat-mask mismatch={f ^ m}")
tr, va, te = (sets['train'][0], sets['val'][0], sets['test'][0])
print("train∩val:", tr & va, "| train∩test:", tr & te, "| val∩test:", va & te)
```
**PASS criteria:**
- [ ] In each split, the feature ID set **equals** the mask ID set (no orphans).
- [ ] **No overlap** between any two splits (`train∩val`, `train∩test`, `val∩test` all empty).
      Leakage here inflates validation/test scores and is the most dangerous silent bug.
- [ ] `train + val + test` counts add up to the harmonized pair total.

---

### Step 7. Extract fixed-size patches
- **Objective:** Convert variable-sized `.tif` images into fixed **64Γ—64** `.npy` tensors the
  model can batch, while controlling class imbalance.
- **Config:** `preprocess/patch_config.py` (all knobs live here):
  - `patch_size = 64`, `stride = 32` (50 % overlap).
  - `background_keep_ratio = 0.25` β€” keep only ~25 % of pure-background patches (the rest are
    discarded to fight the ~4.5 : 1 background:glacier imbalance).
  - `minimum_glacier_percentage = 0.0` β€” keep **any** patch that contains at least one glacier
    pixel.
  - `padding_mode = "reflect"` β€” features are reflect-padded at the right/bottom edge so the
    sliding window covers the whole image; **masks are always zero-padded** (padding is
    background, never glacier).
  - `num_bands = 12`, `random_seed = 42`.
- **Run:** `python preprocess/extract_patches.py`
- **What it does internally:** reads each `.tif`, reorders to `(H, W, 12)` `float32`,
  **cleans no-data** (`-9999` and any non-finite β†’ `0.0`), slides the window, filters
  background patches by the keep-ratio, and saves kept patches as `.npy`.
- **Outputs:**
  - `patches/{split}/features/{id}_r{row}_c{col}.npy` β€” shape `(64, 64, 12)` `float32`.
  - `patches/{split}/masks/{id}_r{row}_c{col}.npy` β€” shape `(64, 64, 1)` `float32`.
  - `patches/metadata/config.json` (exact config used),
    `patch_metadata.csv` (per-patch stats + per-band mean/std),
    `dataset_summary.csv` (per-image stats),
    `diagnostics_report.txt` (human-readable summary: counts, class balance, coverage
    histograms, padding stats, expansion factor).
  - (Original result: train **2081**, val **454**, test **510** kept patches.)

#### βœ… TEST GATE 7 β€” Read the diagnostics before validating
Open `patches/metadata/diagnostics_report.txt` and confirm:
- [ ] **Image counts** per split match Step 6.
- [ ] **Pixel class balance** is sane (original β‰ˆ 4.5 : 1 background:glacier). If it is wildly
      imbalanced (e.g. 50 : 1), revisit `background_keep_ratio`.
- [ ] **Padding stats** are small β€” only tiny images should need padding. A high
      "patches with padding" fraction hints at many sub-64 px images (expected for small
      glaciers, but worth a glance).
- [ ] Kept-patch counts are non-zero for **all three** splits.

---

## Phase 4 β€” MANDATORY Dataset Validation (the safety net before training)

This is the most important phase for "nothing bad during training." Run **both** scripts
below and require a clean result. Each guards against a concrete failure mode.

### Step 8. Patch alignment & integrity validation
- **Run:** `python preprocess/validate_alignment.py`
- **What it checks (per split, every patch):**

| # | Check | Why it matters |
| :-: | :--- | :--- |
| 1 | **File-count match** features vs masks | a missing mask = an unlabeled training sample |
| 2 | **Name match** every `feature/<id>.npy` has `mask/<id>.npy` | mispaired tensors teach the wrong label |
| 3 | **Shape** features `(64,64,12)`, masks `(64,64,1)`, H/W equal | shape mismatch crashes the model or mislabels pixels |
| 4 | **Dtype** features `float32`; masks `float32/uint8/int32/int64` | wrong dtype breaks loss/normalization |
| 5 | **No NaN / Inf** in features | a single NaN turns the whole loss into NaN |
| 6 | **Mask values βŠ† {0, 1}** | any other value corrupts BCE/Dice loss & metrics |
| 7 | **Zero-bleed** glacier pixels are **not** all-zero across all 12 bands | catches padding/alignment errors where labels point at no-data |
| 8 | **Spot-check** prints shape/glacier%/min/max for 5 random patches/split | human eyeball sanity |

#### βœ… TEST GATE 8 β€” Alignment must be perfectly clean
**PASS criteria:** the script ends with
```
OVERALL: ALL CHECKS PASSED
```
- [ ] `Count match : OK` and `Name match : OK` for every split.
- [ ] `Shape errors : 0`, `NaN errors : 0`, `Inf errors : 0`.
- [ ] `Mask value : 0 errors`, `Zero-bleed : 0 patches`.
- [ ] Spot-check rows look reasonable: feature `min/max` finite, `glacier%` between 0–100.

> If **any** count is non-zero, the script prints the first 10 offending patch IDs. Fix the
> root cause (usually Step 5 harmonization or Step 7 extraction) and re-run β€” **do not**
> hand-delete patches to make the gate pass.

### Step 9. Trainability / separability report (recommended)
- **Run:** `python check_separability.py` (operates on the harmonized pairs).
- **Output:** `is_trainable.md` β€” global class balance + per-band mean/std for glacier vs
  background and **Cohen's d** effect size (how discriminative each feature is).
- **Use it to confirm the signal is real**, not just that files are well-formed:
  - [ ] Class imbalance is manageable (original β‰ˆ 4.5 : 1 β†’ fine for BCE + Dice loss).
  - [ ] At least a few bands show meaningful separation (original strongest: **Slope**
        dβ‰ˆ0.41, **NDSI**, **NDVI**, **SWIR-1**). If *every* effect size β‰ˆ 0, the features and
        labels may be misaligned β€” go back to Phase 2/3.

### Step 10. Visual spot-check (recommended)
- **Run:** `python preprocess/visualize_patches.py`
- **Outputs** in `outputs/patch_viz/`: `sample_grid_{split}.png` (RGB + red glacier overlay),
  `class_distribution.png`, `patch_size_distribution.png`, `band_histograms.png`.
- [ ] On the sample grids, the **red overlay should land on plausible glacier terrain**
      (debris tongues), not on random background β€” the ultimate human confirmation that
      features and masks line up.

---

## End-to-End Run Order (cheat sheet)

```bash
# Phase 1 β€” EDA & split
python EDA/explore_shapefile.py          # β†’ Gate 1
python EDA/separate_polygons.py          # β†’ Gate 2

# Phase 2 β€” masks & features
python preprocess/generate_masks.py      # β†’ Gate 3
python preprocess/gee_feature_extraction.py   # GEE auth needed β†’ Gate 4

# Phase 3 β€” harmonize, split, patch
python preprocess/harmonize_dataset.py   # β†’ Gate 5
python preprocess/split_dataset.py       # β†’ Gate 6
python preprocess/extract_patches.py     # β†’ Gate 7

# Phase 4 β€” MANDATORY validation
python preprocess/validate_alignment.py  # β†’ Gate 8 (must say ALL CHECKS PASSED)
python check_separability.py             # β†’ Gate 9 (is_trainable.md)
python preprocess/visualize_patches.py   # β†’ Gate 10 (eyeball outputs/patch_viz/)
```

**Only start model training after Gate 8 prints `ALL CHECKS PASSED`.**

---

## Troubleshooting β€” failure β†’ likely cause β†’ fix

| Symptom at gate | Likely cause | Fix |
| :--- | :--- | :--- |
| Gate 1: `CRS: None` | shapefile missing `.prj` | obtain the `.prj` sidecar or set CRS explicitly |
| Gate 3: mask all-background | polygon didn't rasterize (CRS/geometry) | check the polygon's GeoJSON validity; confirm reprojection to `EPSG:32645` |
| Gate 4: missing features for some masks | GEE download failed/skipped | re-run `gee_feature_extraction.py` (it skips existing, retries gaps) |
| Gate 4: band count β‰  12 | edited band list or export error | restore the band/index list in `get_s2_image` / `get_terrain_image` |
| Gate 5: shape/transform mismatch | ran split before harmonize | (re)run `harmonize_dataset.py`, then split |
| Gate 6: split overlap | re-split with different seed / mixed old files | clear `dataset/` and re-run `split_dataset.py` |
| Gate 8: NaN/Inf errors | no-data not cleaned | confirm Step 7 ran (it converts `-9999`/non-finite β†’ 0); re-extract |
| Gate 8: mask value errors | mask resampled with interpolation | masks must be nearest-neighbor / untouched `uint8` {0,1} |
| Gate 8: zero-bleed > 0 | feature/mask misaligned or padding over real labels | re-check Gate 5; ensure masks are zero-padded, never reflect-padded |
| Gate 9: all effect sizes β‰ˆ 0 | features ↔ masks mismatched | verify `idx` linkage from Step 2 onward |

---

## Notes for Automated Agents

- **Follow the phases linearly.** Each `python …` command corresponds to exactly one
  reference script; do not reorder.
- **Authenticate GEE before Phase 2** (`ee.Authenticate()` / set the cloud `project=`).
- **Treat every TEST GATE as a hard precondition** for the next stage. On failure, fix the
  *upstream* stage; never silently delete data to pass a gate.
- **The ID is sacred.** The integer `idx` assigned in Step 2 links `polygon_{idx}` β†’
  `mask_{idx}` β†’ `feature_{idx}` β†’ `dataset` β†’ `patches`. Never renumber or sort-rename files
  in a way that breaks this correspondence.
- **All geometry/config constants** (CRS `EPSG:32645`, 10 m, 200 m pad, 64-px patch, stride 32,
  seed 42, 12 bands) must stay identical between mask generation, feature extraction, and
  patching, or alignment breaks.
- **Definition of done:** `validate_alignment.py` prints `ALL CHECKS PASSED` and
  `is_trainable.md` reports a manageable class balance with non-trivial feature separability.
```