File size: 20,013 Bytes
c2a61b6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
# CODEBASE β€” Detailed Code Explanations

This section provides comprehensive explanations of the most critical code modules in the ATMOS project, covering the implementation of all four improvements, data processing, and API serving.

---

## 1. Model Downscaler Module (`src/models/downscaler.py`)

### 1.1 DownscalerModel β€” Single Swin2SR Wrapper

```python

class DownscalerModel(nn.Module):

    def __init__(self, hf_model, scale: int = 4):

        super().__init__()

        self.model = hf_model

        self.scale = scale



    def forward(self, x: torch.Tensor) -> torch.Tensor:

        B, C, H, W = x.shape

        th, tw = H * self.scale, W * self.scale

        

        # Convert single-channel to RGB (Swin2SR expects 3 channels)

        x3 = x.repeat(1, 3, 1, 1)

        

        # Normalize to [0,1] range for model input

        lo, hi = x3.min(), x3.max()

        x_norm = (x3 - lo) / (hi - lo + 1e-8)

        

        # Run inference

        with torch.no_grad():

            out = self.model(pixel_values=x_norm)

        

        # Extract prediction and denormalize

        pred = out.reconstruction if hasattr(out, "reconstruction") else out[0]

        pred = pred[:, 0:1, :, :]  # Take first channel only

        pred = pred * (hi - lo) + lo

        

        # Ensure correct output shape

        if pred.shape[2:] != (th, tw):

            pred = F.interpolate(pred, size=(th, tw), mode="bilinear")

        return pred

```

**Explanation:** Wraps HuggingFace Swin2SR to handle ERA5 (single-channel z-score) ↔ RGB format conversion. Key steps: (1) replicate channel 3Γ—, (2) normalize to [0,1], (3) inference, (4) extract first channel, (5) denormalize back to z-score.

---

### 1.2 EnsembleDownscaler β€” IMPROVEMENT 1

```python

class EnsembleDownscaler(nn.Module):

    def __init__(self, model_rw: DownscalerModel, model_cl: DownscalerModel):

        super().__init__()

        self.rw = model_rw  # Realworld (BSRGAN-PSNR)

        self.cl = model_cl  # Classical (bicubic)



    def forward(self, x: torch.Tensor) -> torch.Tensor:

        with torch.no_grad():

            pred_rw = self.rw(x)

            pred_cl = self.cl(x)

        return (pred_rw + pred_cl) * 0.5  # Pixel-wise average

```

**Explanation:** Implements Improvement 1 β€” ensemble averaging. Realworld model excels at texture, classical model preserves smooth gradients. Averaging cancels each model's noise while keeping real structure. **Result:** Sharpness gain doubles from ~10% (single model) to +21.3% (ensemble).

---

### 1.3 INT8 Dynamic Quantization

```python

def _apply_int8(model: nn.Module) -> nn.Module:

    n = sum(1 for m in model.modules() if isinstance(m, nn.Linear))

    try:

        with warnings.catch_warnings():

            warnings.simplefilter("ignore")

            q = torch.quantization.quantize_dynamic(

                model, {nn.Linear}, dtype=torch.qint8)

        print(f"  INT8: {n} Linear layers quantised")

        return q

    except Exception as e:

        print(f"  INT8 skipped ({e})")

        return model

```

**Explanation:** Quantizes all 288 Linear layers (attention + MLP) from float32 (4 bytes) to INT8 (1 byte). **Impact:** Memory 97MB β†’ 24MB (4Γ— reduction), ~10-15% faster inference, negligible accuracy loss (<0.1%).

---

### 1.4 PhysicsPreprocessor β€” IMPROVEMENT 2

```python

class PhysicsPreprocessor:

    def __init__(self, data: np.ndarray):

        # Compute temporal mean across all 8784 timesteps

        self.mean_field = data.mean(axis=0)  # (H, W)

        self.std_anom = (data - self.mean_field).std()



    def to_anomaly(self, grid: np.ndarray) -> np.ndarray:

        return grid - self.mean_field  # Subtract mean



    def from_anomaly(self, anom_pred: np.ndarray, 

                     era5_mean_upsampled: np.ndarray) -> np.ndarray:

        return anom_pred + era5_mean_upsampled  # Add mean back

```

**Explanation:** Implements Improvement 2 β€” anomaly-based inference. ERA5 India spans ~70K range (232K–302K) with strong north-south gradient. Subtracting temporal mean converts absolute temps to anomalies (~Β±2K, std=0.46z). Model focuses on fine-scale structure, not large-scale gradient. **Impact:** Sharpness +15.2% β†’ +21.3%, PSD gain +3.1dB β†’ +4.58dB.

---

### 1.5 ElevationCorrector β€” IMPROVEMENT 3

```python

class ElevationCorrector:

    LAPSE_RATE = 6.5 / 1000.0  # K/m



    def __init__(self, mean_field_K: np.ndarray, output_shape: tuple):

        from scipy.ndimage import zoom as spz, gaussian_filter

        

        # Laplacian of mean field β†’ cold spots = mountains

        lap = np.gradient(np.gradient(mean_field_K, axis=0), axis=0) + \

              np.gradient(np.gradient(mean_field_K, axis=1), axis=1)

        

        # Build DEM proxy: smooth, invert, normalize to 0-3000m

        dem_lr = gaussian_filter(-lap, sigma=1.5)

        dem_lr = np.clip(dem_lr, 0, None)

        dem_lr = dem_lr / (dem_lr.max() + 1e-6) * 3000.0

        

        # Upsample to HR and compute elevation difference

        f = output_shape[0] / mean_field_K.shape[0]

        dem_hr = spz(dem_lr, f, order=3)[:output_shape[0], :output_shape[1]]

        dem_lr_up = spz(dem_lr, f, order=1)[:output_shape[0], :output_shape[1]]

        

        self.delta_dem = dem_hr - dem_lr_up

        self.correction_K = self.delta_dem * self.LAPSE_RATE



    def apply(self, pred_K: np.ndarray) -> np.ndarray:

        return pred_K + self.correction_K

```

**Explanation:** Implements Improvement 3 β€” terrain-aware lapse rate correction. ERA5 averages over elevation within each 28km cell, causing cold bias at mountains. Uses Laplacian to build DEM proxy (cold anomalies = high terrain), applies 6.5K/1000m correction. **Impact:** Correction range βˆ’1.09K to +1.67K, fixes Himalayan/Western Ghats bias (+1.5K at Srinagar).

---

## 2. Land-Sea Mask Module (`src/models/land_mask.py`)



### 2.1 build_land_mask β€” IMPROVEMENT 4



```python

def build_land_mask(data: np.ndarray, lat_min: float, lat_max: float,

                    lon_min: float, lon_max: float, output_scale: int = 4):
    T, H, W = data.shape

    

    # Method 1: Temporal variance threshold

    std_map = data.std(axis=0)  # Ocean=low variance, Land=high variance

    thresh = (std_map.min() + np.percentile(std_map, 40)) / 2

    land_mask_lr = std_map > thresh

    land_mask_lr = binary_dilation(land_mask_lr, iterations=1)

    

    # Method 2: Hard-code known ocean boxes

    lats = np.linspace(lat_max, lat_min, H)

    lons = np.linspace(lon_min, lon_max, W)

    ocean_boxes = [

        (38.0, 6.0, 68.0, 71.0),   # Arabian Sea

        (12.0, 6.0, 71.0, 79.0),   # Indian Ocean

        (22.0, 6.0, 88.0, 98.0),   # Bay of Bengal

    ]

    for ln, ls, lw, le in ocean_boxes:

        for r in range(H):

            for c in range(W):

                if ls <= lats[r] <= ln and lw <= lons[c] <= le:

                    land_mask_lr[r, c] = False

    

    # Upsample to output resolution

    land_mask_hr = spz(land_mask_lr.astype(float), output_scale, order=0) > 0.5

    return land_mask_lr, land_mask_hr

```


**Explanation:** Implements Improvement 4 β€” land-sea mask. Swin2SR has no ocean physics knowledge; applying SR to ocean creates hallucinated SST structure. Uses temporal variance (ocean ~2K std, land >10K std) + conservative ocean boxes. **Impact:** AI applied to land only (~55-60%), ERA5 SST preserved over ocean.

### 2.2 apply_land_mask

```python

def apply_land_mask(pred_K: np.ndarray, era5_K: np.ndarray, 

                    mask_hr: np.ndarray) -> np.ndarray:

    H, W = era5_K.shape

    era5_up = spz(era5_K, pred_K.shape[0] / H, order=3)

    era5_up = era5_up[:pred_K.shape[0], :pred_K.shape[1]]

    

    out = era5_up.copy()

    out[mask_hr] = pred_K[mask_hr]  # Land=AI, Ocean=ERA5

    return out

```

**Explanation:** Blends AI prediction (land) with ERA5 upsampled (ocean). Scientifically correct: ERA5 SST is already high quality at 0.25Β°.

---

## 3. Data Loading (`src/data/netcdf_loader.py`)



### 3.1 NetCDFLoader Class



```python

class NetCDFLoader:

    def __init__(self, filepath: Union[str, Path], config: Dict[str, Any]):

        self.filepath = Path(filepath)

        self.config = config

        region = config.get("data", {}).get("region", {})

        self.lat_min = region.get("lat_min", 6.0)

        self.lat_max = region.get("lat_max", 38.0)

        self.lon_min = region.get("lon_min", 68.0)

        self.lon_max = region.get("lon_max", 98.0)

        self.variables = config.get("data", {}).get("variables", ["t2m"])



    def load(self) -> None:

        import xarray as xr

        self.dataset = xr.open_dataset(self.filepath)
        self._normalize_coordinates()  # lat/latitude, lon/longitude

        self._normalize_variables()    # 2t/var167 β†’ t2m

        self._subset_region()          # Extract India bounding box

        self._is_loaded = True


    def _subset_region(self) -> None:

        lats = self.dataset.coords["latitude"].values

        lat_ascending = lats[0] < lats[-1]

        lat_slice = slice(self.lat_min, self.lat_max) if lat_ascending \

                    else slice(self.lat_max, self.lat_min)

        self.dataset = self.dataset.sel(

            latitude=lat_slice,

            longitude=slice(self.lon_min, self.lon_max)

        )


    def get_variable(self, var_name: str) -> np.ndarray:

        mapped_name = self.ERA5_VARIABLE_MAP.get(var_name, var_name)

        return self.dataset[mapped_name].values.astype(np.float32)

```


**Explanation:** Memory-efficient ERA5 loader with automatic coordinate normalization and regional subsetting. **Impact:** Global 1440Γ—721 β†’ India 129Γ—121 (98.5% reduction), ~100GB β†’ ~550MB.

---

## 4. Preprocessing (`src/data/preprocessor.py`)

### 4.1 Z-Score Normalization

```python

class Preprocessor:

    def fit(self, data: np.ndarray, variable: str):

        clean_data = self._handle_missing_values(data)

        mean = float(np.nanmean(clean_data))

        std = float(np.nanstd(clean_data))

        if std < 1e-8: std = 1.0

        

        self.statistics[variable] = {

            "mean": mean, "std": std,

            "min": float(np.nanmin(clean_data)),

            "max": float(np.nanmax(clean_data))

        }

        self.is_fitted = True

        return self



    def transform(self, data: np.ndarray, variable: str):

        processed = self._handle_missing_values(data.copy())

        outlier_mask = self._detect_outliers(processed, variable)

        if self.normalize:

            processed = self._normalize(processed, variable)

        return processed.astype(np.float32), outlier_mask



    def inverse_transform(self, data: np.ndarray, variable: str):

        stats = self.statistics[variable]

        return data * stats["std"] + stats["mean"]  # z β†’ Kelvin

```

**Explanation:** Z-score normalization: `z = (T - ΞΌ) / Οƒ` where ΞΌ=292.24K, Οƒ=14.43K. Neural networks train better with zero-mean, unit-variance inputs. Fully invertible for physical unit reconstruction.

---

## 5. FastAPI Backend (`dashboard_backend/main.py`)



### 5.1 Application Startup



```python

@asynccontextmanager

async def lifespan(app: FastAPI):

    global _model, _data, _preproc, _physics, _elev, _mask_hr, _mean_field_hr

    

    # Load ensemble model (both variants, INT8 quantized)

    _model = load_model(device="cpu")

    _model.eval()
    

    # Load ERA5 data

    cfg = load_config()

    loader = load_climate_data(cfg, data_path=str(nc))

    raw = loader.get_variable(cfg["data"]["variables"][0])

    loader.close()

    

    # Preprocess

    _preproc = Preprocessor(cfg)

    _data, _ = _preproc.fit_transform(raw, cfg["data"]["variables"][0])

    stats = _preproc.statistics[cfg["data"]["variables"][0]]

    _mean, _std = float(stats["mean"]), float(stats["std"])

    

    # IMPROVEMENT 2: Physics preprocessor + precompute HR mean field

    _physics = PhysicsPreprocessor(_data)

    _mean_field_hr = spz(_physics.mean_field, 4, order=3)[:H*4, :W*4]

    

    # IMPROVEMENT 3: Elevation corrector

    mean_K = _data.mean(axis=0) * _std + _mean

    _elev = ElevationCorrector(mean_K, output_shape=(H*4, W*4))

    

    # IMPROVEMENT 4: Land-sea mask

    _mask_lr, _mask_hr = build_land_mask(_data, LAT_MIN, LAT_MAX, 

                                          LON_MIN, LON_MAX, output_scale=4)

    

    yield

    _cache.stop()

```


**Explanation:** Loads models, data, and precomputes all improvements at startup (~30s). Precomputing HR mean field, DEM proxy, and land mask avoids repeated computation during inference.

---

### 5.2 Inference Pipeline

```python

def _run_inference(t: int):

    era5 = _data[t].copy()

    era5_K = _z2k(era5)  # z-score β†’ Kelvin

    

    # Check cache

    cached = _cache.get(t)

    if cached is not None:

        return era5_K, cached.astype(np.float32)

    

    H, W = era5.shape

    ph, pw = (64 - H % 64) % 64, (64 - W % 64) % 64

    

    # IMPROVEMENT 2: Anomaly pre-processing

    inp = _physics.to_anomaly(era5) if _physics else era5

    padded = np.pad(inp, ((0, ph), (0, pw)), mode="edge")

    x = torch.from_numpy(padded).unsqueeze(0).unsqueeze(0).float()

    

    # IMPROVEMENT 1: Ensemble inference

    with torch.no_grad():

        out = _model(x).squeeze().numpy()

    pred = out[:H*4, :W*4]

    

    # IMPROVEMENT 2: Add mean back (precomputed HR field)

    if _physics and _mean_field_hr is not None:

        pred = _physics.from_anomaly(pred, _mean_field_hr)

    

    pred_K = _z2k(pred)

    

    # IMPROVEMENT 3: Elevation correction

    if _elev:

        pred_K = _elev.apply(pred_K)

    

    # IMPROVEMENT 4: Land-sea mask

    if _mask_hr is not None:

        pred_K = apply_land_mask(pred_K, era5_K, _mask_hr)

    

    return era5_K, pred_K



@lru_cache(maxsize=16)

def _cached_inference(t: int):

    return _run_inference(t)

```

**Explanation:** Complete 11-step inference pipeline with all 4 improvements. LRU cache stores 16 most recent timesteps. **Performance:** First call ~12s, cached call ~28ms (428Γ— speedup).

---

### 5.3 PNG Rendering with Unsharp Mask

```python

def _to_png(arr_K, vmin=None, vmax=None, cmap="RdYlBu_r", 

            alpha=220, sharpen=False):

    data = arr_K.copy()

    

    # Unsharp mask sharpening (AI output only)

    if sharpen:

        blurred = gaussian_filter(data, sigma=1.2)

        data = data + 1.8 * (data - blurred)  # Ξ±=1.8

    

    # Normalize to [0,1]

    v0 = vmin if vmin else float(data.min())

    v1 = vmax if vmax else float(data.max())

    norm = np.clip((data - v0) / (v1 - v0 + 1e-8), 0, 1)

    

    # Apply colormap and render

    rgba = (plt.get_cmap(cmap)(norm) * 255).astype(np.uint8)

    rgba[..., 3] = alpha

    buf = io.BytesIO()

    Image.fromarray(rgba, "RGBA").save(buf, format="PNG", compress_level=1)

    buf.seek(0)

    return Response(content=buf.getvalue(), media_type="image/png")

```

**Explanation:** Renders temperature as PNG with optional unsharp mask (`sharpened = original + 1.8 Γ— (original - blurred)`). Applied only to AI output for visual clarity. Sharpening affects PNG only, not underlying data.

---

### 5.4 Sharpness Gain Measurement

```python

def _laplacian(arr):

    gy, gx = np.gradient(arr)

    gyy, _ = np.gradient(gy)

    _, gxx = np.gradient(gx)

    return float(np.mean(np.abs(gyy + gxx)))  # βˆ‡Β²f = βˆ‚Β²f/βˆ‚xΒ² + βˆ‚Β²f/βˆ‚yΒ²



def _sharpness_gain(era5_K, pred_K):

    f = pred_K.shape[0] / era5_K.shape[0]

    up = spz(era5_K, f, order=3)[:pred_K.shape[0], :pred_K.shape[1]]

    s0, s1 = _laplacian(up), _laplacian(pred_K)

    return s0, s1, round((s1 / (s0 + 1e-8) - 1.0) * 100, 1)

```

**Explanation:** Laplacian measures local curvature (edge energy). Sharpness gain = (AI_laplacian / baseline_laplacian - 1) Γ— 100%. **Result:** +21.3% means AI has 21.3% more edge energy than cubic baseline.

---

### 5.5 Power Spectral Density Analysis

```python

@lru_cache(maxsize=6)

def _compute_psd(t: int):

    era5_K, pred_K = _cached_inference(t)

    up = spz(era5_K, pred_K.shape[0] / era5_K.shape[0], order=3)

    

    def rpsd(arr):

        arr = (arr - arr.mean()) * np.hanning(arr.shape[0])[:, None] \

                                  * np.hanning(arr.shape[1])[None, :]

        F = np.fft.fftshift(np.fft.fft2(arr))

        P = (np.abs(F)**2) / (arr.shape[0] * arr.shape[1])

        

        # Radial averaging

        cy, cx = arr.shape[0]//2, arr.shape[1]//2

        Y, X = np.mgrid[-cy:arr.shape[0]-cy, -cx:arr.shape[1]-cx]

        R = np.sqrt(X**2 + Y**2).astype(int)

        return np.array([P[R==r].mean() if np.any(R==r) else 0 

                         for r in range(1, min(cy, cx))])

    

    p0, p1 = rpsd(up), rpsd(pred_K)

    n = min(len(p0), len(p1))

    wl = (min(pred_K.shape) / np.arange(1, n+1)) * 7.0  # km

    mask = (wl >= 10) & (wl <= 500)

    return {"wavelengths_km": wl[mask].tolist(),

            "psd_era5": np.log10(p0[:n][mask] + 1e-20).tolist(),

            "psd_ai": np.log10(p1[:n][mask] + 1e-20).tolist()}

```

**Explanation:** 2D FFT β†’ power spectrum β†’ radial averaging. Quantifies spatial frequency content. **Result:** +4.58 dB gain @ 27km wavelength (AI has 2.87Γ— more power at fine scales).

---

## 6. Background Prediction Cache

### 6.1 PredictionCache Class

```python

class PredictionCache:

    def __init__(self):

        self._cache = {}

        self._lock = threading.Lock()

        self.ready = False

        self.progress = 0.0



    def start(self, model, data, zscore_fn, physics, elevation, 

              mask_hr, workers=2, on_progress=None):

        T = data.shape[0]

        

        def build():

            t0 = time.time()

            done = 0

            

            def infer_one(idx):

                # Full inference pipeline for timestep idx

                # ... (anomaly, ensemble, elevation, mask) ...

                self.put(idx, pred_K)

            

            with ThreadPoolExecutor(max_workers=workers) as pool:

                futs = {pool.submit(infer_one, t): t for t in range(T)}

                for f in futs:

                    f.result()

                    done += 1

                    self.progress = done / T

                    self.eta_sec = int(((time.time()-t0)/done) * (T-done))

                    if on_progress:

                        on_progress(done, T, self.eta_sec)

            

            self.ready = True

        

        threading.Thread(target=build, daemon=True).start()



    def get(self, t: int):

        with self._lock:

            return self._cache.get(t)



    def put(self, t: int, arr: np.ndarray):

        with self._lock:

            self._cache[t] = arr.astype(np.float16)  # Half precision

```

**Explanation:** Multi-threaded background pre-compute. Processes all 8,784 timesteps using ThreadPoolExecutor (2–6 workers). Stores results as float16 (~4.3GB for all frames). **Performance:** 2 workers ~2hrs, 4 workers ~1hr, 6 workers ~45min. Once built, every frame returns in ~28ms.

---

## Summary Statistics

| Component | Details |
|-----------|---------|
| **Total Code** | ~2,500 lines Python |
| **Key Classes** | 7 (DownscalerModel, EnsembleDownscaler, PhysicsPreprocessor, ElevationCorrector, PredictionCache, NetCDFLoader, Preprocessor) |
| **API Endpoints** | 11 REST endpoints |
| **Model Parameters** | 24.2M (12.1M Γ— 2, INT8 quantized) |
| **Memory Footprint** | Models 24MB, Data 550MB, Cache 4.3GB, Total ~13.5GB |
| **Performance** | Live inference 12s, Cached 28ms (428Γ— speedup) |
| **Improvements** | Ensemble (+21.3%), Physics (anomaly), Elevation (lapse rate), Mask (land-sea) |

---

**End of CodeBase Documentation**