Commit ·
a0c6371
1
Parent(s): 9bd18f8
Add ablation study and spatial performance scripts; update pipeline configuration and dataset handling
Browse files- .gitignore +8 -1
- __init__.py +0 -0
- analysis/spatial_performance.py +273 -0
- forecasting/data_loaders/SDOAIA_dataloader.py +125 -1
- forecasting/inference/ablation_inference.py +189 -0
- forecasting/inference/ablation_inference_config.yaml +62 -0
- pipeline_config.yaml +56 -27
- run_pipeline.py +59 -0
.gitignore
CHANGED
|
@@ -155,4 +155,11 @@ wandb/
|
|
| 155 |
|
| 156 |
.claude/
|
| 157 |
Untracked/
|
| 158 |
-
foxes.yml
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
|
| 156 |
.claude/
|
| 157 |
Untracked/
|
| 158 |
+
foxes.yml
|
| 159 |
+
.git-backup
|
| 160 |
+
/forecasting/training/convert_flux_to_npy.py
|
| 161 |
+
/forecasting/inference/extract_patch_flux.py
|
| 162 |
+
/.merged_train_config.yaml
|
| 163 |
+
/.merged_inference_config.yaml
|
| 164 |
+
/.merged_evaluate_config.yaml
|
| 165 |
+
.npz
|
__init__.py
ADDED
|
File without changes
|
analysis/spatial_performance.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Flux-Weighted Error Heatmap on Solar Disk
|
| 3 |
+
==========================================
|
| 4 |
+
For each matched flux map, accumulates:
|
| 5 |
+
mae_sum[i,j] += flux[i,j] * |log10 error|
|
| 6 |
+
bias_sum[i,j] += flux[i,j] * log10 error
|
| 7 |
+
weight[i,j] += flux[i,j]
|
| 8 |
+
|
| 9 |
+
Then normalizes to get flux-weighted mean error per patch.
|
| 10 |
+
|
| 11 |
+
Usage
|
| 12 |
+
-----
|
| 13 |
+
python analysis/spatial_performance.py
|
| 14 |
+
|
| 15 |
+
Outputs
|
| 16 |
+
-------
|
| 17 |
+
analysis/flux_weighted_errors_t0.npz — accumulation cache
|
| 18 |
+
analysis/performance_heatmap_all.png
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import os
|
| 22 |
+
import sys
|
| 23 |
+
import numpy as np
|
| 24 |
+
import pandas as pd
|
| 25 |
+
import matplotlib.pyplot as plt
|
| 26 |
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
| 27 |
+
from tqdm import tqdm
|
| 28 |
+
from pathlib import Path
|
| 29 |
+
from cmap import Colormap
|
| 30 |
+
|
| 31 |
+
PROJECT_ROOT = Path(__file__).parent.parent
|
| 32 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 33 |
+
from forecasting.inference.evaluation import setup_barlow_font
|
| 34 |
+
|
| 35 |
+
# ---------------------------------------------------------------------------
|
| 36 |
+
# Paths — edit here
|
| 37 |
+
# ---------------------------------------------------------------------------
|
| 38 |
+
FLUX_DIR = "/Volumes/T9/FOXES_Data/flux/"
|
| 39 |
+
PREDICTIONS_CSV = "/Volumes/T9/FOXES_Misc/batch_results/vit/vit_predictions_test.csv"
|
| 40 |
+
OUT_DIR = Path(__file__).parent
|
| 41 |
+
GRID_SIZE = 64 # 512px / 8px patch size
|
| 42 |
+
BIN_SIZE = 1 # downsample factor (1 = full 64×64 resolution)
|
| 43 |
+
CROP_FACTOR = 1.1 # AIA images cropped at 1.1 solar radii
|
| 44 |
+
SOLAR_RADIUS_PATCHES = (GRID_SIZE / 2) / CROP_FACTOR # ≈ 29.1 patches
|
| 45 |
+
|
| 46 |
+
# Only patches above this percentile (per flux map) contribute.
|
| 47 |
+
# 0 = include all non-zero patches.
|
| 48 |
+
FLUX_THRESHOLD_PERCENTILE = 0
|
| 49 |
+
|
| 50 |
+
# Percentile cap for colorbar scaling (applied to non-NaN values).
|
| 51 |
+
# e.g. 99 clips the top 1% of values so detail in the bulk is visible.
|
| 52 |
+
VMAX_PERCENTILE = 99.9
|
| 53 |
+
|
| 54 |
+
# ---------------------------------------------------------------------------
|
| 55 |
+
# Helpers
|
| 56 |
+
# ---------------------------------------------------------------------------
|
| 57 |
+
|
| 58 |
+
def normalize_ts(series: pd.Series) -> pd.Series:
|
| 59 |
+
return pd.to_datetime(
|
| 60 |
+
series.astype(str).str.replace("_", ":", regex=False), utc=False,
|
| 61 |
+
).dt.floor("s")
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _ts_key(fpath: str) -> str:
|
| 65 |
+
raw = os.path.basename(fpath).replace('.npy', '').replace('_', ':')
|
| 66 |
+
return pd.Timestamp(raw).floor('s').isoformat()
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def load_predictions(predictions_csv: str) -> pd.DataFrame:
|
| 70 |
+
df = pd.read_csv(predictions_csv)
|
| 71 |
+
df["timestamp"] = normalize_ts(df["timestamp"])
|
| 72 |
+
df["log_pred"] = np.log10(df["predictions"])
|
| 73 |
+
df["log_gt"] = np.log10(df["groundtruth"])
|
| 74 |
+
df["log_error"] = df["log_pred"] - df["log_gt"]
|
| 75 |
+
df["log_abs_error"] = df["log_error"].abs()
|
| 76 |
+
print(f"Loaded {len(df)} predictions")
|
| 77 |
+
return df
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# ---------------------------------------------------------------------------
|
| 81 |
+
# Heatmap accumulation
|
| 82 |
+
# ---------------------------------------------------------------------------
|
| 83 |
+
|
| 84 |
+
# NOTE: module-level for ProcessPoolExecutor (spawn on macOS)
|
| 85 |
+
def _accumulate_flux_map(args):
|
| 86 |
+
fpath, log_abs_error, log_error, threshold_pct = args
|
| 87 |
+
fmap = np.load(fpath).astype(np.float64)
|
| 88 |
+
active = fmap[fmap > 0]
|
| 89 |
+
if active.size == 0:
|
| 90 |
+
return None
|
| 91 |
+
if threshold_pct > 0:
|
| 92 |
+
thresh = np.percentile(active, threshold_pct)
|
| 93 |
+
fmap = np.where(fmap >= thresh, fmap, 0.0)
|
| 94 |
+
else:
|
| 95 |
+
fmap = np.where(fmap > 0, fmap, 0.0)
|
| 96 |
+
total = fmap.sum()
|
| 97 |
+
if total == 0:
|
| 98 |
+
return None
|
| 99 |
+
fmap = fmap / total # normalise: each timestamp contributes equal total weight
|
| 100 |
+
return fmap * log_abs_error, fmap * log_error, fmap
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def compute_flux_weighted_errors(flux_dir: str, df: pd.DataFrame, cache_path: Path,
|
| 104 |
+
threshold_pct: int = FLUX_THRESHOLD_PERCENTILE) -> dict:
|
| 105 |
+
cache_path = cache_path.with_stem(f"{cache_path.stem}_t{threshold_pct}")
|
| 106 |
+
|
| 107 |
+
if cache_path.exists():
|
| 108 |
+
print(f"Loading cached flux-weighted error maps from {cache_path}")
|
| 109 |
+
data = np.load(cache_path)
|
| 110 |
+
result = {}
|
| 111 |
+
n = float(data['count'])
|
| 112 |
+
w = data['weight']
|
| 113 |
+
mae = np.where(w > 0, data['mae_sum'] / n, np.nan) if n > 0 else np.full_like(w, np.nan)
|
| 114 |
+
bias = np.where(w > 0, data['bias_sum'] / n, np.nan) if n > 0 else np.full_like(w, np.nan)
|
| 115 |
+
return mae, bias, w
|
| 116 |
+
|
| 117 |
+
lookup = {}
|
| 118 |
+
for _, row in df.iterrows():
|
| 119 |
+
key = pd.Timestamp(row['timestamp']).floor('s').isoformat()
|
| 120 |
+
lookup[key] = (float(row['log_abs_error']), float(row['log_error']))
|
| 121 |
+
|
| 122 |
+
shape = (GRID_SIZE, GRID_SIZE)
|
| 123 |
+
mae_sum = np.zeros(shape)
|
| 124 |
+
bias_sum = np.zeros(shape)
|
| 125 |
+
weight = np.zeros(shape)
|
| 126 |
+
count = 0
|
| 127 |
+
|
| 128 |
+
files = sorted([os.path.join(flux_dir, f)
|
| 129 |
+
for f in os.listdir(flux_dir) if f.endswith('.npy')])
|
| 130 |
+
args_list = []
|
| 131 |
+
for fpath in files:
|
| 132 |
+
try:
|
| 133 |
+
ts_key = _ts_key(fpath)
|
| 134 |
+
except Exception:
|
| 135 |
+
continue
|
| 136 |
+
if ts_key not in lookup:
|
| 137 |
+
continue
|
| 138 |
+
abs_err, err = lookup[ts_key]
|
| 139 |
+
args_list.append((fpath, abs_err, err, threshold_pct))
|
| 140 |
+
|
| 141 |
+
print(f"Matched {len(args_list)} / {len(files)} flux maps")
|
| 142 |
+
|
| 143 |
+
with ProcessPoolExecutor(max_workers=os.cpu_count()) as executor:
|
| 144 |
+
futures = {executor.submit(_accumulate_flux_map, a): i
|
| 145 |
+
for i, a in enumerate(args_list)}
|
| 146 |
+
for future in tqdm(as_completed(futures), total=len(args_list),
|
| 147 |
+
desc="Accumulating flux-weighted errors"):
|
| 148 |
+
result = future.result()
|
| 149 |
+
if result is None:
|
| 150 |
+
continue
|
| 151 |
+
mae_c, bias_c, flux_c = result
|
| 152 |
+
mae_sum += mae_c
|
| 153 |
+
bias_sum += bias_c
|
| 154 |
+
weight += flux_c
|
| 155 |
+
count += 1
|
| 156 |
+
|
| 157 |
+
np.savez(cache_path, mae_sum=mae_sum, bias_sum=bias_sum,
|
| 158 |
+
weight=weight, count=np.array(count))
|
| 159 |
+
print(f"Saved → {cache_path}")
|
| 160 |
+
|
| 161 |
+
mae = np.where(weight > 0, mae_sum / count, np.nan) if count > 0 else np.full(shape, np.nan)
|
| 162 |
+
bias = np.where(weight > 0, bias_sum / count, np.nan) if count > 0 else np.full(shape, np.nan)
|
| 163 |
+
return mae, bias, weight
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
# ---------------------------------------------------------------------------
|
| 167 |
+
# Plot
|
| 168 |
+
# ---------------------------------------------------------------------------
|
| 169 |
+
|
| 170 |
+
def _bin_grid(grid: np.ndarray, bin_size: int) -> np.ndarray:
|
| 171 |
+
if bin_size == 1:
|
| 172 |
+
return grid
|
| 173 |
+
h, w = grid.shape
|
| 174 |
+
bh, bw = h // bin_size, w // bin_size
|
| 175 |
+
cropped = grid[:bh * bin_size, :bw * bin_size]
|
| 176 |
+
return np.nanmean(cropped.reshape(bh, bin_size, bw, bin_size), axis=(1, 3))
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def plot_flux_weighted_heatmap(mae_grid: np.ndarray, bias_grid: np.ndarray,
|
| 180 |
+
weight_grid: np.ndarray, out_path: Path,
|
| 181 |
+
subtitle: str = "", bin_size: int = BIN_SIZE,
|
| 182 |
+
vmax_pct: int = VMAX_PERCENTILE):
|
| 183 |
+
setup_barlow_font()
|
| 184 |
+
text_color = "#111111"
|
| 185 |
+
theta = np.linspace(0, 2 * np.pi, 300)
|
| 186 |
+
|
| 187 |
+
mae_b = _bin_grid(mae_grid, bin_size)
|
| 188 |
+
bias_b = _bin_grid(bias_grid, bin_size)
|
| 189 |
+
weight_b = _bin_grid(weight_grid, bin_size)
|
| 190 |
+
n_bins = mae_b.shape[0]
|
| 191 |
+
cy, cx = n_bins / 2, n_bins / 2
|
| 192 |
+
|
| 193 |
+
r_limb = SOLAR_RADIUS_PATCHES / bin_size
|
| 194 |
+
|
| 195 |
+
mae_vmax = np.nanpercentile(mae_b, vmax_pct)
|
| 196 |
+
mae_norm = plt.Normalize(vmin=0, vmax=mae_vmax)
|
| 197 |
+
|
| 198 |
+
bias_cap = np.nanpercentile(np.abs(bias_b), vmax_pct)
|
| 199 |
+
bias_norm = plt.Normalize(vmin=-bias_cap, vmax=bias_cap)
|
| 200 |
+
|
| 201 |
+
panels = [
|
| 202 |
+
(mae_b, r"Flux-Weighted MAE (log$_{10}$ Space)", Colormap('cmocean:thermal').to_mpl(), mae_norm),
|
| 203 |
+
(bias_b, r"Flux-Weighted MBE (log$_{10}$ Space)", Colormap('cmasher:fusion_r').to_mpl(), bias_norm),
|
| 204 |
+
# (np.log10(np.where(weight_b > 0, weight_b, np.nan)),
|
| 205 |
+
# r"log$_{10}$ Accumulated Flux", "viridis", None),
|
| 206 |
+
]
|
| 207 |
+
|
| 208 |
+
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
|
| 209 |
+
fig.patch.set_facecolor("white")
|
| 210 |
+
|
| 211 |
+
for ax, (grid, title, cmap, norm) in zip(axes, panels):
|
| 212 |
+
im = ax.imshow(grid, origin="lower", cmap=cmap, norm=norm,
|
| 213 |
+
interpolation="bilinear", extent=[0, n_bins, 0, n_bins])
|
| 214 |
+
cbar = fig.colorbar(im, ax=ax, shrink=0.82)
|
| 215 |
+
cbar.ax.tick_params(labelsize=9, colors=text_color)
|
| 216 |
+
def _fmt(x, _):
|
| 217 |
+
m, e = f"{x:.2e}".split("e")
|
| 218 |
+
return f"{m}e{int(e)}"
|
| 219 |
+
cbar.ax.yaxis.set_major_formatter(plt.matplotlib.ticker.FuncFormatter(_fmt))
|
| 220 |
+
for lbl in cbar.ax.get_yticklabels():
|
| 221 |
+
lbl.set_fontfamily("Barlow")
|
| 222 |
+
lbl.set_fontsize(9)
|
| 223 |
+
lbl.set_color(text_color)
|
| 224 |
+
#cbar.set_label(title, fontsize=9, color=text_color, fontfamily="Barlow")
|
| 225 |
+
|
| 226 |
+
ax.plot(cx + r_limb * np.cos(theta), cy + r_limb * np.sin(theta),
|
| 227 |
+
color="#4488FF", linestyle="--", linewidth=1.2, alpha=0.8,
|
| 228 |
+
label=f"Solar Limb")
|
| 229 |
+
|
| 230 |
+
tick_bins = np.linspace(0, n_bins, 7)
|
| 231 |
+
tick_labels = [f"{int((t - n_bins / 2) * bin_size)}" for t in tick_bins]
|
| 232 |
+
ax.set_xticks(tick_bins); ax.set_xticklabels(tick_labels)
|
| 233 |
+
ax.set_yticks(tick_bins); ax.set_yticklabels(tick_labels)
|
| 234 |
+
|
| 235 |
+
ax.set_title(title, fontsize=10, color=text_color, fontfamily="Barlow")
|
| 236 |
+
ax.set_xlabel("Solar X (ViT Patches From Center)", fontsize=9,
|
| 237 |
+
color=text_color, fontfamily="Barlow")
|
| 238 |
+
ax.set_ylabel("Solar Y (ViT Patches From Center)", fontsize=9,
|
| 239 |
+
color=text_color, fontfamily="Barlow")
|
| 240 |
+
ax.tick_params(labelsize=8, colors=text_color)
|
| 241 |
+
ax.legend(fontsize=7, facecolor="white", edgecolor="grey", loc="upper right",)
|
| 242 |
+
for spine in ax.spines.values():
|
| 243 |
+
spine.set_color(text_color)
|
| 244 |
+
|
| 245 |
+
plt.tight_layout()
|
| 246 |
+
plt.savefig(out_path, dpi=400, bbox_inches="tight", facecolor="white")
|
| 247 |
+
plt.show()
|
| 248 |
+
print(f"Saved → {out_path}")
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
# ---------------------------------------------------------------------------
|
| 252 |
+
# Main
|
| 253 |
+
# ---------------------------------------------------------------------------
|
| 254 |
+
|
| 255 |
+
if __name__ == "__main__":
|
| 256 |
+
import argparse
|
| 257 |
+
parser = argparse.ArgumentParser()
|
| 258 |
+
parser.add_argument("--flux_dir", default=FLUX_DIR)
|
| 259 |
+
parser.add_argument("--predictions_csv", default=PREDICTIONS_CSV)
|
| 260 |
+
parser.add_argument("--out_dir", default=str(OUT_DIR))
|
| 261 |
+
args = parser.parse_args()
|
| 262 |
+
|
| 263 |
+
out = Path(args.out_dir)
|
| 264 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 265 |
+
|
| 266 |
+
df = load_predictions(args.predictions_csv)
|
| 267 |
+
|
| 268 |
+
mae, bias, weight = compute_flux_weighted_errors(
|
| 269 |
+
args.flux_dir, df, out / "flux_weighted_errors.npz"
|
| 270 |
+
)
|
| 271 |
+
plot_flux_weighted_heatmap(mae, bias, weight,
|
| 272 |
+
out / "performance_heatmap_all.png",
|
| 273 |
+
subtitle="All flares")
|
forecasting/data_loaders/SDOAIA_dataloader.py
CHANGED
|
@@ -63,7 +63,7 @@ class AIA_GOESDataset(torch.utils.data.Dataset):
|
|
| 63 |
Strategy for balancing dataset classes.
|
| 64 |
"""
|
| 65 |
|
| 66 |
-
def __init__(self, aia_dir, sxr_dir, wavelengths=[94, 131, 171, 193, 211, 304], sxr_transform=None,
|
| 67 |
target_size=(512, 512), cadence=1, reference_time=None, only_prediction=False, oversample=False,
|
| 68 |
flare_threshold=1e-5, balance_strategy='upsample_minority'):
|
| 69 |
self.aia_dir = Path(aia_dir).resolve()
|
|
@@ -311,6 +311,130 @@ class AIA_GOESDataset(torch.utils.data.Dataset):
|
|
| 311 |
return timestamp
|
| 312 |
|
| 313 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
class AIA_GOESDataModule(LightningDataModule):
|
| 315 |
"""
|
| 316 |
PyTorch Lightning DataModule for managing AIA-GOES datasets.
|
|
|
|
| 63 |
Strategy for balancing dataset classes.
|
| 64 |
"""
|
| 65 |
|
| 66 |
+
def __init__(self, aia_dir, sxr_dir, wavelengths=[94, 131, 171, 193, 211, 304, 335], sxr_transform=None,
|
| 67 |
target_size=(512, 512), cadence=1, reference_time=None, only_prediction=False, oversample=False,
|
| 68 |
flare_threshold=1e-5, balance_strategy='upsample_minority'):
|
| 69 |
self.aia_dir = Path(aia_dir).resolve()
|
|
|
|
| 311 |
return timestamp
|
| 312 |
|
| 313 |
|
| 314 |
+
class NoisyAIA_GOESDataset(AIA_GOESDataset):
|
| 315 |
+
"""
|
| 316 |
+
Ablation dataset that applies Gaussian noise to specific AIA wavelength channels.
|
| 317 |
+
|
| 318 |
+
Inherits all behavior from AIA_GOESDataset and injects additive Gaussian noise
|
| 319 |
+
on the specified wavelengths after loading, enabling channel-wise ablation studies.
|
| 320 |
+
|
| 321 |
+
Parameters
|
| 322 |
+
----------
|
| 323 |
+
noisy_wavelengths : list of int
|
| 324 |
+
Subset of wavelengths to corrupt with noise (e.g. [171, 193]).
|
| 325 |
+
Must be a subset of the `wavelengths` argument.
|
| 326 |
+
noise_std : float or dict
|
| 327 |
+
Standard deviation of the Gaussian noise. If a float, the same std is
|
| 328 |
+
applied to all noisy channels. If a dict mapping wavelength -> std, each
|
| 329 |
+
channel gets its own scale.
|
| 330 |
+
*args, **kwargs
|
| 331 |
+
Forwarded to AIA_GOESDataset.
|
| 332 |
+
"""
|
| 333 |
+
|
| 334 |
+
def __init__(self, *args, noisy_wavelengths, noise_std=1.0, **kwargs):
|
| 335 |
+
super().__init__(*args, **kwargs)
|
| 336 |
+
self.noisy_wavelengths = noisy_wavelengths
|
| 337 |
+
# Build a per-channel std lookup
|
| 338 |
+
if isinstance(noise_std, dict):
|
| 339 |
+
self.noise_std = noise_std
|
| 340 |
+
else:
|
| 341 |
+
self.noise_std = {w: noise_std for w in noisy_wavelengths}
|
| 342 |
+
|
| 343 |
+
invalid = set(noisy_wavelengths) - set(self.wavelengths)
|
| 344 |
+
if invalid:
|
| 345 |
+
raise ValueError(f"noisy_wavelengths {invalid} not in dataset wavelengths {self.wavelengths}")
|
| 346 |
+
|
| 347 |
+
def __getitem__(self, idx):
|
| 348 |
+
aia_img, sxr_val = super().__getitem__(idx) # (H, W, C)
|
| 349 |
+
|
| 350 |
+
for wav in self.noisy_wavelengths:
|
| 351 |
+
c = self.wavelengths.index(wav)
|
| 352 |
+
std = self.noise_std[wav]
|
| 353 |
+
noise = torch.randn_like(aia_img[..., c]) * std
|
| 354 |
+
aia_img[..., c] = aia_img[..., c] + noise
|
| 355 |
+
|
| 356 |
+
return aia_img, sxr_val
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
class NoisyAIA_GOESDataModule(LightningDataModule):
|
| 360 |
+
"""
|
| 361 |
+
DataModule for the Gaussian-noise ablation study.
|
| 362 |
+
|
| 363 |
+
Wraps NoisyAIA_GOESDataset for test/val splits while keeping training
|
| 364 |
+
clean (or optionally noisy too). Noise is applied only at evaluation time
|
| 365 |
+
by default so the pre-trained model is evaluated without retraining.
|
| 366 |
+
|
| 367 |
+
Parameters
|
| 368 |
+
----------
|
| 369 |
+
noisy_wavelengths : list of int
|
| 370 |
+
Wavelengths to corrupt during evaluation.
|
| 371 |
+
noise_std : float or dict
|
| 372 |
+
Noise standard deviation(s); see NoisyAIA_GOESDataset.
|
| 373 |
+
apply_noise_to_train : bool, optional
|
| 374 |
+
If True, also corrupt the training split (default: False).
|
| 375 |
+
All other parameters match AIA_GOESDataModule.
|
| 376 |
+
"""
|
| 377 |
+
|
| 378 |
+
def __init__(self, aia_train_dir, aia_val_dir, aia_test_dir,
|
| 379 |
+
sxr_train_dir, sxr_val_dir, sxr_test_dir,
|
| 380 |
+
sxr_norm, noisy_wavelengths, noise_std=1.0,
|
| 381 |
+
apply_noise_to_train=False,
|
| 382 |
+
batch_size=64, num_workers=4,
|
| 383 |
+
wavelengths=[94, 131, 171, 193, 211, 304, 335],
|
| 384 |
+
cadence=1, reference_time=None):
|
| 385 |
+
super().__init__()
|
| 386 |
+
self.aia_train_dir = aia_train_dir
|
| 387 |
+
self.aia_val_dir = aia_val_dir
|
| 388 |
+
self.aia_test_dir = aia_test_dir
|
| 389 |
+
self.sxr_train_dir = sxr_train_dir
|
| 390 |
+
self.sxr_val_dir = sxr_val_dir
|
| 391 |
+
self.sxr_test_dir = sxr_test_dir
|
| 392 |
+
self.sxr_norm = sxr_norm
|
| 393 |
+
self.noisy_wavelengths = noisy_wavelengths
|
| 394 |
+
self.noise_std = noise_std
|
| 395 |
+
self.apply_noise_to_train = apply_noise_to_train
|
| 396 |
+
self.batch_size = batch_size
|
| 397 |
+
self.num_workers = num_workers
|
| 398 |
+
self.wavelengths = wavelengths
|
| 399 |
+
self.cadence = cadence
|
| 400 |
+
self.reference_time = reference_time
|
| 401 |
+
|
| 402 |
+
def _make_dataset(self, aia_dir, sxr_dir, noisy):
|
| 403 |
+
cls = NoisyAIA_GOESDataset if noisy else AIA_GOESDataset
|
| 404 |
+
kwargs = dict(
|
| 405 |
+
aia_dir=aia_dir,
|
| 406 |
+
sxr_dir=sxr_dir,
|
| 407 |
+
sxr_transform=SXRLogNormTransform(self.sxr_norm[0], self.sxr_norm[1]),
|
| 408 |
+
target_size=(512, 512),
|
| 409 |
+
wavelengths=self.wavelengths,
|
| 410 |
+
cadence=self.cadence,
|
| 411 |
+
reference_time=self.reference_time,
|
| 412 |
+
)
|
| 413 |
+
if noisy:
|
| 414 |
+
kwargs["noisy_wavelengths"] = self.noisy_wavelengths
|
| 415 |
+
kwargs["noise_std"] = self.noise_std
|
| 416 |
+
return cls(**kwargs)
|
| 417 |
+
|
| 418 |
+
def setup(self, stage=None):
|
| 419 |
+
self.train_ds = self._make_dataset(
|
| 420 |
+
self.aia_train_dir, self.sxr_train_dir, noisy=self.apply_noise_to_train
|
| 421 |
+
)
|
| 422 |
+
self.val_ds = self._make_dataset(self.aia_val_dir, self.sxr_val_dir, noisy=True)
|
| 423 |
+
self.test_ds = self._make_dataset(self.aia_test_dir, self.sxr_test_dir, noisy=True)
|
| 424 |
+
|
| 425 |
+
def train_dataloader(self):
|
| 426 |
+
return DataLoader(self.train_ds, batch_size=self.batch_size,
|
| 427 |
+
shuffle=True, num_workers=self.num_workers, prefetch_factor=4)
|
| 428 |
+
|
| 429 |
+
def val_dataloader(self):
|
| 430 |
+
return DataLoader(self.val_ds, batch_size=self.batch_size,
|
| 431 |
+
shuffle=False, num_workers=self.num_workers, prefetch_factor=4)
|
| 432 |
+
|
| 433 |
+
def test_dataloader(self):
|
| 434 |
+
return DataLoader(self.test_ds, batch_size=self.batch_size,
|
| 435 |
+
shuffle=False, num_workers=self.num_workers, prefetch_factor=1)
|
| 436 |
+
|
| 437 |
+
|
| 438 |
class AIA_GOESDataModule(LightningDataModule):
|
| 439 |
"""
|
| 440 |
PyTorch Lightning DataModule for managing AIA-GOES datasets.
|
forecasting/inference/ablation_inference.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Ablation Inference Script — Gaussian Noise Channel Masking
|
| 3 |
+
==========================================================
|
| 4 |
+
|
| 5 |
+
Runs inference with the pretrained model while applying Gaussian noise to
|
| 6 |
+
specific AIA wavelength channels, one condition at a time. This lets you
|
| 7 |
+
measure how much each channel (or combination of channels) contributes to
|
| 8 |
+
forecast skill.
|
| 9 |
+
|
| 10 |
+
Each ablation condition produces its own output CSV so results can be
|
| 11 |
+
compared directly against the clean baseline.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import argparse
|
| 15 |
+
import re
|
| 16 |
+
import sys
|
| 17 |
+
import gc
|
| 18 |
+
import pandas as pd
|
| 19 |
+
import torch
|
| 20 |
+
import numpy as np
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
import yaml
|
| 23 |
+
from tqdm import tqdm
|
| 24 |
+
|
| 25 |
+
PROJECT_ROOT = Path(__file__).parent.parent.parent.absolute()
|
| 26 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 27 |
+
|
| 28 |
+
from forecasting.data_loaders.SDOAIA_dataloader import AIA_GOESDataset, NoisyAIA_GOESDataset
|
| 29 |
+
import forecasting.models as models
|
| 30 |
+
# Reuse inference helpers from the main inference script
|
| 31 |
+
from forecasting.inference.inference import load_model_from_config, evaluate_model_on_dataset
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def build_dataset(config_data, noisy_wavelengths, noise_std):
|
| 35 |
+
"""
|
| 36 |
+
Build an AIA_GOESDataset (or NoisyAIA_GOESDataset if wavelengths are given).
|
| 37 |
+
|
| 38 |
+
Parameters
|
| 39 |
+
----------
|
| 40 |
+
config_data : dict
|
| 41 |
+
noisy_wavelengths : list of int
|
| 42 |
+
Empty list → clean baseline (plain AIA_GOESDataset).
|
| 43 |
+
noise_std : float or dict
|
| 44 |
+
|
| 45 |
+
Returns
|
| 46 |
+
-------
|
| 47 |
+
dataset : torch.utils.data.Dataset
|
| 48 |
+
"""
|
| 49 |
+
aia_dir = config_data['data']['aia_dir']
|
| 50 |
+
sxr_dir = config_data['data']['sxr_dir']
|
| 51 |
+
wavelengths = config_data['wavelengths']
|
| 52 |
+
prediction_only = config_data.get('prediction_only', 'false').lower() == 'true'
|
| 53 |
+
|
| 54 |
+
common_kwargs = dict(
|
| 55 |
+
aia_dir=aia_dir,
|
| 56 |
+
sxr_dir=sxr_dir,
|
| 57 |
+
wavelengths=wavelengths,
|
| 58 |
+
only_prediction=prediction_only,
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
if noisy_wavelengths:
|
| 62 |
+
return NoisyAIA_GOESDataset(
|
| 63 |
+
**common_kwargs,
|
| 64 |
+
noisy_wavelengths=noisy_wavelengths,
|
| 65 |
+
noise_std=noise_std,
|
| 66 |
+
)
|
| 67 |
+
else:
|
| 68 |
+
return AIA_GOESDataset(**common_kwargs)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def run_condition(model, dataset, label, config_data, model_params, output_dir):
|
| 72 |
+
"""
|
| 73 |
+
Run inference for a single ablation condition and save results to CSV.
|
| 74 |
+
|
| 75 |
+
Parameters
|
| 76 |
+
----------
|
| 77 |
+
model : torch.nn.Module
|
| 78 |
+
dataset : torch.utils.data.Dataset
|
| 79 |
+
label : str
|
| 80 |
+
Human-readable name for this condition (used as filename stem).
|
| 81 |
+
config_data : dict
|
| 82 |
+
model_params : dict
|
| 83 |
+
output_dir : Path
|
| 84 |
+
"""
|
| 85 |
+
times = dataset.samples
|
| 86 |
+
total_samples = len(times)
|
| 87 |
+
batch_size = model_params.get('batch_size', 10)
|
| 88 |
+
input_size = model_params.get('input_size', 512)
|
| 89 |
+
patch_size = model_params.get('patch_size', 16)
|
| 90 |
+
save_weights = not model_params.get('no_weights', True)
|
| 91 |
+
save_flux = not model_params.get('no_flux', True)
|
| 92 |
+
prediction_only = config_data.get('prediction_only', 'false').lower() == 'true'
|
| 93 |
+
|
| 94 |
+
print(f"\n{'='*60}")
|
| 95 |
+
print(f" Condition: {label} ({total_samples} samples)")
|
| 96 |
+
print(f"{'='*60}")
|
| 97 |
+
|
| 98 |
+
timestamps, predictions, ground_truths = [], [], []
|
| 99 |
+
|
| 100 |
+
pbar = tqdm(
|
| 101 |
+
evaluate_model_on_dataset(
|
| 102 |
+
model, dataset, batch_size, times, config_data,
|
| 103 |
+
save_weights, input_size, patch_size, save_flux,
|
| 104 |
+
),
|
| 105 |
+
total=total_samples,
|
| 106 |
+
desc=label,
|
| 107 |
+
unit="sample",
|
| 108 |
+
ncols=100,
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
for prediction, sxr, _weight, _flux, idx in pbar:
|
| 112 |
+
predictions.append(float(prediction.item() if hasattr(prediction, 'item') else prediction))
|
| 113 |
+
ground_truths.append(0.0 if prediction_only else
|
| 114 |
+
float(sxr.item() if hasattr(sxr, 'item') else sxr))
|
| 115 |
+
timestamps.append(str(times[idx]))
|
| 116 |
+
|
| 117 |
+
output_path = output_dir / f"{label}.csv"
|
| 118 |
+
pd.DataFrame({
|
| 119 |
+
'timestamp': timestamps,
|
| 120 |
+
'predictions': predictions,
|
| 121 |
+
'groundtruth': ground_truths,
|
| 122 |
+
}).to_csv(output_path, index=False)
|
| 123 |
+
|
| 124 |
+
print(f" Saved → {output_path}")
|
| 125 |
+
|
| 126 |
+
# Free dataset memory between conditions
|
| 127 |
+
gc.collect()
|
| 128 |
+
if torch.cuda.is_available():
|
| 129 |
+
torch.cuda.empty_cache()
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def resolve_config_variables(config_dict):
|
| 133 |
+
"""Recursively resolve ${variable} references within config."""
|
| 134 |
+
variables = {k: v for k, v in config_dict.items()
|
| 135 |
+
if isinstance(v, str) and not v.startswith('${')}
|
| 136 |
+
|
| 137 |
+
def substitute(obj):
|
| 138 |
+
if isinstance(obj, dict):
|
| 139 |
+
return {k: substitute(v) for k, v in obj.items()}
|
| 140 |
+
elif isinstance(obj, list):
|
| 141 |
+
return [substitute(item) for item in obj]
|
| 142 |
+
elif isinstance(obj, str):
|
| 143 |
+
for match in re.finditer(r'\$\{([^}]+)\}', obj):
|
| 144 |
+
var = match.group(1)
|
| 145 |
+
if var in variables:
|
| 146 |
+
obj = obj.replace(f'${{{var}}}', variables[var])
|
| 147 |
+
return obj
|
| 148 |
+
return obj
|
| 149 |
+
|
| 150 |
+
return substitute(config_dict)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def main():
|
| 154 |
+
parser = argparse.ArgumentParser(description="Ablation inference with Gaussian noise masking.")
|
| 155 |
+
parser.add_argument('-config', type=str, required=True,
|
| 156 |
+
help='Path to the ablation inference YAML config.')
|
| 157 |
+
args = parser.parse_args()
|
| 158 |
+
|
| 159 |
+
with open(args.config, 'r') as f:
|
| 160 |
+
config_data = yaml.load(f, Loader=yaml.SafeLoader)
|
| 161 |
+
config_data = resolve_config_variables(config_data)
|
| 162 |
+
sys.modules['models'] = models
|
| 163 |
+
|
| 164 |
+
model_params = config_data.get('model_params', {})
|
| 165 |
+
ablation_cfg = config_data.get('ablation', {})
|
| 166 |
+
noise_std = ablation_cfg.get('noise_std', 1.0)
|
| 167 |
+
conditions = ablation_cfg.get('conditions', [])
|
| 168 |
+
|
| 169 |
+
if not conditions:
|
| 170 |
+
raise ValueError("No ablation conditions defined in config under 'ablation.conditions'.")
|
| 171 |
+
|
| 172 |
+
output_dir = Path(config_data['output_dir'])
|
| 173 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 174 |
+
|
| 175 |
+
print(f"Loading model...")
|
| 176 |
+
model = load_model_from_config(config_data)
|
| 177 |
+
|
| 178 |
+
for condition in conditions:
|
| 179 |
+
label = condition.get('label', 'unnamed')
|
| 180 |
+
noisy_wavelengths = condition.get('wavelengths', [])
|
| 181 |
+
|
| 182 |
+
dataset = build_dataset(config_data, noisy_wavelengths, noise_std)
|
| 183 |
+
run_condition(model, dataset, label, config_data, model_params, output_dir)
|
| 184 |
+
|
| 185 |
+
print(f"\nAll conditions complete. Results saved to: {output_dir}")
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
if __name__ == '__main__':
|
| 189 |
+
main()
|
forecasting/inference/ablation_inference_config.yaml
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =============================================================================
|
| 2 |
+
# FOXES Ablation Inference Configuration
|
| 3 |
+
# =============================================================================
|
| 4 |
+
# Used by ablation_inference.py to run Gaussian noise channel-masking ablation.
|
| 5 |
+
#
|
| 6 |
+
# Usage:
|
| 7 |
+
# python ablation_inference.py -config ablation_inference_config.yaml
|
| 8 |
+
#
|
| 9 |
+
# Variables
|
| 10 |
+
# ---------
|
| 11 |
+
# Define top-level string keys and reference them anywhere with ${key}.
|
| 12 |
+
|
| 13 |
+
base_dir: "/Volumes/T9/FOXES_Data"
|
| 14 |
+
checkpoint: "/Users/griffingoodwin/Downloads/FOXES_Model_Checkpoint.ckpt"
|
| 15 |
+
|
| 16 |
+
model: "ViTLocal"
|
| 17 |
+
wavelengths: [94, 131, 171, 193, 211, 304, 335]
|
| 18 |
+
prediction_only: "false"
|
| 19 |
+
|
| 20 |
+
data:
|
| 21 |
+
aia_dir: "${base_dir}/AIA_processed/val"
|
| 22 |
+
sxr_dir: "${base_dir}/SXR_processed/val"
|
| 23 |
+
sxr_norm_path: "${base_dir}/SXR_processed/normalized_sxr.npy"
|
| 24 |
+
checkpoint_path: "${checkpoint}"
|
| 25 |
+
|
| 26 |
+
# Output directory — each condition saves <label>.csv here
|
| 27 |
+
output_dir: "${base_dir}/inference/ablation"
|
| 28 |
+
|
| 29 |
+
model_params:
|
| 30 |
+
input_size: 512
|
| 31 |
+
patch_size: 8
|
| 32 |
+
batch_size: 10
|
| 33 |
+
no_weights: true # skip attention saving for speed
|
| 34 |
+
no_flux: true
|
| 35 |
+
|
| 36 |
+
# -----------------------------------------------------------------------------
|
| 37 |
+
# Ablation conditions
|
| 38 |
+
# -----------------------------------------------------------------------------
|
| 39 |
+
# noise_std: standard deviation of Gaussian noise applied in normalized tensor space.
|
| 40 |
+
# Each condition names a wavelength subset to corrupt; empty list = clean baseline.
|
| 41 |
+
# You can also pass a per-wavelength dict, e.g. {171: 0.5, 193: 2.0}
|
| 42 |
+
ablation:
|
| 43 |
+
noise_std: 1.0
|
| 44 |
+
conditions:
|
| 45 |
+
- label: baseline
|
| 46 |
+
wavelengths: []
|
| 47 |
+
- label: ablate_94
|
| 48 |
+
wavelengths: [94]
|
| 49 |
+
- label: ablate_131
|
| 50 |
+
wavelengths: [131]
|
| 51 |
+
- label: ablate_171
|
| 52 |
+
wavelengths: [171]
|
| 53 |
+
- label: ablate_193
|
| 54 |
+
wavelengths: [193]
|
| 55 |
+
- label: ablate_211
|
| 56 |
+
wavelengths: [211]
|
| 57 |
+
- label: ablate_304
|
| 58 |
+
wavelengths: [304]
|
| 59 |
+
- label: ablate_335
|
| 60 |
+
wavelengths: [335]
|
| 61 |
+
- label: ablate_all
|
| 62 |
+
wavelengths: [94, 131, 171, 193, 211, 304, 335]
|
pipeline_config.yaml
CHANGED
|
@@ -7,6 +7,14 @@
|
|
| 7 |
# python run_pipeline.py --config pipeline_config.yaml --steps all
|
| 8 |
# python run_pipeline.py --config pipeline_config.yaml --steps train,inference,flare_analysis
|
| 9 |
# python run_pipeline.py --list
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
# -----------------------------------------------------------------------------
|
| 12 |
# HuggingFace download (step: hf_download)
|
|
@@ -14,7 +22,7 @@
|
|
| 14 |
# Use this instead of download_aia + download_sxr + preprocess + split.
|
| 15 |
# -----------------------------------------------------------------------------
|
| 16 |
hf_download:
|
| 17 |
-
config: "download/hf_download_config.yaml"
|
| 18 |
|
| 19 |
# -----------------------------------------------------------------------------
|
| 20 |
# Shared date range (used by download_aia and download_sxr)
|
|
@@ -26,36 +34,36 @@ end_date: "2014-07-08 00:00:00"
|
|
| 26 |
# AIA download (step: download_aia)
|
| 27 |
# -----------------------------------------------------------------------------
|
| 28 |
aia:
|
| 29 |
-
download_dir: "/
|
| 30 |
email: "" # Must be registered at http://jsoc.stanford.edu
|
| 31 |
-
cadence: 1
|
| 32 |
|
| 33 |
# -----------------------------------------------------------------------------
|
| 34 |
# SXR download (step: download_sxr)
|
| 35 |
# -----------------------------------------------------------------------------
|
| 36 |
sxr:
|
| 37 |
-
save_dir: "/
|
| 38 |
|
| 39 |
# -----------------------------------------------------------------------------
|
| 40 |
# Preprocessing (step: preprocess)
|
| 41 |
# -----------------------------------------------------------------------------
|
| 42 |
preprocess:
|
| 43 |
-
config: "data/pipeline_config.yaml"
|
| 44 |
|
| 45 |
# -----------------------------------------------------------------------------
|
| 46 |
# SXR normalization (step: normalize)
|
| 47 |
# -----------------------------------------------------------------------------
|
| 48 |
normalize:
|
| 49 |
-
sxr_dir: "/
|
| 50 |
-
output_path: "/
|
| 51 |
|
| 52 |
# -----------------------------------------------------------------------------
|
| 53 |
# Train/val/test split (step: split)
|
| 54 |
# Runs split_data.py once for AIA and once for SXR.
|
| 55 |
# -----------------------------------------------------------------------------
|
| 56 |
split:
|
| 57 |
-
aia_input_dir: "/
|
| 58 |
-
sxr_input_dir: "/
|
| 59 |
train_start: "2014-07-01"
|
| 60 |
train_end: "2014-07-05"
|
| 61 |
val_start: "2014-07-06"
|
|
@@ -68,14 +76,14 @@ split:
|
|
| 68 |
# -----------------------------------------------------------------------------
|
| 69 |
train:
|
| 70 |
config: "forecasting/training/train_config.yaml"
|
| 71 |
-
overrides:
|
| 72 |
-
base_data_dir:
|
| 73 |
-
base_checkpoint_dir: "
|
| 74 |
epochs: 150
|
| 75 |
batch_size: 6
|
| 76 |
wandb:
|
| 77 |
run_name: "pipeline-run"
|
| 78 |
-
entity: jayantbiradar619-university-of-arizona
|
| 79 |
project: Paper
|
| 80 |
job_type: training
|
| 81 |
tags:
|
|
@@ -89,32 +97,53 @@ train:
|
|
| 89 |
# -----------------------------------------------------------------------------
|
| 90 |
inference:
|
| 91 |
config: "forecasting/inference/local_config.yaml"
|
| 92 |
-
overrides:
|
| 93 |
data:
|
| 94 |
-
aia_dir: "/
|
| 95 |
-
sxr_dir: "/
|
| 96 |
-
sxr_norm_path: "/
|
| 97 |
-
checkpoint_path: "
|
| 98 |
-
output_path: "/
|
| 99 |
prediction_only: "false"
|
| 100 |
paths:
|
| 101 |
-
data_dir: "
|
| 102 |
-
predictions_csv: "/
|
| 103 |
-
aia_path: "/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
# -----------------------------------------------------------------------------
|
| 106 |
# Evaluation (step: evaluate)
|
| 107 |
# -----------------------------------------------------------------------------
|
| 108 |
evaluate:
|
| 109 |
config: "forecasting/inference/evaluation_config.yaml"
|
| 110 |
-
overrides:
|
| 111 |
model_predictions:
|
| 112 |
-
main_model_csv: "/
|
| 113 |
data:
|
| 114 |
-
aia_dir: "/
|
| 115 |
-
weight_path: "/
|
| 116 |
evaluation:
|
| 117 |
-
output_dir: "/
|
| 118 |
time_range:
|
| 119 |
start_time: "2023-01-01T00:00:00"
|
| 120 |
end_time: "2023-12-31T23:59:59"
|
|
|
|
| 7 |
# python run_pipeline.py --config pipeline_config.yaml --steps all
|
| 8 |
# python run_pipeline.py --config pipeline_config.yaml --steps train,inference,flare_analysis
|
| 9 |
# python run_pipeline.py --list
|
| 10 |
+
#
|
| 11 |
+
# Variables
|
| 12 |
+
# ---------
|
| 13 |
+
# Define top-level string keys and reference them anywhere with ${key}.
|
| 14 |
+
# Change base_dir or checkpoint once and every path updates automatically.
|
| 15 |
+
|
| 16 |
+
base_dir: "/Volumes/T9/FOXES_Data"
|
| 17 |
+
checkpoint: "/Users/griffingoodwin/Downloads/FOXES_Model_Checkpoint.ckpt"
|
| 18 |
|
| 19 |
# -----------------------------------------------------------------------------
|
| 20 |
# HuggingFace download (step: hf_download)
|
|
|
|
| 22 |
# Use this instead of download_aia + download_sxr + preprocess + split.
|
| 23 |
# -----------------------------------------------------------------------------
|
| 24 |
hf_download:
|
| 25 |
+
config: "download/hf_download_config.yaml"
|
| 26 |
|
| 27 |
# -----------------------------------------------------------------------------
|
| 28 |
# Shared date range (used by download_aia and download_sxr)
|
|
|
|
| 34 |
# AIA download (step: download_aia)
|
| 35 |
# -----------------------------------------------------------------------------
|
| 36 |
aia:
|
| 37 |
+
download_dir: "${base_dir}/AIA_raw"
|
| 38 |
email: "" # Must be registered at http://jsoc.stanford.edu
|
| 39 |
+
cadence: 1 # Minutes between frames
|
| 40 |
|
| 41 |
# -----------------------------------------------------------------------------
|
| 42 |
# SXR download (step: download_sxr)
|
| 43 |
# -----------------------------------------------------------------------------
|
| 44 |
sxr:
|
| 45 |
+
save_dir: "${base_dir}/SXR_raw"
|
| 46 |
|
| 47 |
# -----------------------------------------------------------------------------
|
| 48 |
# Preprocessing (step: preprocess)
|
| 49 |
# -----------------------------------------------------------------------------
|
| 50 |
preprocess:
|
| 51 |
+
config: "data/pipeline_config.yaml"
|
| 52 |
|
| 53 |
# -----------------------------------------------------------------------------
|
| 54 |
# SXR normalization (step: normalize)
|
| 55 |
# -----------------------------------------------------------------------------
|
| 56 |
normalize:
|
| 57 |
+
sxr_dir: "${base_dir}/SXR_processed/train"
|
| 58 |
+
output_path: "${base_dir}/SXR_processed/normalized_sxr.npy"
|
| 59 |
|
| 60 |
# -----------------------------------------------------------------------------
|
| 61 |
# Train/val/test split (step: split)
|
| 62 |
# Runs split_data.py once for AIA and once for SXR.
|
| 63 |
# -----------------------------------------------------------------------------
|
| 64 |
split:
|
| 65 |
+
aia_input_dir: "${base_dir}/AIA_processed"
|
| 66 |
+
sxr_input_dir: "${base_dir}/SXR_processed"
|
| 67 |
train_start: "2014-07-01"
|
| 68 |
train_end: "2014-07-05"
|
| 69 |
val_start: "2014-07-06"
|
|
|
|
| 76 |
# -----------------------------------------------------------------------------
|
| 77 |
train:
|
| 78 |
config: "forecasting/training/train_config.yaml"
|
| 79 |
+
overrides:
|
| 80 |
+
base_data_dir: "${base_dir}"
|
| 81 |
+
base_checkpoint_dir: "${base_dir}"
|
| 82 |
epochs: 150
|
| 83 |
batch_size: 6
|
| 84 |
wandb:
|
| 85 |
run_name: "pipeline-run"
|
| 86 |
+
entity: jayantbiradar619-university-of-arizona
|
| 87 |
project: Paper
|
| 88 |
job_type: training
|
| 89 |
tags:
|
|
|
|
| 97 |
# -----------------------------------------------------------------------------
|
| 98 |
inference:
|
| 99 |
config: "forecasting/inference/local_config.yaml"
|
| 100 |
+
overrides:
|
| 101 |
data:
|
| 102 |
+
aia_dir: "${base_dir}/AIA_processed"
|
| 103 |
+
sxr_dir: "${base_dir}/SXR_processed"
|
| 104 |
+
sxr_norm_path: "${base_dir}/SXR_processed/normalized_sxr.npy"
|
| 105 |
+
checkpoint_path: "${checkpoint}"
|
| 106 |
+
output_path: "${base_dir}/inference/predictions.csv"
|
| 107 |
prediction_only: "false"
|
| 108 |
paths:
|
| 109 |
+
data_dir: "${base_dir}"
|
| 110 |
+
predictions_csv: "${base_dir}/inference/predictions.csv"
|
| 111 |
+
aia_path: "${base_dir}/AIA_processed/test"
|
| 112 |
+
|
| 113 |
+
# -----------------------------------------------------------------------------
|
| 114 |
+
# Ablation study (step: ablation)
|
| 115 |
+
# -----------------------------------------------------------------------------
|
| 116 |
+
ablation:
|
| 117 |
+
config: "forecasting/inference/ablation_inference_config.yaml"
|
| 118 |
+
overrides:
|
| 119 |
+
data:
|
| 120 |
+
aia_dir: "${base_dir}/AIA_processed/test"
|
| 121 |
+
sxr_dir: "${base_dir}/SXR_processed/test"
|
| 122 |
+
sxr_norm_path: "${base_dir}/SXR_processed/normalized_sxr.npy"
|
| 123 |
+
checkpoint_path: "${checkpoint}"
|
| 124 |
+
output_dir: "${base_dir}/inference/ablation"
|
| 125 |
+
|
| 126 |
+
# -----------------------------------------------------------------------------
|
| 127 |
+
# Spatial performance (step: spatial_performance)
|
| 128 |
+
# -----------------------------------------------------------------------------
|
| 129 |
+
spatial_performance:
|
| 130 |
+
flux_dir: "${base_dir}/flux"
|
| 131 |
+
predictions_csv: "${base_dir}/inference/predictions.csv"
|
| 132 |
+
out_dir: "${base_dir}/inference/spatial_performance"
|
| 133 |
|
| 134 |
# -----------------------------------------------------------------------------
|
| 135 |
# Evaluation (step: evaluate)
|
| 136 |
# -----------------------------------------------------------------------------
|
| 137 |
evaluate:
|
| 138 |
config: "forecasting/inference/evaluation_config.yaml"
|
| 139 |
+
overrides:
|
| 140 |
model_predictions:
|
| 141 |
+
main_model_csv: "${base_dir}/inference/predictions.csv"
|
| 142 |
data:
|
| 143 |
+
aia_dir: "${base_dir}/AIA_processed/val"
|
| 144 |
+
weight_path: "${base_dir}/inference/weights"
|
| 145 |
evaluation:
|
| 146 |
+
output_dir: "${base_dir}/inference/evaluation"
|
| 147 |
time_range:
|
| 148 |
start_time: "2023-01-01T00:00:00"
|
| 149 |
end_time: "2023-12-31T23:59:59"
|
run_pipeline.py
CHANGED
|
@@ -23,6 +23,7 @@ Usage:
|
|
| 23 |
|
| 24 |
import argparse
|
| 25 |
import logging
|
|
|
|
| 26 |
import subprocess
|
| 27 |
import sys
|
| 28 |
import time
|
|
@@ -47,6 +48,33 @@ log = logging.getLogger(__name__)
|
|
| 47 |
# Config helpers
|
| 48 |
# ---------------------------------------------------------------------------
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
def deep_merge(base: dict, overrides: dict) -> dict:
|
| 51 |
"""Recursively merge overrides into base, modifying base in-place."""
|
| 52 |
for key, val in overrides.items():
|
|
@@ -88,6 +116,8 @@ STEP_ORDER = [
|
|
| 88 |
"inference",
|
| 89 |
"evaluate",
|
| 90 |
"flare_analysis",
|
|
|
|
|
|
|
| 91 |
]
|
| 92 |
|
| 93 |
STEP_INFO = {
|
|
@@ -135,6 +165,14 @@ STEP_INFO = {
|
|
| 135 |
"description": "Detect, track, and match flares; generate plots/movies",
|
| 136 |
"script": ROOT / "forecasting" / "inference" / "flare_analysis.py",
|
| 137 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
}
|
| 139 |
|
| 140 |
|
|
@@ -268,6 +306,26 @@ def build_commands(step: str, cfg: dict, force: bool) -> list[list[str]] | None:
|
|
| 268 |
config_path = str(write_merged_config(config_path, inf["overrides"], "inference_config"))
|
| 269 |
return [base + ["--config", config_path]]
|
| 270 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
return [base]
|
| 272 |
|
| 273 |
|
|
@@ -338,6 +396,7 @@ def main():
|
|
| 338 |
|
| 339 |
with open(args.config, "r") as f:
|
| 340 |
cfg = yaml.safe_load(f)
|
|
|
|
| 341 |
|
| 342 |
# Resolve step list
|
| 343 |
if args.steps.strip().lower() == "all":
|
|
|
|
| 23 |
|
| 24 |
import argparse
|
| 25 |
import logging
|
| 26 |
+
import re
|
| 27 |
import subprocess
|
| 28 |
import sys
|
| 29 |
import time
|
|
|
|
| 48 |
# Config helpers
|
| 49 |
# ---------------------------------------------------------------------------
|
| 50 |
|
| 51 |
+
def resolve_variables(cfg: dict) -> dict:
|
| 52 |
+
"""
|
| 53 |
+
Resolve ${variable} placeholders in a config dict.
|
| 54 |
+
|
| 55 |
+
Top-level string keys whose values don't themselves contain placeholders
|
| 56 |
+
are treated as variables. Substitution is applied recursively to all
|
| 57 |
+
string values in the config.
|
| 58 |
+
"""
|
| 59 |
+
variables = {k: v for k, v in cfg.items()
|
| 60 |
+
if isinstance(v, str) and '${' not in v}
|
| 61 |
+
|
| 62 |
+
def substitute(obj):
|
| 63 |
+
if isinstance(obj, dict):
|
| 64 |
+
return {k: substitute(v) for k, v in obj.items()}
|
| 65 |
+
elif isinstance(obj, list):
|
| 66 |
+
return [substitute(item) for item in obj]
|
| 67 |
+
elif isinstance(obj, str):
|
| 68 |
+
for match in re.finditer(r'\$\{([^}]+)\}', obj):
|
| 69 |
+
var = match.group(1)
|
| 70 |
+
if var in variables:
|
| 71 |
+
obj = obj.replace(f'${{{var}}}', variables[var])
|
| 72 |
+
return obj
|
| 73 |
+
return obj
|
| 74 |
+
|
| 75 |
+
return substitute(cfg)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
def deep_merge(base: dict, overrides: dict) -> dict:
|
| 79 |
"""Recursively merge overrides into base, modifying base in-place."""
|
| 80 |
for key, val in overrides.items():
|
|
|
|
| 116 |
"inference",
|
| 117 |
"evaluate",
|
| 118 |
"flare_analysis",
|
| 119 |
+
"ablation",
|
| 120 |
+
"spatial_performance",
|
| 121 |
]
|
| 122 |
|
| 123 |
STEP_INFO = {
|
|
|
|
| 165 |
"description": "Detect, track, and match flares; generate plots/movies",
|
| 166 |
"script": ROOT / "forecasting" / "inference" / "flare_analysis.py",
|
| 167 |
},
|
| 168 |
+
"ablation": {
|
| 169 |
+
"description": "Run Gaussian noise channel-masking ablation study on pretrained model",
|
| 170 |
+
"script": ROOT / "forecasting" / "inference" / "ablation_inference.py",
|
| 171 |
+
},
|
| 172 |
+
"spatial_performance": {
|
| 173 |
+
"description": "Generate flux-weighted spatial error heatmap on the solar disk",
|
| 174 |
+
"script": ROOT / "analysis" / "spatial_performance.py",
|
| 175 |
+
},
|
| 176 |
}
|
| 177 |
|
| 178 |
|
|
|
|
| 306 |
config_path = str(write_merged_config(config_path, inf["overrides"], "inference_config"))
|
| 307 |
return [base + ["--config", config_path]]
|
| 308 |
|
| 309 |
+
if step == "ablation":
|
| 310 |
+
if not require(["config"], "ablation"):
|
| 311 |
+
return None
|
| 312 |
+
abl = cfg["ablation"]
|
| 313 |
+
config_path = abl["config"]
|
| 314 |
+
if abl.get("overrides"):
|
| 315 |
+
config_path = str(write_merged_config(config_path, abl["overrides"], "ablation_config"))
|
| 316 |
+
return [base + ["-config", config_path]]
|
| 317 |
+
|
| 318 |
+
if step == "spatial_performance":
|
| 319 |
+
sp = cfg.get("spatial_performance", {})
|
| 320 |
+
cmd = base[:]
|
| 321 |
+
if sp.get("flux_dir"):
|
| 322 |
+
cmd += ["--flux_dir", sp["flux_dir"]]
|
| 323 |
+
if sp.get("predictions_csv"):
|
| 324 |
+
cmd += ["--predictions_csv", sp["predictions_csv"]]
|
| 325 |
+
if sp.get("out_dir"):
|
| 326 |
+
cmd += ["--out_dir", sp["out_dir"]]
|
| 327 |
+
return [cmd]
|
| 328 |
+
|
| 329 |
return [base]
|
| 330 |
|
| 331 |
|
|
|
|
| 396 |
|
| 397 |
with open(args.config, "r") as f:
|
| 398 |
cfg = yaml.safe_load(f)
|
| 399 |
+
cfg = resolve_variables(cfg)
|
| 400 |
|
| 401 |
# Resolve step list
|
| 402 |
if args.steps.strip().lower() == "all":
|