Commit ·
dc04e07
1
Parent(s): a1e0076
refactiring rest of code base and adding checkpoints
Browse files- .gitignore +1 -0
- README.md +65 -20
- data/align_aia_sxr.py +4 -1
- data/build_dataset.py +30 -16
- data/build_dataset_config.yaml +28 -15
- data/clean_aia.py +40 -12
- data/combine_sxr.py +16 -12
- data/convert_aia.py +8 -8
- data/split_train_val_test.py +101 -0
- download/download_sdo.py +31 -9
- download/download_sxr.py +1 -1
- download/hf_download_config.yaml +8 -0
- download/hugging_face_data_download.py +172 -30
- download/sdo_download_config.yaml +4 -4
- download/sxr_download_config.yaml +4 -4
- forecasting/dataset.py +55 -0
- forecasting/evaluation.py +2 -2
- forecasting/evaluation_config.yaml +1 -1
- forecasting/inference.py +2 -4
- forecasting/inference_config.yaml +1 -1
- forecasting/trained_weights_and_normalization/{FOXES_Model_Checkpoint.ckpt → inverted-attention-mask.ckpt} +0 -0
- forecasting/trained_weights_and_normalization/localized-attention-mask.ckpt +3 -0
- forecasting/trained_weights_and_normalization/no-attention-mask.ckpt +3 -0
- training/__init__.py +0 -0
- training/callbacks.py +226 -0
- training/train.py +224 -0
- training/train_config.yaml +61 -0
.gitignore
CHANGED
|
@@ -172,3 +172,4 @@ analysis/performance_heatmap_all.png
|
|
| 172 |
download/download_flares.py
|
| 173 |
download/test_download_sdo.py
|
| 174 |
forecasting/models/test_vit_mask.py
|
|
|
|
|
|
| 172 |
download/download_flares.py
|
| 173 |
download/test_download_sdo.py
|
| 174 |
forecasting/models/test_vit_mask.py
|
| 175 |
+
forecasting/griffin_config.yaml
|
README.md
CHANGED
|
@@ -46,20 +46,18 @@ The solar soft X-ray (SXR) irradiance is a long-standing proxy of solar activity
|
|
| 46 |
|
| 47 |
## Repository Structure
|
| 48 |
|
| 49 |
-
This repository
|
| 50 |
-
and evaluating
|
| 51 |
-
code used to produce the released checkpoint.
|
| 52 |
|
| 53 |
```text
|
| 54 |
FOXES
|
| 55 |
├── download
|
| 56 |
-
│ ├── hugging_face_data_download.py # Recommended:
|
| 57 |
-
│ ├── parquet_to_npy.py # Convert already-downloaded HF parquet files to .npy
|
| 58 |
│ ├── hf_download_config.yaml # Config for hugging_face_data_download.py
|
| 59 |
│ ├── download_sdo.py # Advanced: raw AIA download from JSOC (needs data/build_dataset.py after)
|
| 60 |
│ ├── sdo_download_config.yaml # Config for download_sdo.py
|
| 61 |
-
│ ├──
|
| 62 |
-
│ └── sxr_download_config.yaml # Config for
|
| 63 |
├── data
|
| 64 |
│ ├── build_dataset.py # Runs the full raw -> processed pipeline below in one command
|
| 65 |
│ ├── build_dataset_config.yaml # Config for build_dataset.py
|
|
@@ -67,14 +65,19 @@ FOXES
|
|
| 67 |
│ ├── convert_aia.py # Raw AIA FITS -> paired 512x512 .npy stacks (itipy)
|
| 68 |
│ ├── combine_sxr.py # Combine raw multi-satellite GOES files into per-satellite CSVs
|
| 69 |
│ ├── align_aia_sxr.py # Match AIA timestamps to GOES CSVs -> per-timestamp SXR .npy
|
|
|
|
| 70 |
│ └── sxr_normalization.py # Compute log-space mean/std over SXR .npy files for training
|
| 71 |
├── forecasting
|
| 72 |
-
│ ├── dataset.py #
|
| 73 |
│ ├── model.py # ViTLocal: Vision Transformer with patch flux heads
|
| 74 |
│ ├── inference.py # Run a checkpoint over a folder of data; writes predictions.csv
|
| 75 |
│ ├── inference_config.yaml # Config for inference.py
|
| 76 |
│ ├── evaluation.py # Compute metrics and generate evaluation plots
|
| 77 |
│ └── evaluation_config.yaml # Config for evaluation.py
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
└── requirements.txt # Python dependencies
|
| 79 |
```
|
| 80 |
|
|
@@ -107,9 +110,10 @@ conda activate foxes
|
|
| 107 |
|
| 108 |
## Running the Model
|
| 109 |
|
| 110 |
-
FOXES is run in
|
| 111 |
-
your data),
|
| 112 |
-
|
|
|
|
| 113 |
|
| 114 |
### 0) Get data
|
| 115 |
|
|
@@ -124,7 +128,9 @@ Edit `download/hf_download_config.yaml` first to set `aia_dir`/`sxr_dir`, which
|
|
| 124 |
splits to pull, and whether to subsample.
|
| 125 |
|
| 126 |
If you've already downloaded the HF parquet files locally instead of
|
| 127 |
-
streaming,
|
|
|
|
|
|
|
| 128 |
|
| 129 |
**Advanced:** to acquire and process raw data yourself instead, edit and run
|
| 130 |
the two download configs, then the one dataset-build config, in order:
|
|
@@ -134,18 +140,22 @@ the two download configs, then the one dataset-build config, in order:
|
|
| 134 |
python download/download_sdo.py --config download/sdo_download_config.yaml
|
| 135 |
|
| 136 |
# 2) Raw GOES XRS data via SunPy Fido
|
| 137 |
-
python download/
|
| 138 |
|
| 139 |
# 3) Clean + convert AIA, combine + align SXR -> paired .npy (see data/build_dataset_config.yaml)
|
| 140 |
-
python data/build_dataset.py -config data/build_dataset_config.yaml
|
| 141 |
```
|
| 142 |
|
| 143 |
`data/build_dataset.py` runs the full raw-to-processed pipeline in one command
|
| 144 |
(clean AIA → convert AIA → combine GOES → align AIA/SXR); each step can be
|
| 145 |
-
skipped via the `steps:` block in its config if you've already run it.
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
### 1) Data format
|
| 151 |
|
|
@@ -186,7 +196,7 @@ output_path: "/path/to/predictions.csv"
|
|
| 186 |
Then run:
|
| 187 |
|
| 188 |
```bash
|
| 189 |
-
python forecasting/inference.py -config forecasting/inference_config.yaml
|
| 190 |
```
|
| 191 |
|
| 192 |
This writes `output_path` (a CSV of timestamp/prediction/groundtruth). Per-patch
|
|
@@ -201,11 +211,46 @@ Edit `forecasting/evaluation_config.yaml` to point at the predictions
|
|
| 201 |
CSV and data directories, then run:
|
| 202 |
|
| 203 |
```bash
|
| 204 |
-
python forecasting/evaluation.py -config forecasting/evaluation_config.yaml
|
| 205 |
```
|
| 206 |
|
| 207 |
This computes metrics (MSE, MAE, R²) and generates plots under `evaluation.output_dir`.
|
| 208 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
---
|
| 210 |
|
| 211 |
## Citation
|
|
|
|
| 46 |
|
| 47 |
## Repository Structure
|
| 48 |
|
| 49 |
+
This repository covers the full loop: getting data, training, running
|
| 50 |
+
inference, and evaluating a FOXES model.
|
|
|
|
| 51 |
|
| 52 |
```text
|
| 53 |
FOXES
|
| 54 |
├── download
|
| 55 |
+
│ ├── hugging_face_data_download.py # Recommended: HF Hub -> .npy (streamed, or from local parquet)
|
|
|
|
| 56 |
│ ├── hf_download_config.yaml # Config for hugging_face_data_download.py
|
| 57 |
│ ├── download_sdo.py # Advanced: raw AIA download from JSOC (needs data/build_dataset.py after)
|
| 58 |
│ ├── sdo_download_config.yaml # Config for download_sdo.py
|
| 59 |
+
│ ├── download_sxr.py # Advanced: raw GOES SXR download via SunPy Fido (needs data/build_dataset.py after)
|
| 60 |
+
│ └── sxr_download_config.yaml # Config for download_sxr.py
|
| 61 |
├── data
|
| 62 |
│ ├── build_dataset.py # Runs the full raw -> processed pipeline below in one command
|
| 63 |
│ ├── build_dataset_config.yaml # Config for build_dataset.py
|
|
|
|
| 65 |
│ ├── convert_aia.py # Raw AIA FITS -> paired 512x512 .npy stacks (itipy)
|
| 66 |
│ ├── combine_sxr.py # Combine raw multi-satellite GOES files into per-satellite CSVs
|
| 67 |
│ ├── align_aia_sxr.py # Match AIA timestamps to GOES CSVs -> per-timestamp SXR .npy
|
| 68 |
+
│ ├── split_train_val_test.py # Split processed AIA/SXR into train/val/test (training only)
|
| 69 |
│ └── sxr_normalization.py # Compute log-space mean/std over SXR .npy files for training
|
| 70 |
├── forecasting
|
| 71 |
+
│ ├── dataset.py # AIAGOESDataset / AIAGOESDataModule: loads paired AIA + SXR .npy files
|
| 72 |
│ ├── model.py # ViTLocal: Vision Transformer with patch flux heads
|
| 73 |
│ ├── inference.py # Run a checkpoint over a folder of data; writes predictions.csv
|
| 74 |
│ ├── inference_config.yaml # Config for inference.py
|
| 75 |
│ ├── evaluation.py # Compute metrics and generate evaluation plots
|
| 76 |
│ └── evaluation_config.yaml # Config for evaluation.py
|
| 77 |
+
├── training
|
| 78 |
+
│ ├── train.py # Train ViTLocal with PyTorch Lightning + Weights & Biases logging
|
| 79 |
+
│ ├── train_config.yaml # Config for train.py
|
| 80 |
+
│ └── callbacks.py # W&B callbacks: SXR pred-vs-true plots, attention map visualization
|
| 81 |
└── requirements.txt # Python dependencies
|
| 82 |
```
|
| 83 |
|
|
|
|
| 110 |
|
| 111 |
## Running the Model
|
| 112 |
|
| 113 |
+
FOXES is run in four steps: **get data**, **inference** (run a checkpoint over
|
| 114 |
+
your data), **evaluation** (score the predictions and generate plots), and
|
| 115 |
+
optionally **training** your own model. All are driven by a YAML config — edit
|
| 116 |
+
the config, then run the script.
|
| 117 |
|
| 118 |
### 0) Get data
|
| 119 |
|
|
|
|
| 128 |
splits to pull, and whether to subsample.
|
| 129 |
|
| 130 |
If you've already downloaded the HF parquet files locally instead of
|
| 131 |
+
streaming, set `local_parquet_dir` in that same config to the root folder
|
| 132 |
+
containing your per-split subdirs (`train/`, `validation/` or `val/`, `test/`)
|
| 133 |
+
— streaming and the HF Hub login are skipped entirely in that case.
|
| 134 |
|
| 135 |
**Advanced:** to acquire and process raw data yourself instead, edit and run
|
| 136 |
the two download configs, then the one dataset-build config, in order:
|
|
|
|
| 140 |
python download/download_sdo.py --config download/sdo_download_config.yaml
|
| 141 |
|
| 142 |
# 2) Raw GOES XRS data via SunPy Fido
|
| 143 |
+
python download/download_sxr.py --config download/sxr_download_config.yaml
|
| 144 |
|
| 145 |
# 3) Clean + convert AIA, combine + align SXR -> paired .npy (see data/build_dataset_config.yaml)
|
| 146 |
+
python data/build_dataset.py --config data/build_dataset_config.yaml
|
| 147 |
```
|
| 148 |
|
| 149 |
`data/build_dataset.py` runs the full raw-to-processed pipeline in one command
|
| 150 |
(clean AIA → convert AIA → combine GOES → align AIA/SXR); each step can be
|
| 151 |
+
skipped via the `steps:` block in its config if you've already run it.
|
| 152 |
+
Inference/evaluation just need the flat output of that — training needs two
|
| 153 |
+
more things, both off by default and only relevant if you're training:
|
| 154 |
+
- `steps.split: true` — splits `aia.processed_dir`/`output.sxr_dir` into
|
| 155 |
+
`train/`/`val/`/`test/` subfolders (date ranges or a month-based default;
|
| 156 |
+
see the `split:` block in the config).
|
| 157 |
+
- `sxr_normalization.compute: true` — computes SXR normalization stats from
|
| 158 |
+
the train split (requires `steps.split` to have run first).
|
| 159 |
|
| 160 |
### 1) Data format
|
| 161 |
|
|
|
|
| 196 |
Then run:
|
| 197 |
|
| 198 |
```bash
|
| 199 |
+
python forecasting/inference.py --config forecasting/inference_config.yaml
|
| 200 |
```
|
| 201 |
|
| 202 |
This writes `output_path` (a CSV of timestamp/prediction/groundtruth). Per-patch
|
|
|
|
| 211 |
CSV and data directories, then run:
|
| 212 |
|
| 213 |
```bash
|
| 214 |
+
python forecasting/evaluation.py --config forecasting/evaluation_config.yaml
|
| 215 |
```
|
| 216 |
|
| 217 |
This computes metrics (MSE, MAE, R²) and generates plots under `evaluation.output_dir`.
|
| 218 |
|
| 219 |
+
### 4) Train your own model (optional)
|
| 220 |
+
|
| 221 |
+
Unlike inference, training expects `aia_dir`/`sxr_dir` each to have `train/`,
|
| 222 |
+
`val/`, and `test/` subfolders of paired `.npy` files — exactly what
|
| 223 |
+
`data/build_dataset.py` and the Hugging Face download path produce.
|
| 224 |
+
|
| 225 |
+
Edit `training/train_config.yaml`:
|
| 226 |
+
|
| 227 |
+
```yaml
|
| 228 |
+
base_data_dir: "/path/to/processed_data" # holds AIA_processed/ and SXR_processed/
|
| 229 |
+
base_checkpoint_dir: "/path/to/checkpoints"
|
| 230 |
+
|
| 231 |
+
gpu_ids: -1 # -1 = CPU, 0 = GPU 0, [0,1] = specific GPUs, "all" = every GPU
|
| 232 |
+
batch_size: 6
|
| 233 |
+
epochs: 150
|
| 234 |
+
|
| 235 |
+
vit_architecture:
|
| 236 |
+
mask_mode: inverted # inverted (released model) | local | none (full/global attention)
|
| 237 |
+
local_window: 9
|
| 238 |
+
|
| 239 |
+
wandb:
|
| 240 |
+
entity: "" # your W&B username or team name
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
Then run:
|
| 244 |
+
|
| 245 |
+
```bash
|
| 246 |
+
python training/train.py --config training/train_config.yaml
|
| 247 |
+
```
|
| 248 |
+
|
| 249 |
+
Training logs to Weights & Biases (predicted-vs-true SXR plots and attention
|
| 250 |
+
map visualizations each validation epoch — see `training/callbacks.py`) and
|
| 251 |
+
saves the top 10 checkpoints by validation loss to `data.checkpoints_dir`,
|
| 252 |
+
ready to point `forecasting/inference_config.yaml` at.
|
| 253 |
+
|
| 254 |
---
|
| 255 |
|
| 256 |
## Citation
|
data/align_aia_sxr.py
CHANGED
|
@@ -171,7 +171,10 @@ def align_aia_sxr(goes_data_dir, aia_processed_dir, output_sxr_dir, aia_missing_
|
|
| 171 |
|
| 172 |
# Get target timestamps from AIA files
|
| 173 |
print(f"\nFinding target timestamps from AIA files in: {aia_processed_dir}")
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
| 175 |
aia_files_split = [file.split('/')[-1].split('.')[0] for file in aia_files]
|
| 176 |
common_timestamps = [
|
| 177 |
datetime.fromisoformat(date_str).strftime('%Y-%m-%dT%H:%M:%S')
|
|
|
|
| 171 |
|
| 172 |
# Get target timestamps from AIA files
|
| 173 |
print(f"\nFinding target timestamps from AIA files in: {aia_processed_dir}")
|
| 174 |
+
# Skip macOS AppleDouble sidecar files (e.g. "._2023-08-01T00:00:00.npy")
|
| 175 |
+
# that external/non-native filesystems litter alongside real files.
|
| 176 |
+
aia_files = sorted(f for f in glob.glob(f"{aia_processed_dir}/*.npy", recursive=True)
|
| 177 |
+
if not os.path.basename(f).startswith('._'))
|
| 178 |
aia_files_split = [file.split('/')[-1].split('.')[0] for file in aia_files]
|
| 179 |
common_timestamps = [
|
| 180 |
datetime.fromisoformat(date_str).strftime('%Y-%m-%dT%H:%M:%S')
|
data/build_dataset.py
CHANGED
|
@@ -1,15 +1,16 @@
|
|
| 1 |
"""
|
| 2 |
End-to-end raw -> processed dataset builder for FOXES.
|
| 3 |
|
| 4 |
-
Runs, in order: clean_aia -> convert_aia -> combine_sxr -> align ->
|
| 5 |
-
sxr_normalization. Each step can be skipped via the `steps` section
|
| 6 |
-
config if you already ran it. Everything is driven by one YAML config
|
| 7 |
-
build_dataset_config.yaml for the fields.
|
| 8 |
|
| 9 |
Usage:
|
| 10 |
-
python data/build_dataset.py -config data/build_dataset_config.yaml
|
| 11 |
"""
|
| 12 |
import argparse
|
|
|
|
| 13 |
|
| 14 |
import numpy as np
|
| 15 |
import yaml
|
|
@@ -18,6 +19,7 @@ from clean_aia import clean_aia_data
|
|
| 18 |
from convert_aia import process_aia_to_npy
|
| 19 |
from combine_sxr import SXRDataProcessor
|
| 20 |
from align_aia_sxr import align_aia_sxr
|
|
|
|
| 21 |
from sxr_normalization import compute_sxr_norm
|
| 22 |
|
| 23 |
|
|
@@ -28,7 +30,7 @@ def load_config(config_path):
|
|
| 28 |
|
| 29 |
def main():
|
| 30 |
parser = argparse.ArgumentParser(description='Build a FOXES-ready dataset from raw AIA/GOES data.')
|
| 31 |
-
parser.add_argument('-config', type=str, default='data/build_dataset_config.yaml',
|
| 32 |
help='Path to build_dataset config YAML file')
|
| 33 |
args = parser.parse_args()
|
| 34 |
|
|
@@ -40,30 +42,31 @@ def main():
|
|
| 40 |
output = config['output']
|
| 41 |
processing = config.get('processing', {})
|
| 42 |
steps = config.get('steps', {})
|
|
|
|
| 43 |
norm = config.get('sxr_normalization', {})
|
| 44 |
|
| 45 |
print("=== FOXES Dataset Build ===")
|
| 46 |
|
| 47 |
if steps.get('clean_aia', True):
|
| 48 |
-
print("\n--- Step 1/
|
| 49 |
clean_aia_data(aia['raw_dir'], aia['bad_files_dir'], wavelengths)
|
| 50 |
else:
|
| 51 |
-
print("\n--- Step 1/
|
| 52 |
|
| 53 |
if steps.get('convert_aia', True):
|
| 54 |
-
print("\n--- Step 2/
|
| 55 |
process_aia_to_npy(aia['raw_dir'], aia['processed_dir'], wavelengths)
|
| 56 |
else:
|
| 57 |
-
print("\n--- Step 2/
|
| 58 |
|
| 59 |
if steps.get('combine_sxr', True):
|
| 60 |
-
print("\n--- Step 3/
|
| 61 |
SXRDataProcessor(data_dir=sxr['raw_dir'], output_dir=sxr['combined_dir']).combine_goes_data()
|
| 62 |
else:
|
| 63 |
-
print("\n--- Step 3/
|
| 64 |
|
| 65 |
if steps.get('align', True):
|
| 66 |
-
print("\n--- Step 4/
|
| 67 |
align_aia_sxr(
|
| 68 |
goes_data_dir=sxr['combined_dir'],
|
| 69 |
aia_processed_dir=aia['processed_dir'],
|
|
@@ -74,11 +77,22 @@ def main():
|
|
| 74 |
min_batch_size=processing.get('min_batch_size', 1),
|
| 75 |
)
|
| 76 |
else:
|
| 77 |
-
print("\n--- Step 4/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
if norm.get('compute', False):
|
| 80 |
-
print("\n--- Optional: Compute SXR normalization stats ---")
|
| 81 |
-
sxr_norm = compute_sxr_norm(output['sxr_dir'])
|
| 82 |
np.save(norm['output_path'], sxr_norm)
|
| 83 |
print(f"Saved SXR normalization to {norm['output_path']}")
|
| 84 |
|
|
|
|
| 1 |
"""
|
| 2 |
End-to-end raw -> processed dataset builder for FOXES.
|
| 3 |
|
| 4 |
+
Runs, in order: clean_aia -> convert_aia -> combine_sxr -> align -> split ->
|
| 5 |
+
(optional) sxr_normalization. Each step can be skipped via the `steps` section
|
| 6 |
+
of the config if you already ran it. Everything is driven by one YAML config
|
| 7 |
+
— see build_dataset_config.yaml for the fields.
|
| 8 |
|
| 9 |
Usage:
|
| 10 |
+
python data/build_dataset.py --config data/build_dataset_config.yaml
|
| 11 |
"""
|
| 12 |
import argparse
|
| 13 |
+
import os
|
| 14 |
|
| 15 |
import numpy as np
|
| 16 |
import yaml
|
|
|
|
| 19 |
from convert_aia import process_aia_to_npy
|
| 20 |
from combine_sxr import SXRDataProcessor
|
| 21 |
from align_aia_sxr import align_aia_sxr
|
| 22 |
+
from split_train_val_test import split_train_val_test
|
| 23 |
from sxr_normalization import compute_sxr_norm
|
| 24 |
|
| 25 |
|
|
|
|
| 30 |
|
| 31 |
def main():
|
| 32 |
parser = argparse.ArgumentParser(description='Build a FOXES-ready dataset from raw AIA/GOES data.')
|
| 33 |
+
parser.add_argument('--config', type=str, default='data/build_dataset_config.yaml',
|
| 34 |
help='Path to build_dataset config YAML file')
|
| 35 |
args = parser.parse_args()
|
| 36 |
|
|
|
|
| 42 |
output = config['output']
|
| 43 |
processing = config.get('processing', {})
|
| 44 |
steps = config.get('steps', {})
|
| 45 |
+
split = config.get('split', {})
|
| 46 |
norm = config.get('sxr_normalization', {})
|
| 47 |
|
| 48 |
print("=== FOXES Dataset Build ===")
|
| 49 |
|
| 50 |
if steps.get('clean_aia', True):
|
| 51 |
+
print("\n--- Step 1/5: Clean AIA FITS files ---")
|
| 52 |
clean_aia_data(aia['raw_dir'], aia['bad_files_dir'], wavelengths)
|
| 53 |
else:
|
| 54 |
+
print("\n--- Step 1/5: Clean AIA FITS files (skipped) ---")
|
| 55 |
|
| 56 |
if steps.get('convert_aia', True):
|
| 57 |
+
print("\n--- Step 2/5: Convert AIA FITS -> .npy ---")
|
| 58 |
process_aia_to_npy(aia['raw_dir'], aia['processed_dir'], wavelengths)
|
| 59 |
else:
|
| 60 |
+
print("\n--- Step 2/5: Convert AIA FITS -> .npy (skipped) ---")
|
| 61 |
|
| 62 |
if steps.get('combine_sxr', True):
|
| 63 |
+
print("\n--- Step 3/5: Combine raw GOES satellite files ---")
|
| 64 |
SXRDataProcessor(data_dir=sxr['raw_dir'], output_dir=sxr['combined_dir']).combine_goes_data()
|
| 65 |
else:
|
| 66 |
+
print("\n--- Step 3/5: Combine raw GOES satellite files (skipped) ---")
|
| 67 |
|
| 68 |
if steps.get('align', True):
|
| 69 |
+
print("\n--- Step 4/5: Align AIA timestamps with GOES SXR data ---")
|
| 70 |
align_aia_sxr(
|
| 71 |
goes_data_dir=sxr['combined_dir'],
|
| 72 |
aia_processed_dir=aia['processed_dir'],
|
|
|
|
| 77 |
min_batch_size=processing.get('min_batch_size', 1),
|
| 78 |
)
|
| 79 |
else:
|
| 80 |
+
print("\n--- Step 4/5: Align AIA timestamps with GOES SXR data (skipped) ---")
|
| 81 |
+
|
| 82 |
+
# Off by default: only needed if you're training (inference/evaluation
|
| 83 |
+
# work fine against the flat processed_dir/sxr_dir from the align step).
|
| 84 |
+
if steps.get('split', False):
|
| 85 |
+
print("\n--- Step 5/5: Split AIA/SXR into train/val/test ---")
|
| 86 |
+
split_kwargs = dict(train_range=split.get('train_range'), val_range=split.get('val_range'),
|
| 87 |
+
test_range=split.get('test_range'), copy_files=split.get('copy_files', False))
|
| 88 |
+
split_train_val_test(aia['processed_dir'], aia['processed_dir'], **split_kwargs)
|
| 89 |
+
split_train_val_test(output['sxr_dir'], output['sxr_dir'], **split_kwargs)
|
| 90 |
+
else:
|
| 91 |
+
print("\n--- Step 5/5: Split AIA/SXR into train/val/test (skipped) ---")
|
| 92 |
|
| 93 |
if norm.get('compute', False):
|
| 94 |
+
print("\n--- Optional: Compute SXR normalization stats (from the train split) ---")
|
| 95 |
+
sxr_norm = compute_sxr_norm(os.path.join(output['sxr_dir'], 'train'))
|
| 96 |
np.save(norm['output_path'], sxr_norm)
|
| 97 |
print(f"Saved SXR normalization to {norm['output_path']}")
|
| 98 |
|
data/build_dataset_config.yaml
CHANGED
|
@@ -4,23 +4,23 @@
|
|
| 4 |
# Turns raw AIA FITS + raw GOES XRS data into the paired .npy layout that
|
| 5 |
# forecasting/dataset.py expects.
|
| 6 |
#
|
| 7 |
-
# Usage: python data/build_dataset.py -config data/build_dataset_config.yaml
|
| 8 |
# =============================================================================
|
| 9 |
|
| 10 |
wavelengths: [94, 131, 171, 193, 211, 304, 335]
|
| 11 |
|
| 12 |
aia:
|
| 13 |
-
raw_dir: "/
|
| 14 |
-
bad_files_dir: "/
|
| 15 |
-
processed_dir: "/
|
| 16 |
|
| 17 |
sxr:
|
| 18 |
-
raw_dir: "/
|
| 19 |
-
combined_dir: "/
|
| 20 |
|
| 21 |
output:
|
| 22 |
-
sxr_dir: "/
|
| 23 |
-
aia_missing_dir: "/
|
| 24 |
|
| 25 |
processing:
|
| 26 |
max_processes: null # cap worker processes; null = use all CPUs
|
|
@@ -30,13 +30,26 @@ processing:
|
|
| 30 |
# Which build steps to run, in order. Set any to false to skip it (e.g. if
|
| 31 |
# you already ran that step and are re-running after fixing a later one).
|
| 32 |
steps:
|
| 33 |
-
clean_aia: true
|
| 34 |
-
convert_aia: true
|
| 35 |
-
combine_sxr: true
|
| 36 |
-
align: true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
# Optional — only needed if you're training, not for running inference
|
| 39 |
-
# against a released checkpoint.
|
|
|
|
| 40 |
sxr_normalization:
|
| 41 |
-
compute:
|
| 42 |
-
output_path: "/
|
|
|
|
| 4 |
# Turns raw AIA FITS + raw GOES XRS data into the paired .npy layout that
|
| 5 |
# forecasting/dataset.py expects.
|
| 6 |
#
|
| 7 |
+
# Usage: python data/build_dataset.py --config data/build_dataset_config.yaml
|
| 8 |
# =============================================================================
|
| 9 |
|
| 10 |
wavelengths: [94, 131, 171, 193, 211, 304, 335]
|
| 11 |
|
| 12 |
aia:
|
| 13 |
+
raw_dir: "/Volumes/T9/testing_foxes/AIA_raw" # raw FITS from download/download_sdo.py
|
| 14 |
+
bad_files_dir: "/Volumes/T9/testing_foxes/SDO/AIA_bad" # bad-timestamp FITS get moved here
|
| 15 |
+
processed_dir: "/Volumes/T9/testing_foxes/AIA_processed" # paired 512x512 .npy stacks
|
| 16 |
|
| 17 |
sxr:
|
| 18 |
+
raw_dir: "/Volumes/T9/testing_foxes/GOES_raw" # raw netCDF from download/download_sxr.py
|
| 19 |
+
combined_dir: "/Volumes/T9/testing_foxes/GOES_combined" # one interpolated CSV per satellite
|
| 20 |
|
| 21 |
output:
|
| 22 |
+
sxr_dir: "/Volumes/T9/testing_foxes/SXR_processed" # per-timestamp xrsb_flux .npy, aligned to AIA
|
| 23 |
+
aia_missing_dir: "/Volumes/T9/testing_foxes/AIA_missing" # AIA files with no matching SXR value
|
| 24 |
|
| 25 |
processing:
|
| 26 |
max_processes: null # cap worker processes; null = use all CPUs
|
|
|
|
| 30 |
# Which build steps to run, in order. Set any to false to skip it (e.g. if
|
| 31 |
# you already ran that step and are re-running after fixing a later one).
|
| 32 |
steps:
|
| 33 |
+
clean_aia: true # drop AIA FITS files with a bad DATE-OBS timestamp
|
| 34 |
+
convert_aia: true # raw AIA FITS -> paired .npy stacks
|
| 35 |
+
combine_sxr: true # combine raw multi-satellite GOES files into per-satellite CSVs
|
| 36 |
+
align: true # match AIA timestamps to GOES CSVs -> per-timestamp SXR .npy
|
| 37 |
+
split: true # split aia.processed_dir / output.sxr_dir into train/val/test
|
| 38 |
+
|
| 39 |
+
# Only needed if you're training (training/train_config.yaml expects
|
| 40 |
+
# aia_dir/sxr_dir to each have train/val/test subfolders) — not needed for
|
| 41 |
+
# inference or evaluation against a released checkpoint. Splits both
|
| 42 |
+
# aia.processed_dir and output.sxr_dir in place, using the same date ranges
|
| 43 |
+
# so a given timestamp lands in the same split for both.
|
| 44 |
+
split:
|
| 45 |
+
train_range: ["2023-08-01","2023-08-15"] # e.g. ["2012-01-01", "2022-12-31"]; null with the others -> month-based default
|
| 46 |
+
val_range: ["2023-08-16","2023-08-22"] # e.g. ["2023-01-01", "2023-06-30"]
|
| 47 |
+
test_range: ["2023-08-23","2023-08-31"] # e.g. ["2023-08-01", "2023-08-31"]
|
| 48 |
+
copy_files: false # copy instead of move
|
| 49 |
|
| 50 |
# Optional — only needed if you're training, not for running inference
|
| 51 |
+
# against a released checkpoint. Requires steps.split to have run (computes
|
| 52 |
+
# stats from the train split only).
|
| 53 |
sxr_normalization:
|
| 54 |
+
compute: true
|
| 55 |
+
output_path: "/Volumes/T9/testing_foxes/normalized_sxr.npy"
|
data/clean_aia.py
CHANGED
|
@@ -1,6 +1,11 @@
|
|
| 1 |
"""
|
| 2 |
-
Remove AIA FITS files
|
| 3 |
-
header (more than 60s apart
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
Called by build_dataset.py — not meant to be run standalone.
|
| 6 |
"""
|
|
@@ -9,36 +14,51 @@ import os
|
|
| 9 |
import shutil
|
| 10 |
from multiprocessing import Pool
|
| 11 |
|
|
|
|
| 12 |
import pandas as pd
|
| 13 |
from tqdm import tqdm
|
| 14 |
|
| 15 |
# itipy needs these aliases done manually on newer Python versions.
|
| 16 |
-
collections.Iterable = collections.abc.Iterable
|
| 17 |
-
collections.Mapping = collections.abc.Mapping
|
| 18 |
-
collections.MutableSet = collections.abc.MutableSet
|
| 19 |
-
collections.MutableMapping = collections.abc.MutableMapping
|
| 20 |
from itipy.data.dataset import get_intersecting_files
|
| 21 |
from astropy.io import fits
|
| 22 |
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
def process_fits_file(file_path):
|
| 25 |
try:
|
| 26 |
with fits.open(file_path) as hdu:
|
| 27 |
-
header = hdu[1].header
|
| 28 |
date_obs = pd.to_datetime(header['DATE-OBS'])
|
| 29 |
# Ensure timezone-naive datetime
|
| 30 |
if date_obs.tz is not None:
|
| 31 |
date_obs = date_obs.tz_localize(None)
|
| 32 |
wavelength = header['WAVELNTH']
|
| 33 |
filename = pd.to_datetime(os.path.basename(file_path).split('.')[0])
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
except Exception as e:
|
| 36 |
print(f"Error processing {file_path}: {e}")
|
| 37 |
return None
|
| 38 |
|
| 39 |
|
| 40 |
def clean_aia_data(input_folder, bad_files_dir, wavelengths):
|
| 41 |
-
"""Move AIA FITS files with bad DATE-OBS timestamps out of input_folder."""
|
| 42 |
aia_files = get_intersecting_files(input_folder, wavelengths)
|
| 43 |
file_list = aia_files[0] # List of FITS file paths
|
| 44 |
|
|
@@ -48,6 +68,10 @@ def clean_aia_data(input_folder, bad_files_dir, wavelengths):
|
|
| 48 |
# Filter out None results (in case of failed files)
|
| 49 |
results = [r for r in results if r is not None]
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
# Convert to DataFrame
|
| 52 |
aia_header = pd.DataFrame(results)
|
| 53 |
aia_header['DATE-OBS'] = pd.to_datetime(aia_header['DATE-OBS'])
|
|
@@ -57,9 +81,13 @@ def clean_aia_data(input_folder, bad_files_dir, wavelengths):
|
|
| 57 |
pd.to_datetime(aia_header['FILENAME']) - pd.to_datetime(aia_header['DATE-OBS'])
|
| 58 |
).dt.total_seconds()
|
| 59 |
|
| 60 |
-
# Remove rows where DATE_DIFF is greater than ±60 seconds
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
for wavelength in wavelengths:
|
| 65 |
print(f"\nProcessing wavelength: {wavelength}")
|
|
|
|
| 1 |
"""
|
| 2 |
+
Remove AIA FITS files that are unusable: filename timestamp doesn't match the
|
| 3 |
+
DATE-OBS header (more than 60s apart, indicating a mis-tagged/corrupt download),
|
| 4 |
+
the image itself is blank (all-zero/constant/NaN), or QUALITY is nonzero (AIA's
|
| 5 |
+
own bitmask flagging eclipse/calibration/off-point/etc. frames). download_sdo.py
|
| 6 |
+
only warns and falls back to the closest available frame when no quality-0
|
| 7 |
+
frame exists nearby, so a bad-quality frame can still make it to disk — this
|
| 8 |
+
is the backstop that catches it.
|
| 9 |
|
| 10 |
Called by build_dataset.py — not meant to be run standalone.
|
| 11 |
"""
|
|
|
|
| 14 |
import shutil
|
| 15 |
from multiprocessing import Pool
|
| 16 |
|
| 17 |
+
import numpy as np
|
| 18 |
import pandas as pd
|
| 19 |
from tqdm import tqdm
|
| 20 |
|
| 21 |
# itipy needs these aliases done manually on newer Python versions.
|
| 22 |
+
collections.Iterable = collections.abc.Iterable # type: ignore[attr-defined]
|
| 23 |
+
collections.Mapping = collections.abc.Mapping # type: ignore[attr-defined]
|
| 24 |
+
collections.MutableSet = collections.abc.MutableSet # type: ignore[attr-defined]
|
| 25 |
+
collections.MutableMapping = collections.abc.MutableMapping # type: ignore[attr-defined]
|
| 26 |
from itipy.data.dataset import get_intersecting_files
|
| 27 |
from astropy.io import fits
|
| 28 |
|
| 29 |
|
| 30 |
+
def _is_blank(data):
|
| 31 |
+
"""True if the image has no usable signal (all-zero, constant, or all-NaN)."""
|
| 32 |
+
if data is None:
|
| 33 |
+
return True
|
| 34 |
+
finite = data[np.isfinite(data)]
|
| 35 |
+
if finite.size == 0:
|
| 36 |
+
return True
|
| 37 |
+
return bool(np.nanmax(finite) == np.nanmin(finite))
|
| 38 |
+
|
| 39 |
+
|
| 40 |
def process_fits_file(file_path):
|
| 41 |
try:
|
| 42 |
with fits.open(file_path) as hdu:
|
| 43 |
+
header = hdu[1].header # type: ignore[union-attr]
|
| 44 |
date_obs = pd.to_datetime(header['DATE-OBS'])
|
| 45 |
# Ensure timezone-naive datetime
|
| 46 |
if date_obs.tz is not None:
|
| 47 |
date_obs = date_obs.tz_localize(None)
|
| 48 |
wavelength = header['WAVELNTH']
|
| 49 |
filename = pd.to_datetime(os.path.basename(file_path).split('.')[0])
|
| 50 |
+
blank = _is_blank(hdu[1].data) # type: ignore[union-attr]
|
| 51 |
+
quality = pd.to_numeric(pd.Series([header.get('QUALITY')]), errors='coerce').iloc[0]
|
| 52 |
+
bad_quality = bool(pd.notna(quality) and quality != 0)
|
| 53 |
+
return {'DATE-OBS': date_obs, 'WAVELNTH': wavelength, 'FILENAME': filename,
|
| 54 |
+
'BLANK': blank, 'BAD_QUALITY': bad_quality}
|
| 55 |
except Exception as e:
|
| 56 |
print(f"Error processing {file_path}: {e}")
|
| 57 |
return None
|
| 58 |
|
| 59 |
|
| 60 |
def clean_aia_data(input_folder, bad_files_dir, wavelengths):
|
| 61 |
+
"""Move AIA FITS files with bad DATE-OBS timestamps or blank images out of input_folder."""
|
| 62 |
aia_files = get_intersecting_files(input_folder, wavelengths)
|
| 63 |
file_list = aia_files[0] # List of FITS file paths
|
| 64 |
|
|
|
|
| 68 |
# Filter out None results (in case of failed files)
|
| 69 |
results = [r for r in results if r is not None]
|
| 70 |
|
| 71 |
+
if not results:
|
| 72 |
+
print("No readable AIA files found — nothing to clean.")
|
| 73 |
+
return
|
| 74 |
+
|
| 75 |
# Convert to DataFrame
|
| 76 |
aia_header = pd.DataFrame(results)
|
| 77 |
aia_header['DATE-OBS'] = pd.to_datetime(aia_header['DATE-OBS'])
|
|
|
|
| 81 |
pd.to_datetime(aia_header['FILENAME']) - pd.to_datetime(aia_header['DATE-OBS'])
|
| 82 |
).dt.total_seconds()
|
| 83 |
|
| 84 |
+
# Remove rows where DATE_DIFF is greater than ±60 seconds, the image is
|
| 85 |
+
# blank, or QUALITY flags a known instrumental issue
|
| 86 |
+
bad_timing = (aia_header['DATE_DIFF'] <= -60) | (aia_header['DATE_DIFF'] >= 60)
|
| 87 |
+
files_to_remove = aia_header[bad_timing | aia_header['BLANK'] | aia_header['BAD_QUALITY']]
|
| 88 |
+
print(f"{len(files_to_remove)} bad files found "
|
| 89 |
+
f"({bad_timing.sum()} bad timing, {aia_header['BLANK'].sum()} blank, "
|
| 90 |
+
f"{aia_header['BAD_QUALITY'].sum()} bad quality)")
|
| 91 |
|
| 92 |
for wavelength in wavelengths:
|
| 93 |
print(f"\nProcessing wavelength: {wavelength}")
|
data/combine_sxr.py
CHANGED
|
@@ -29,7 +29,7 @@ class SXRDataProcessor:
|
|
| 29 |
def __init__(self, data_dir: str, output_dir: str):
|
| 30 |
self.data_dir = Path(data_dir)
|
| 31 |
self.output_dir = Path(output_dir)
|
| 32 |
-
self.output_dir.mkdir(exist_ok=True)
|
| 33 |
self.used_g13_files = []
|
| 34 |
self.used_g14_files = []
|
| 35 |
self.used_g15_files = []
|
|
@@ -42,14 +42,19 @@ class SXRDataProcessor:
|
|
| 42 |
Combine GOES-16 and GOES-18 files and track source files used.
|
| 43 |
Parameters
|
| 44 |
"""
|
| 45 |
-
print("
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
total_files = len(g13_files) + len(g14_files) + len(g15_files) + len(g16_files) + len(g17_files) + len(g18_files)
|
| 55 |
logging.info(
|
|
@@ -62,11 +67,10 @@ class SXRDataProcessor:
|
|
| 62 |
|
| 63 |
def process_files(files, satellite_name, output_file, used_file_list):
|
| 64 |
datasets = []
|
| 65 |
-
combined_meta = {}
|
| 66 |
successful_files = 0
|
| 67 |
failed_files = 0
|
| 68 |
|
| 69 |
-
print(f"
|
| 70 |
|
| 71 |
# Progress bar for file loading
|
| 72 |
with tqdm(files, desc=f"Loading {satellite_name}", unit="file",
|
|
@@ -85,7 +89,7 @@ class SXRDataProcessor:
|
|
| 85 |
continue
|
| 86 |
finally:
|
| 87 |
if 'ds' in locals():
|
| 88 |
-
ds.close()
|
| 89 |
|
| 90 |
if not datasets:
|
| 91 |
print(f"No valid datasets for {satellite_name}")
|
|
|
|
| 29 |
def __init__(self, data_dir: str, output_dir: str):
|
| 30 |
self.data_dir = Path(data_dir)
|
| 31 |
self.output_dir = Path(output_dir)
|
| 32 |
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
| 33 |
self.used_g13_files = []
|
| 34 |
self.used_g14_files = []
|
| 35 |
self.used_g15_files = []
|
|
|
|
| 42 |
Combine GOES-16 and GOES-18 files and track source files used.
|
| 43 |
Parameters
|
| 44 |
"""
|
| 45 |
+
print("Scanning for GOES data files...")
|
| 46 |
+
|
| 47 |
+
def _glob(pattern):
|
| 48 |
+
# Skip macOS AppleDouble sidecar files (e.g. "._sci_xrsf...nc")
|
| 49 |
+
# that external/non-native filesystems litter alongside real files.
|
| 50 |
+
return sorted(p for p in self.data_dir.glob(pattern) if not p.name.startswith('._'))
|
| 51 |
+
|
| 52 |
+
g13_files = _glob("*g13*.nc")
|
| 53 |
+
g14_files = _glob("*g14*.nc")
|
| 54 |
+
g15_files = _glob("*g15*.nc")
|
| 55 |
+
g16_files = _glob("*g16*.nc")
|
| 56 |
+
g17_files = _glob("*g17*.nc")
|
| 57 |
+
g18_files = _glob("*g18*.nc")
|
| 58 |
|
| 59 |
total_files = len(g13_files) + len(g14_files) + len(g15_files) + len(g16_files) + len(g17_files) + len(g18_files)
|
| 60 |
logging.info(
|
|
|
|
| 67 |
|
| 68 |
def process_files(files, satellite_name, output_file, used_file_list):
|
| 69 |
datasets = []
|
|
|
|
| 70 |
successful_files = 0
|
| 71 |
failed_files = 0
|
| 72 |
|
| 73 |
+
print(f"Processing {satellite_name} ({len(files)} files)...")
|
| 74 |
|
| 75 |
# Progress bar for file loading
|
| 76 |
with tqdm(files, desc=f"Loading {satellite_name}", unit="file",
|
|
|
|
| 89 |
continue
|
| 90 |
finally:
|
| 91 |
if 'ds' in locals():
|
| 92 |
+
ds.close() # type: ignore[attr-defined]
|
| 93 |
|
| 94 |
if not datasets:
|
| 95 |
print(f"No valid datasets for {satellite_name}")
|
data/convert_aia.py
CHANGED
|
@@ -8,10 +8,10 @@ import collections.abc
|
|
| 8 |
import os
|
| 9 |
from multiprocessing import Pool
|
| 10 |
|
| 11 |
-
collections.Iterable = collections.abc.Iterable
|
| 12 |
-
collections.Mapping = collections.abc.Mapping
|
| 13 |
-
collections.MutableSet = collections.abc.MutableSet
|
| 14 |
-
collections.MutableMapping = collections.abc.MutableMapping
|
| 15 |
import numpy as np
|
| 16 |
from itipy.data.dataset import StackDataset, get_intersecting_files, AIADataset
|
| 17 |
from itipy.data.editor import BrightestPixelPatchEditor
|
|
@@ -40,7 +40,7 @@ class AIAStackDataset(StackDataset):
|
|
| 40 |
ds_mapping = {94: AIADataset, 131: AIADataset, 171: AIADataset, 193: AIADataset, 211: AIADataset,
|
| 41 |
304: AIADataset, 335: AIADataset, 1600: AIADataset, 1700: AIADataset, 4500: AIADataset, 6173: AIADataset}
|
| 42 |
data_sets = [ds_mapping[wl_id](files, wavelength=wl_id, resolution=resolution, ext=ext, allow_errors=allow_errors)
|
| 43 |
-
for wl_id, files in zip(wavelengths, paths)]
|
| 44 |
super().__init__(data_sets, **kwargs)
|
| 45 |
if patch_shape is not None:
|
| 46 |
self.addEditor(BrightestPixelPatchEditor(patch_shape))
|
|
@@ -58,11 +58,11 @@ def _init_worker(dataset, out_folder):
|
|
| 58 |
|
| 59 |
def save_sample(i):
|
| 60 |
try:
|
| 61 |
-
data = _aia_dataset[i]
|
| 62 |
-
file_path = os.path.join(_output_folder, _aia_dataset.getId(i)) + '.npy'
|
| 63 |
np.save(file_path, data)
|
| 64 |
except Exception as e:
|
| 65 |
-
print(f"Warning: Could not process sample {i} (ID: {_aia_dataset.getId(i)}): {e}")
|
| 66 |
|
| 67 |
|
| 68 |
def check_existing_files(base_input_folder, wavelengths, output_folder):
|
|
|
|
| 8 |
import os
|
| 9 |
from multiprocessing import Pool
|
| 10 |
|
| 11 |
+
collections.Iterable = collections.abc.Iterable # type: ignore[attr-defined]
|
| 12 |
+
collections.Mapping = collections.abc.Mapping # type: ignore[attr-defined]
|
| 13 |
+
collections.MutableSet = collections.abc.MutableSet # type: ignore[attr-defined]
|
| 14 |
+
collections.MutableMapping = collections.abc.MutableMapping # type: ignore[attr-defined]
|
| 15 |
import numpy as np
|
| 16 |
from itipy.data.dataset import StackDataset, get_intersecting_files, AIADataset
|
| 17 |
from itipy.data.editor import BrightestPixelPatchEditor
|
|
|
|
| 40 |
ds_mapping = {94: AIADataset, 131: AIADataset, 171: AIADataset, 193: AIADataset, 211: AIADataset,
|
| 41 |
304: AIADataset, 335: AIADataset, 1600: AIADataset, 1700: AIADataset, 4500: AIADataset, 6173: AIADataset}
|
| 42 |
data_sets = [ds_mapping[wl_id](files, wavelength=wl_id, resolution=resolution, ext=ext, allow_errors=allow_errors)
|
| 43 |
+
for wl_id, files in zip(wavelengths, paths)] # type: ignore[attr-defined]
|
| 44 |
super().__init__(data_sets, **kwargs)
|
| 45 |
if patch_shape is not None:
|
| 46 |
self.addEditor(BrightestPixelPatchEditor(patch_shape))
|
|
|
|
| 58 |
|
| 59 |
def save_sample(i):
|
| 60 |
try:
|
| 61 |
+
data = _aia_dataset[i] # type: ignore[attr-defined]
|
| 62 |
+
file_path = os.path.join(_output_folder, _aia_dataset.getId(i)) + '.npy' # type: ignore[attr-defined]
|
| 63 |
np.save(file_path, data)
|
| 64 |
except Exception as e:
|
| 65 |
+
print(f"Warning: Could not process sample {i} (ID: {_aia_dataset.getId(i)}): {e}") # type: ignore[attr-defined]
|
| 66 |
|
| 67 |
|
| 68 |
def check_existing_files(base_input_folder, wavelengths, output_folder):
|
data/split_train_val_test.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Split a flat folder of timestamp-named .npy files (AIA or SXR) into
|
| 3 |
+
train/val/test subfolders, so forecasting/training's AIAGOESDataModule can
|
| 4 |
+
find them.
|
| 5 |
+
|
| 6 |
+
Called by build_dataset.py — not meant to be run standalone.
|
| 7 |
+
"""
|
| 8 |
+
import os
|
| 9 |
+
import shutil
|
| 10 |
+
|
| 11 |
+
import pandas as pd
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _normalize_timestamp(ts: str) -> str:
|
| 15 |
+
"""Normalize timestamp strings with underscores instead of colons (cross-platform filenames)."""
|
| 16 |
+
if 'T' in ts:
|
| 17 |
+
date_part, time_part = ts.split('T', 1)
|
| 18 |
+
return f"{date_part}T{time_part.replace('_', ':')}"
|
| 19 |
+
return ts
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _assign_split(file_time, train_range, val_range, test_range):
|
| 23 |
+
"""Return 'train'/'val'/'test' for this timestamp, or None if no range matches."""
|
| 24 |
+
ranges = {'train': train_range, 'val': val_range, 'test': test_range}
|
| 25 |
+
if any(ranges.values()):
|
| 26 |
+
for split_name, rng in ranges.items():
|
| 27 |
+
if rng is None:
|
| 28 |
+
continue
|
| 29 |
+
start = pd.to_datetime(rng[0])
|
| 30 |
+
end = pd.to_datetime(rng[1]).replace(hour=23, minute=59, second=59, microsecond=999999)
|
| 31 |
+
if start <= file_time <= end:
|
| 32 |
+
return split_name
|
| 33 |
+
return None
|
| 34 |
+
|
| 35 |
+
# Default: month-based split (August held out for test, Jan-Mar for val)
|
| 36 |
+
month = file_time.month
|
| 37 |
+
if month == 8:
|
| 38 |
+
return 'test'
|
| 39 |
+
if month in (1, 2, 3):
|
| 40 |
+
return 'val'
|
| 41 |
+
return 'train'
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def split_train_val_test(input_dir, output_dir, train_range=None, val_range=None, test_range=None,
|
| 45 |
+
copy_files=False):
|
| 46 |
+
"""
|
| 47 |
+
Split .npy files in input_dir into train/val/test subfolders under output_dir,
|
| 48 |
+
based on each file's timestamp filename.
|
| 49 |
+
|
| 50 |
+
Parameters
|
| 51 |
+
----------
|
| 52 |
+
input_dir : str
|
| 53 |
+
Flat folder of .npy files named by timestamp (e.g. from align_aia_sxr.py
|
| 54 |
+
or convert_aia.py). Can be the same path as output_dir to split in place.
|
| 55 |
+
output_dir : str
|
| 56 |
+
Destination folder; train/val/test subfolders are created under it.
|
| 57 |
+
train_range, val_range, test_range : [start, end] date strings, optional
|
| 58 |
+
Inclusive date ranges ("YYYY-MM-DD") for each split. If none are given,
|
| 59 |
+
falls back to a month-based default: August -> test, Jan-Mar -> val,
|
| 60 |
+
everything else -> train.
|
| 61 |
+
copy_files : bool
|
| 62 |
+
Copy instead of move (default: move).
|
| 63 |
+
"""
|
| 64 |
+
if not os.path.isdir(input_dir):
|
| 65 |
+
raise ValueError(f"Input folder does not exist: {input_dir}")
|
| 66 |
+
|
| 67 |
+
for split_name in ("train", "val", "test"):
|
| 68 |
+
os.makedirs(os.path.join(output_dir, split_name), exist_ok=True)
|
| 69 |
+
|
| 70 |
+
files = sorted(f for f in os.listdir(input_dir) if f.endswith(".npy"))
|
| 71 |
+
print(f"Splitting {len(files)} files from {input_dir}")
|
| 72 |
+
|
| 73 |
+
moved = skipped = 0
|
| 74 |
+
for filename in files:
|
| 75 |
+
try:
|
| 76 |
+
file_time = pd.to_datetime(_normalize_timestamp(filename[:-len(".npy")]))
|
| 77 |
+
except ValueError:
|
| 78 |
+
print(f"Skipping {filename}: invalid timestamp")
|
| 79 |
+
skipped += 1
|
| 80 |
+
continue
|
| 81 |
+
|
| 82 |
+
split_name = _assign_split(file_time, train_range, val_range, test_range)
|
| 83 |
+
if split_name is None:
|
| 84 |
+
print(f"Skipping {filename}: no matching date range ({file_time.date()})")
|
| 85 |
+
skipped += 1
|
| 86 |
+
continue
|
| 87 |
+
|
| 88 |
+
src = os.path.join(input_dir, filename)
|
| 89 |
+
dst = os.path.join(output_dir, split_name, filename)
|
| 90 |
+
if os.path.exists(dst):
|
| 91 |
+
skipped += 1
|
| 92 |
+
continue
|
| 93 |
+
|
| 94 |
+
(shutil.copy2 if copy_files else shutil.move)(src, dst)
|
| 95 |
+
moved += 1
|
| 96 |
+
|
| 97 |
+
action = "copied" if copy_files else "moved"
|
| 98 |
+
print(f"Done: {moved} files {action}, {skipped} skipped")
|
| 99 |
+
for split_name in ("train", "val", "test"):
|
| 100 |
+
n = len(os.listdir(os.path.join(output_dir, split_name)))
|
| 101 |
+
print(f" {split_name}: {n} files")
|
download/download_sdo.py
CHANGED
|
@@ -91,12 +91,20 @@ class SDODownloader:
|
|
| 91 |
obs = pd.to_datetime(date_str, errors='coerce', utc=True).dt.tz_localize(None)
|
| 92 |
header['_diff'] = (obs - date).abs()
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
rows = []
|
| 95 |
for wl in wavelengths:
|
| 96 |
-
|
|
|
|
| 97 |
if len(sub) == 0:
|
| 98 |
continue
|
| 99 |
-
good = sub[(
|
| 100 |
if len(good) > 0:
|
| 101 |
sub = good
|
| 102 |
else:
|
|
@@ -173,13 +181,27 @@ class SDODownloader:
|
|
| 173 |
|
| 174 |
header['DATE_OBS'] = header['DATE__OBS']
|
| 175 |
header = header_to_fits(MetaDict(header))
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
|
| 184 |
os.replace(tmp_path, map_path)
|
| 185 |
return map_path
|
|
|
|
| 91 |
obs = pd.to_datetime(date_str, errors='coerce', utc=True).dt.tz_localize(None)
|
| 92 |
header['_diff'] = (obs - date).abs()
|
| 93 |
|
| 94 |
+
# QUALITY comes back from JSOC as a numeric keyword, but drms occasionally
|
| 95 |
+
# yields it as object/string dtype — compare on the coerced numeric value
|
| 96 |
+
# so a dtype quirk can't silently make every frame look "not quality-0"
|
| 97 |
+
# (which would fall through to accepting whatever frame is closest,
|
| 98 |
+
# calibration/dark frames included).
|
| 99 |
+
quality_numeric = pd.to_numeric(header['QUALITY'], errors='coerce')
|
| 100 |
+
|
| 101 |
rows = []
|
| 102 |
for wl in wavelengths:
|
| 103 |
+
mask = header['WAVELNTH'] == int(wl)
|
| 104 |
+
sub = header[mask]
|
| 105 |
if len(sub) == 0:
|
| 106 |
continue
|
| 107 |
+
good = sub[(quality_numeric[mask] == 0) | quality_numeric[mask].isna()]
|
| 108 |
if len(good) > 0:
|
| 109 |
sub = good
|
| 110 |
else:
|
|
|
|
| 181 |
|
| 182 |
header['DATE_OBS'] = header['DATE__OBS']
|
| 183 |
header = header_to_fits(MetaDict(header))
|
| 184 |
+
|
| 185 |
+
# JSOC serves this segment as a tile-compressed CompImageHDU whose
|
| 186 |
+
# correct physical values depend on internal per-tile scaling
|
| 187 |
+
# (ZSCALE/ZZERO), not just the outer BSCALE/BZERO placeholder.
|
| 188 |
+
# Opening it in 'update' mode and writing header cards in place
|
| 189 |
+
# forces astropy to re-derive that scaling from the (unrelated)
|
| 190 |
+
# placeholder values, silently corrupting the pixel data — most
|
| 191 |
+
# visible on 94A, whose narrow true dynamic range gets clipped to
|
| 192 |
+
# a flat value after calibration. Instead: read the correctly
|
| 193 |
+
# decompressed data once, then write a fresh plain (uncompressed)
|
| 194 |
+
# FITS file with the merged header — never re-touch the original
|
| 195 |
+
# compressed HDU.
|
| 196 |
+
with fits.open(tmp_path) as f:
|
| 197 |
+
data = f[1].data
|
| 198 |
+
hdr = f[1].header.copy()
|
| 199 |
+
for k, v in header.items():
|
| 200 |
+
if pd.isna(v):
|
| 201 |
+
continue
|
| 202 |
+
hdr[k] = v
|
| 203 |
+
fits.HDUList([fits.PrimaryHDU(), fits.ImageHDU(data=data, header=hdr)]).writeto(
|
| 204 |
+
tmp_path, overwrite=True)
|
| 205 |
|
| 206 |
os.replace(tmp_path, map_path)
|
| 207 |
return map_path
|
download/download_sxr.py
CHANGED
|
@@ -12,7 +12,7 @@ class SXRDownloader:
|
|
| 12 |
|
| 13 |
def __init__(self, save_dir: str):
|
| 14 |
self.save_dir = Path(save_dir)
|
| 15 |
-
self.save_dir.mkdir(exist_ok=True)
|
| 16 |
self.used_g13_files = []
|
| 17 |
self.used_g14_files = []
|
| 18 |
self.used_g15_files = []
|
|
|
|
| 12 |
|
| 13 |
def __init__(self, save_dir: str):
|
| 14 |
self.save_dir = Path(save_dir)
|
| 15 |
+
self.save_dir.mkdir(parents=True, exist_ok=True)
|
| 16 |
self.used_g13_files = []
|
| 17 |
self.used_g14_files = []
|
| 18 |
self.used_g15_files = []
|
download/hf_download_config.yaml
CHANGED
|
@@ -46,6 +46,14 @@ subsample_frac: 0.1 # used only when subsample_n is null (0.0–1.0)
|
|
| 46 |
# Only used when subsample: true.
|
| 47 |
shuffle_buffer_size: 10
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
# -----------------------------------------------------------------------------
|
| 50 |
# Misc
|
| 51 |
# -----------------------------------------------------------------------------
|
|
|
|
| 46 |
# Only used when subsample: true.
|
| 47 |
shuffle_buffer_size: 10
|
| 48 |
|
| 49 |
+
# -----------------------------------------------------------------------------
|
| 50 |
+
# Already downloaded parquet files locally instead of streaming from HF Hub?
|
| 51 |
+
# Point this at the root dir containing per-split subdirs (train/, validation/
|
| 52 |
+
# or val/, test/) and streaming + HF Hub login are skipped entirely.
|
| 53 |
+
# -----------------------------------------------------------------------------
|
| 54 |
+
local_parquet_dir: null
|
| 55 |
+
delete_parquet_after: false # delete each parquet file once its rows are written
|
| 56 |
+
|
| 57 |
# -----------------------------------------------------------------------------
|
| 58 |
# Misc
|
| 59 |
# -----------------------------------------------------------------------------
|
download/hugging_face_data_download.py
CHANGED
|
@@ -1,17 +1,18 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
{aia_dir}/{split}/{filename} — shape (7, 512, 512) float32 .npy
|
| 6 |
{sxr_dir}/{split}/{filename} — shape (N,) float32 .npy
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
fetches so the stream never stalls waiting for np.save to finish.
|
|
|
|
|
|
|
| 11 |
|
| 12 |
Usage:
|
| 13 |
-
python download/hugging_face_data_download.py
|
| 14 |
-
--config download/hf_download_config.yaml
|
| 15 |
"""
|
| 16 |
|
| 17 |
import argparse
|
|
@@ -19,8 +20,10 @@ import logging
|
|
| 19 |
import os
|
| 20 |
import time
|
| 21 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
|
| 22 |
|
| 23 |
import numpy as np
|
|
|
|
| 24 |
import yaml
|
| 25 |
from datasets import load_dataset
|
| 26 |
from huggingface_hub import login
|
|
@@ -28,6 +31,7 @@ from huggingface_hub import login
|
|
| 28 |
|
| 29 |
# HuggingFace uses "validation"; local pipeline directories use "val"
|
| 30 |
HF_TO_LOCAL = {"validation": "val"}
|
|
|
|
| 31 |
|
| 32 |
|
| 33 |
def load_config(path: str) -> dict:
|
|
@@ -35,6 +39,22 @@ def load_config(path: str) -> dict:
|
|
| 35 |
return yaml.safe_load(f)
|
| 36 |
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
def resolve_n(repo_id: str, hf_split: str, cfg: dict) -> int | None:
|
| 39 |
"""
|
| 40 |
Resolve how many rows to download. Returns an exact count, or None for all rows.
|
|
@@ -75,20 +95,6 @@ def build_dataset(repo_id: str, hf_split: str, cfg: dict, n: int | None):
|
|
| 75 |
return ds
|
| 76 |
|
| 77 |
|
| 78 |
-
def _write_arrays(filename: str, aia_arr: np.ndarray, sxr_arr: np.ndarray,
|
| 79 |
-
aia_split_dir: str, sxr_split_dir: str) -> bool:
|
| 80 |
-
"""Save pre-materialized arrays to disk. Returns True if written, False if already exists."""
|
| 81 |
-
aia_path = os.path.join(aia_split_dir, filename)
|
| 82 |
-
sxr_path = os.path.join(sxr_split_dir, filename)
|
| 83 |
-
|
| 84 |
-
if os.path.exists(aia_path) and os.path.exists(sxr_path):
|
| 85 |
-
return False
|
| 86 |
-
|
| 87 |
-
np.save(aia_path, aia_arr)
|
| 88 |
-
np.save(sxr_path, sxr_arr)
|
| 89 |
-
return True
|
| 90 |
-
|
| 91 |
-
|
| 92 |
def download_split(hf_split: str, cfg: dict):
|
| 93 |
local_split = HF_TO_LOCAL.get(hf_split, hf_split)
|
| 94 |
aia_split_dir = os.path.join(cfg["aia_dir"], local_split)
|
|
@@ -156,20 +162,156 @@ def download_split(hf_split: str, cfg: dict):
|
|
| 156 |
print(f"[{hf_split}] Done — {saved} saved, {skipped} skipped | {elapsed/60:.1f} min")
|
| 157 |
|
| 158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
def main():
|
| 160 |
-
parser = argparse.ArgumentParser(description="
|
| 161 |
parser.add_argument("--config", default="download/hf_download_config.yaml")
|
| 162 |
args = parser.parse_args()
|
| 163 |
-
|
| 164 |
cfg = load_config(args.config)
|
| 165 |
|
| 166 |
-
login()
|
| 167 |
-
|
| 168 |
splits = cfg.get("splits", ["train", "validation", "test"])
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
# Silence HF/fsspec connection pool teardown — it throws [Errno 9] Bad file
|
| 174 |
# descriptor during GC which is harmless but noisy.
|
| 175 |
for name in ("huggingface_hub", "urllib3", "fsspec", "datasets"):
|
|
|
|
| 1 |
"""
|
| 2 |
+
Get FOXES data from HuggingFace Hub: either stream it directly, or convert
|
| 3 |
+
parquet files you've already downloaded locally. Reconstructs the local AIA /
|
| 4 |
+
SXR directory layout expected by the pipeline:
|
| 5 |
{aia_dir}/{split}/{filename} — shape (7, 512, 512) float32 .npy
|
| 6 |
{sxr_dir}/{split}/{filename} — shape (N,) float32 .npy
|
| 7 |
|
| 8 |
+
Streaming mode fetches parquet shards on-the-fly without loading the full
|
| 9 |
+
dataset into RAM; a ThreadPoolExecutor overlaps disk writes with network
|
| 10 |
+
fetches so the stream never stalls waiting for np.save to finish. Local mode
|
| 11 |
+
reads parquet files you already have on disk, using bulk/vectorized column
|
| 12 |
+
conversion instead of a per-row Python loop.
|
| 13 |
|
| 14 |
Usage:
|
| 15 |
+
python download/hugging_face_data_download.py --config download/hf_download_config.yaml
|
|
|
|
| 16 |
"""
|
| 17 |
|
| 18 |
import argparse
|
|
|
|
| 20 |
import os
|
| 21 |
import time
|
| 22 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 23 |
+
from pathlib import Path
|
| 24 |
|
| 25 |
import numpy as np
|
| 26 |
+
import pyarrow.parquet as pq
|
| 27 |
import yaml
|
| 28 |
from datasets import load_dataset
|
| 29 |
from huggingface_hub import login
|
|
|
|
| 31 |
|
| 32 |
# HuggingFace uses "validation"; local pipeline directories use "val"
|
| 33 |
HF_TO_LOCAL = {"validation": "val"}
|
| 34 |
+
LOCAL_TO_HF = {v: k for k, v in HF_TO_LOCAL.items()}
|
| 35 |
|
| 36 |
|
| 37 |
def load_config(path: str) -> dict:
|
|
|
|
| 39 |
return yaml.safe_load(f)
|
| 40 |
|
| 41 |
|
| 42 |
+
def _write_arrays(filename: str, aia_arr: np.ndarray, sxr_arr: np.ndarray,
|
| 43 |
+
aia_split_dir: str, sxr_split_dir: str) -> bool:
|
| 44 |
+
"""Save pre-materialized arrays to disk. Returns True if written, False if already exists."""
|
| 45 |
+
aia_path = os.path.join(aia_split_dir, filename)
|
| 46 |
+
sxr_path = os.path.join(sxr_split_dir, filename)
|
| 47 |
+
|
| 48 |
+
if os.path.exists(aia_path) and os.path.exists(sxr_path):
|
| 49 |
+
return False
|
| 50 |
+
|
| 51 |
+
np.save(aia_path, aia_arr)
|
| 52 |
+
np.save(sxr_path, sxr_arr)
|
| 53 |
+
return True
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# --------------------------------------------------------------------------- streaming from HF Hub
|
| 57 |
+
|
| 58 |
def resolve_n(repo_id: str, hf_split: str, cfg: dict) -> int | None:
|
| 59 |
"""
|
| 60 |
Resolve how many rows to download. Returns an exact count, or None for all rows.
|
|
|
|
| 95 |
return ds
|
| 96 |
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
def download_split(hf_split: str, cfg: dict):
|
| 99 |
local_split = HF_TO_LOCAL.get(hf_split, hf_split)
|
| 100 |
aia_split_dir = os.path.join(cfg["aia_dir"], local_split)
|
|
|
|
| 162 |
print(f"[{hf_split}] Done — {saved} saved, {skipped} skipped | {elapsed/60:.1f} min")
|
| 163 |
|
| 164 |
|
| 165 |
+
# --------------------------------------------------------------------------- already-downloaded local parquet
|
| 166 |
+
|
| 167 |
+
def _resolve_local_split_dir(local_parquet_dir: str, hf_split: str):
|
| 168 |
+
"""Find the on-disk subdir for this split, trying both the HF name and its local alias."""
|
| 169 |
+
for candidate in (hf_split, HF_TO_LOCAL.get(hf_split), LOCAL_TO_HF.get(hf_split)):
|
| 170 |
+
if candidate is None:
|
| 171 |
+
continue
|
| 172 |
+
split_dir = os.path.join(local_parquet_dir, candidate)
|
| 173 |
+
if os.path.isdir(split_dir):
|
| 174 |
+
return split_dir
|
| 175 |
+
return None
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def _to_numpy_bulk(col, n):
|
| 179 |
+
"""
|
| 180 |
+
Flatten a nested fixed-size list column directly to numpy, then reshape to
|
| 181 |
+
(n, *element_shape). Avoids per-row Python overhead (table.slice / .as_py()
|
| 182 |
+
on large nested arrays).
|
| 183 |
+
"""
|
| 184 |
+
chunk = col.combine_chunks()
|
| 185 |
+
# Walk the array levels to recover per-element shape. FixedSizeList exposes
|
| 186 |
+
# list_size directly; regular List (which HF uses even for fixed-size
|
| 187 |
+
# arrays) stores a uniform stride in its offsets buffer — both give the
|
| 188 |
+
# same answer.
|
| 189 |
+
shape = []
|
| 190 |
+
vals = chunk
|
| 191 |
+
while hasattr(vals, "values"):
|
| 192 |
+
if hasattr(vals, "list_size"): # FixedSizeList
|
| 193 |
+
shape.append(vals.list_size)
|
| 194 |
+
elif hasattr(vals, "offsets"): # List — stride from offsets
|
| 195 |
+
shape.append(vals.offsets[1].as_py())
|
| 196 |
+
vals = vals.values
|
| 197 |
+
arr = vals.to_numpy(zero_copy_only=False).astype(np.float32)
|
| 198 |
+
return arr.reshape(n, *shape) if shape else arr.reshape(n, arr.size // n)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def convert_local_parquet_split(parquet_dir: str, hf_split: str, cfg: dict):
|
| 202 |
+
local_split = HF_TO_LOCAL.get(hf_split, hf_split)
|
| 203 |
+
aia_split_dir = os.path.join(cfg["aia_dir"], local_split)
|
| 204 |
+
sxr_split_dir = os.path.join(cfg["sxr_dir"], local_split)
|
| 205 |
+
os.makedirs(aia_split_dir, exist_ok=True)
|
| 206 |
+
os.makedirs(sxr_split_dir, exist_ok=True)
|
| 207 |
+
|
| 208 |
+
parquet_files = sorted(Path(parquet_dir).glob("*.parquet"))
|
| 209 |
+
if not parquet_files:
|
| 210 |
+
print(f"[warn] No parquet files found in {parquet_dir}")
|
| 211 |
+
return
|
| 212 |
+
|
| 213 |
+
print(f"\n{'='*50}")
|
| 214 |
+
print(f"Converting split: {hf_split} -> local dir: {local_split}")
|
| 215 |
+
print(f"{'='*50}")
|
| 216 |
+
print(f" Parquet dir: {parquet_dir} ({len(parquet_files)} files)")
|
| 217 |
+
print(f" AIA -> {aia_split_dir}")
|
| 218 |
+
print(f" SXR -> {sxr_split_dir}")
|
| 219 |
+
|
| 220 |
+
num_workers = cfg.get("num_workers", 8)
|
| 221 |
+
print_every = cfg.get("print_every", 500)
|
| 222 |
+
delete_after = cfg.get("delete_parquet_after", False)
|
| 223 |
+
saved = skipped = submitted = 0
|
| 224 |
+
start = time.time()
|
| 225 |
+
|
| 226 |
+
with ThreadPoolExecutor(max_workers=num_workers) as pool:
|
| 227 |
+
futures = {}
|
| 228 |
+
|
| 229 |
+
for pq_file in parquet_files:
|
| 230 |
+
table = pq.read_table(pq_file, columns=["filename", "aia_stack", "sxr_value"])
|
| 231 |
+
n_rows = len(table)
|
| 232 |
+
filenames = table.column("filename").to_pylist()
|
| 233 |
+
|
| 234 |
+
try:
|
| 235 |
+
aia_bulk = _to_numpy_bulk(table.column("aia_stack"), n_rows)
|
| 236 |
+
use_bulk_aia = True
|
| 237 |
+
except Exception:
|
| 238 |
+
aia_pylist = table.column("aia_stack").to_pylist()
|
| 239 |
+
use_bulk_aia = False
|
| 240 |
+
|
| 241 |
+
try:
|
| 242 |
+
sxr_bulk = _to_numpy_bulk(table.column("sxr_value"), n_rows)
|
| 243 |
+
use_bulk_sxr = True
|
| 244 |
+
except Exception:
|
| 245 |
+
sxr_pylist = table.column("sxr_value").to_pylist()
|
| 246 |
+
use_bulk_sxr = False
|
| 247 |
+
|
| 248 |
+
file_futures = []
|
| 249 |
+
for i, filename in enumerate(filenames):
|
| 250 |
+
aia_arr = aia_bulk[i] if use_bulk_aia else np.array(aia_pylist[i], dtype=np.float32)
|
| 251 |
+
sxr_arr = sxr_bulk[i] if use_bulk_sxr else np.array(sxr_pylist[i], dtype=np.float32)
|
| 252 |
+
|
| 253 |
+
fut = pool.submit(_write_arrays, filename, aia_arr, sxr_arr, aia_split_dir, sxr_split_dir)
|
| 254 |
+
file_futures.append(fut)
|
| 255 |
+
futures[fut] = submitted
|
| 256 |
+
submitted += 1
|
| 257 |
+
|
| 258 |
+
if submitted % print_every == 0:
|
| 259 |
+
done = [f for f in futures if f.done()]
|
| 260 |
+
for f in done:
|
| 261 |
+
if f.result():
|
| 262 |
+
saved += 1
|
| 263 |
+
else:
|
| 264 |
+
skipped += 1
|
| 265 |
+
del futures[f]
|
| 266 |
+
|
| 267 |
+
elapsed = time.time() - start
|
| 268 |
+
rate = submitted / elapsed if elapsed > 0 else 0
|
| 269 |
+
print(
|
| 270 |
+
f"[{hf_split}] submitted={submitted} | saved={saved} skipped={skipped} | "
|
| 271 |
+
f"{rate:.1f} rows/sec",
|
| 272 |
+
flush=True,
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
# Wait for all writes from this file to finish before deleting it
|
| 276 |
+
if delete_after:
|
| 277 |
+
for fut in as_completed(file_futures):
|
| 278 |
+
pass
|
| 279 |
+
pq_file.unlink()
|
| 280 |
+
print(f" Deleted {pq_file.name}", flush=True)
|
| 281 |
+
|
| 282 |
+
for fut in as_completed(futures):
|
| 283 |
+
if fut.result():
|
| 284 |
+
saved += 1
|
| 285 |
+
else:
|
| 286 |
+
skipped += 1
|
| 287 |
+
|
| 288 |
+
elapsed = time.time() - start
|
| 289 |
+
print(f"[{hf_split}] Done — {saved} saved, {skipped} skipped | {elapsed/60:.1f} min")
|
| 290 |
+
|
| 291 |
+
|
| 292 |
def main():
|
| 293 |
+
parser = argparse.ArgumentParser(description="Get FOXES data from HuggingFace Hub (stream or local parquet)")
|
| 294 |
parser.add_argument("--config", default="download/hf_download_config.yaml")
|
| 295 |
args = parser.parse_args()
|
|
|
|
| 296 |
cfg = load_config(args.config)
|
| 297 |
|
|
|
|
|
|
|
| 298 |
splits = cfg.get("splits", ["train", "validation", "test"])
|
| 299 |
+
local_parquet_dir = cfg.get("local_parquet_dir")
|
| 300 |
+
|
| 301 |
+
if local_parquet_dir:
|
| 302 |
+
print(f"Converting local parquet files from {local_parquet_dir} (no HF Hub access needed)")
|
| 303 |
+
for split in splits:
|
| 304 |
+
split_dir = _resolve_local_split_dir(local_parquet_dir, split)
|
| 305 |
+
if split_dir is None:
|
| 306 |
+
print(f"[warn] Split dir not found under {local_parquet_dir}, skipping: {split}")
|
| 307 |
+
continue
|
| 308 |
+
convert_local_parquet_split(split_dir, split, cfg)
|
| 309 |
+
else:
|
| 310 |
+
login()
|
| 311 |
+
for split in splits:
|
| 312 |
+
download_split(split, cfg)
|
| 313 |
+
|
| 314 |
+
print("\nAll splits done.")
|
| 315 |
# Silence HF/fsspec connection pool teardown — it throws [Errno 9] Bad file
|
| 316 |
# descriptor during GC which is harmless but noisy.
|
| 317 |
for name in ("huggingface_hub", "urllib3", "fsspec", "datasets"):
|
download/sdo_download_config.yaml
CHANGED
|
@@ -10,11 +10,11 @@
|
|
| 10 |
# python download/download_sdo.py --config download/sdo_download_config.yaml
|
| 11 |
# =============================================================================
|
| 12 |
|
| 13 |
-
download_dir: "/
|
| 14 |
-
email: "
|
| 15 |
|
| 16 |
start_date: "2023-08-01 00:00:00"
|
| 17 |
-
end_date: "2023-
|
| 18 |
-
cadence:
|
| 19 |
|
| 20 |
wavelengths: [94, 131, 171, 193, 211, 304, 335]
|
|
|
|
| 10 |
# python download/download_sdo.py --config download/sdo_download_config.yaml
|
| 11 |
# =============================================================================
|
| 12 |
|
| 13 |
+
download_dir: "/Volumes/T9/testing_foxes/AIA_raw"
|
| 14 |
+
email: "ggoodwin5@gsu.edu" # must be registered with JSOC: http://jsoc.stanford.edu/ajax/register_email.html
|
| 15 |
|
| 16 |
start_date: "2023-08-01 00:00:00"
|
| 17 |
+
end_date: "2023-12-01 00:00:00"
|
| 18 |
+
cadence: 7200 # minutes between samples
|
| 19 |
|
| 20 |
wavelengths: [94, 131, 171, 193, 211, 304, 335]
|
download/sxr_download_config.yaml
CHANGED
|
@@ -1,19 +1,19 @@
|
|
| 1 |
# =============================================================================
|
| 2 |
# Raw GOES/SXR Download Configuration
|
| 3 |
# =============================================================================
|
| 4 |
-
# Used by
|
| 5 |
# This is the advanced/raw path — most users should use
|
| 6 |
# hugging_face_data_download.py instead (see the README). Data downloaded
|
| 7 |
# here needs the data/ scripts to become the paired .npy format the model
|
| 8 |
# expects.
|
| 9 |
#
|
| 10 |
# Usage:
|
| 11 |
-
# python download/
|
| 12 |
# =============================================================================
|
| 13 |
|
| 14 |
-
save_dir: "/
|
| 15 |
|
| 16 |
start_date: "2023-08-01"
|
| 17 |
-
end_date: "2023-08-
|
| 18 |
|
| 19 |
max_workers: 4
|
|
|
|
| 1 |
# =============================================================================
|
| 2 |
# Raw GOES/SXR Download Configuration
|
| 3 |
# =============================================================================
|
| 4 |
+
# Used by download_sxr.py to pull raw GOES XRS netCDF files via SunPy Fido.
|
| 5 |
# This is the advanced/raw path — most users should use
|
| 6 |
# hugging_face_data_download.py instead (see the README). Data downloaded
|
| 7 |
# here needs the data/ scripts to become the paired .npy format the model
|
| 8 |
# expects.
|
| 9 |
#
|
| 10 |
# Usage:
|
| 11 |
+
# python download/download_sxr.py --config download/sxr_download_config.yaml
|
| 12 |
# =============================================================================
|
| 13 |
|
| 14 |
+
save_dir: "/Volumes/T9/testing_foxes/GOES_raw"
|
| 15 |
|
| 16 |
start_date: "2023-08-01"
|
| 17 |
+
end_date: "2023-08-31"
|
| 18 |
|
| 19 |
max_workers: 4
|
forecasting/dataset.py
CHANGED
|
@@ -4,6 +4,8 @@ import pandas as pd
|
|
| 4 |
import torch
|
| 5 |
import numpy as np
|
| 6 |
from pathlib import Path
|
|
|
|
|
|
|
| 7 |
import glob
|
| 8 |
|
| 9 |
|
|
@@ -170,3 +172,56 @@ class AIAGOESDataset(torch.utils.data.Dataset):
|
|
| 170 |
return timestamp
|
| 171 |
|
| 172 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import torch
|
| 5 |
import numpy as np
|
| 6 |
from pathlib import Path
|
| 7 |
+
from pytorch_lightning import LightningDataModule
|
| 8 |
+
from torch.utils.data import DataLoader
|
| 9 |
import glob
|
| 10 |
|
| 11 |
|
|
|
|
| 172 |
return timestamp
|
| 173 |
|
| 174 |
|
| 175 |
+
class AIAGOESDataModule(LightningDataModule):
|
| 176 |
+
"""
|
| 177 |
+
PyTorch Lightning DataModule wiring up train/val/test AIAGOESDataset splits.
|
| 178 |
+
Used by train.py.
|
| 179 |
+
|
| 180 |
+
Parameters
|
| 181 |
+
----------
|
| 182 |
+
aia_train_dir, aia_val_dir, aia_test_dir : str
|
| 183 |
+
Directories of AIA .npy files for each split.
|
| 184 |
+
sxr_train_dir, sxr_val_dir, sxr_test_dir : str
|
| 185 |
+
Directories of SXR .npy files for each split.
|
| 186 |
+
sxr_norm : np.ndarray
|
| 187 |
+
(mean, std) used to log-normalize SXR targets.
|
| 188 |
+
batch_size, num_workers : int
|
| 189 |
+
wavelengths : list of int
|
| 190 |
+
"""
|
| 191 |
+
|
| 192 |
+
def __init__(self, aia_train_dir, aia_val_dir, aia_test_dir, sxr_train_dir, sxr_val_dir, sxr_test_dir,
|
| 193 |
+
sxr_norm, batch_size=64, num_workers=4, wavelengths=[94, 131, 171, 193, 211, 304, 335]):
|
| 194 |
+
super().__init__()
|
| 195 |
+
self.aia_train_dir = aia_train_dir
|
| 196 |
+
self.aia_val_dir = aia_val_dir
|
| 197 |
+
self.aia_test_dir = aia_test_dir
|
| 198 |
+
self.sxr_train_dir = sxr_train_dir
|
| 199 |
+
self.sxr_val_dir = sxr_val_dir
|
| 200 |
+
self.sxr_test_dir = sxr_test_dir
|
| 201 |
+
self.sxr_norm = sxr_norm
|
| 202 |
+
self.batch_size = batch_size
|
| 203 |
+
self.num_workers = num_workers
|
| 204 |
+
self.wavelengths = wavelengths
|
| 205 |
+
|
| 206 |
+
def setup(self, stage=None):
|
| 207 |
+
transform = SXRLogNormTransform(self.sxr_norm[0], self.sxr_norm[1])
|
| 208 |
+
self.train_ds = AIAGOESDataset(aia_dir=self.aia_train_dir, sxr_dir=self.sxr_train_dir,
|
| 209 |
+
sxr_transform=transform, wavelengths=self.wavelengths)
|
| 210 |
+
self.val_ds = AIAGOESDataset(aia_dir=self.aia_val_dir, sxr_dir=self.sxr_val_dir,
|
| 211 |
+
sxr_transform=transform, wavelengths=self.wavelengths)
|
| 212 |
+
self.test_ds = AIAGOESDataset(aia_dir=self.aia_test_dir, sxr_dir=self.sxr_test_dir,
|
| 213 |
+
sxr_transform=transform, wavelengths=self.wavelengths)
|
| 214 |
+
|
| 215 |
+
def train_dataloader(self):
|
| 216 |
+
return DataLoader(self.train_ds, batch_size=self.batch_size, shuffle=True,
|
| 217 |
+
num_workers=self.num_workers, prefetch_factor=4 if self.num_workers else None)
|
| 218 |
+
|
| 219 |
+
def val_dataloader(self):
|
| 220 |
+
return DataLoader(self.val_ds, batch_size=self.batch_size, shuffle=False,
|
| 221 |
+
num_workers=self.num_workers, prefetch_factor=4 if self.num_workers else None)
|
| 222 |
+
|
| 223 |
+
def test_dataloader(self):
|
| 224 |
+
return DataLoader(self.test_ds, batch_size=self.batch_size, shuffle=False,
|
| 225 |
+
num_workers=self.num_workers, prefetch_factor=1 if self.num_workers else None)
|
| 226 |
+
|
| 227 |
+
|
forecasting/evaluation.py
CHANGED
|
@@ -474,7 +474,7 @@ def load_evaluation_config(config_path):
|
|
| 474 |
config_data = yaml.load(stream, Loader=yaml.SafeLoader)
|
| 475 |
|
| 476 |
# Resolve variable substitutions
|
| 477 |
-
config_data = resolve_config_variables(config_data)
|
| 478 |
return config_data
|
| 479 |
|
| 480 |
|
|
@@ -488,7 +488,7 @@ def main():
|
|
| 488 |
import argparse
|
| 489 |
|
| 490 |
parser = argparse.ArgumentParser(description='Run FOXES solar flare evaluation')
|
| 491 |
-
parser.add_argument('-config', type=str, default='evaluation_config.yaml',
|
| 492 |
help='Path to evaluation config YAML file')
|
| 493 |
args = parser.parse_args()
|
| 494 |
|
|
|
|
| 474 |
config_data = yaml.load(stream, Loader=yaml.SafeLoader)
|
| 475 |
|
| 476 |
# Resolve variable substitutions
|
| 477 |
+
config_data: dict = resolve_config_variables(config_data)
|
| 478 |
return config_data
|
| 479 |
|
| 480 |
|
|
|
|
| 488 |
import argparse
|
| 489 |
|
| 490 |
parser = argparse.ArgumentParser(description='Run FOXES solar flare evaluation')
|
| 491 |
+
parser.add_argument('--config', type=str, default='evaluation_config.yaml',
|
| 492 |
help='Path to evaluation config YAML file')
|
| 493 |
args = parser.parse_args()
|
| 494 |
|
forecasting/evaluation_config.yaml
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
# =============================================================================
|
| 4 |
# Used by evaluation.py to compute metrics and generate plots.
|
| 5 |
#
|
| 6 |
-
# Usage: python evaluation.py -config evaluation_config.yaml
|
| 7 |
# =============================================================================
|
| 8 |
|
| 9 |
model_predictions:
|
|
|
|
| 3 |
# =============================================================================
|
| 4 |
# Used by evaluation.py to compute metrics and generate plots.
|
| 5 |
#
|
| 6 |
+
# Usage: python evaluation.py --config evaluation_config.yaml
|
| 7 |
# =============================================================================
|
| 8 |
|
| 9 |
model_predictions:
|
forecasting/inference.py
CHANGED
|
@@ -143,8 +143,6 @@ def evaluate_model_on_dataset(model, dataset, batch_size=16, times=None, config_
|
|
| 143 |
flux_contributions = None
|
| 144 |
|
| 145 |
current_batch_size = predictions.shape[0]
|
| 146 |
-
batch_weights = []
|
| 147 |
-
batch_flux_contributions = []
|
| 148 |
|
| 149 |
# Process each sample in the batch to reduce memory footprint
|
| 150 |
for i in range(current_batch_size):
|
|
@@ -319,14 +317,14 @@ def main():
|
|
| 319 |
return recursive_substitute(config_dict, variables)
|
| 320 |
|
| 321 |
parser = argparse.ArgumentParser()
|
| 322 |
-
parser.add_argument('-config', type=str, default='inference_config.yaml', required=True,
|
| 323 |
help='Path to the inference configuration YAML file.')
|
| 324 |
args = parser.parse_args()
|
| 325 |
|
| 326 |
with open(args.config, 'r') as stream:
|
| 327 |
config_data = yaml.load(stream, Loader=yaml.SafeLoader)
|
| 328 |
|
| 329 |
-
config_data = resolve_config_variables(config_data)
|
| 330 |
|
| 331 |
model_params = config_data.get('model_params', {})
|
| 332 |
input_size = model_params.get('input_size', 512)
|
|
|
|
| 143 |
flux_contributions = None
|
| 144 |
|
| 145 |
current_batch_size = predictions.shape[0]
|
|
|
|
|
|
|
| 146 |
|
| 147 |
# Process each sample in the batch to reduce memory footprint
|
| 148 |
for i in range(current_batch_size):
|
|
|
|
| 317 |
return recursive_substitute(config_dict, variables)
|
| 318 |
|
| 319 |
parser = argparse.ArgumentParser()
|
| 320 |
+
parser.add_argument('--config', type=str, default='inference_config.yaml', required=True,
|
| 321 |
help='Path to the inference configuration YAML file.')
|
| 322 |
args = parser.parse_args()
|
| 323 |
|
| 324 |
with open(args.config, 'r') as stream:
|
| 325 |
config_data = yaml.load(stream, Loader=yaml.SafeLoader)
|
| 326 |
|
| 327 |
+
config_data: dict = resolve_config_variables(config_data)
|
| 328 |
|
| 329 |
model_params = config_data.get('model_params', {})
|
| 330 |
input_size = model_params.get('input_size', 512)
|
forecasting/inference_config.yaml
CHANGED
|
@@ -5,7 +5,7 @@
|
|
| 5 |
# AIA data and write out SXR predictions (+ optional attention/flux maps).
|
| 6 |
#
|
| 7 |
# Usage:
|
| 8 |
-
# python forecasting/inference.py -config forecasting/inference_config.yaml
|
| 9 |
# =============================================================================
|
| 10 |
|
| 11 |
wavelengths: [94, 131, 171, 193, 211, 304, 335]
|
|
|
|
| 5 |
# AIA data and write out SXR predictions (+ optional attention/flux maps).
|
| 6 |
#
|
| 7 |
# Usage:
|
| 8 |
+
# python forecasting/inference.py --config forecasting/inference_config.yaml
|
| 9 |
# =============================================================================
|
| 10 |
|
| 11 |
wavelengths: [94, 131, 171, 193, 211, 304, 335]
|
forecasting/trained_weights_and_normalization/{FOXES_Model_Checkpoint.ckpt → inverted-attention-mask.ckpt}
RENAMED
|
File without changes
|
forecasting/trained_weights_and_normalization/localized-attention-mask.ckpt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:df37bef4ac95a3c14e777221ce1fd3fbdf56da78a192d7418303848d84aa2f46
|
| 3 |
+
size 224141974
|
forecasting/trained_weights_and_normalization/no-attention-mask.ckpt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0d6965e5b7c1f41565b1eebf059ce5506d6e6c8b8483f7b569a854f1902ae45b
|
| 3 |
+
size 89939767
|
training/__init__.py
ADDED
|
File without changes
|
training/callbacks.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PyTorch Lightning callbacks for visualizing training progress: predicted vs.
|
| 3 |
+
true SXR flux, and Vision Transformer attention maps, logged to Weights & Biases.
|
| 4 |
+
|
| 5 |
+
Used by train.py — not meant to be run standalone.
|
| 6 |
+
"""
|
| 7 |
+
import wandb
|
| 8 |
+
from pytorch_lightning import Callback
|
| 9 |
+
import matplotlib.pyplot as plt
|
| 10 |
+
import numpy as np
|
| 11 |
+
import torch
|
| 12 |
+
|
| 13 |
+
from forecasting.model import unnormalize_sxr
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ImagePredictionLogger_SXR(Callback):
|
| 17 |
+
"""
|
| 18 |
+
PyTorch Lightning callback for logging AIA input images and corresponding
|
| 19 |
+
true vs predicted Soft X-Ray (SXR) flux values to Weights & Biases (wandb).
|
| 20 |
+
|
| 21 |
+
This helps monitor model performance across validation epochs by
|
| 22 |
+
comparing predicted vs. ground-truth flare intensities.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
def __init__(self, data_samples, sxr_norm):
|
| 26 |
+
"""
|
| 27 |
+
Initialize callback with validation samples and normalization parameters.
|
| 28 |
+
|
| 29 |
+
Parameters
|
| 30 |
+
----------
|
| 31 |
+
data_samples : list
|
| 32 |
+
List of validation samples (AIA image, SXR target pairs).
|
| 33 |
+
sxr_norm : np.ndarray
|
| 34 |
+
Normalization statistics used to unnormalize predicted flux values.
|
| 35 |
+
"""
|
| 36 |
+
super().__init__()
|
| 37 |
+
self.data_samples = data_samples
|
| 38 |
+
self.sxr_norm = sxr_norm
|
| 39 |
+
|
| 40 |
+
def on_validation_epoch_end(self, trainer, pl_module):
|
| 41 |
+
"""
|
| 42 |
+
Log scatter plots comparing predicted and true SXR flux values
|
| 43 |
+
at the end of each validation epoch.
|
| 44 |
+
"""
|
| 45 |
+
true_sxr = []
|
| 46 |
+
pred_sxr = []
|
| 47 |
+
|
| 48 |
+
for aia, target in self.data_samples:
|
| 49 |
+
aia = aia.to(pl_module.device).unsqueeze(0)
|
| 50 |
+
# forward() always returns a tuple (global_flux_raw, ...); we only need the flux.
|
| 51 |
+
pred, *_ = pl_module(aia, return_attention=False)
|
| 52 |
+
pred_sxr.append(pred.item())
|
| 53 |
+
true_sxr.append(target.item())
|
| 54 |
+
|
| 55 |
+
true_unorm = unnormalize_sxr(np.array(true_sxr, dtype=np.float32), self.sxr_norm)
|
| 56 |
+
pred_unnorm = unnormalize_sxr(np.array(pred_sxr, dtype=np.float32), self.sxr_norm)
|
| 57 |
+
|
| 58 |
+
fig1 = self.plot_aia_sxr(true_unorm, pred_unnorm)
|
| 59 |
+
trainer.logger.experiment.log({"Soft X-ray flux plots": wandb.Image(fig1)})
|
| 60 |
+
plt.close(fig1)
|
| 61 |
+
|
| 62 |
+
# Flare-class range: A-class starts at 1e-8 W/m^2, X10 is 10x the 1e-4 X-class threshold.
|
| 63 |
+
AXIS_MIN = 1e-8
|
| 64 |
+
AXIS_MAX = 1e-3
|
| 65 |
+
|
| 66 |
+
def plot_aia_sxr(self, val_sxr, pred_sxr):
|
| 67 |
+
"""Log-log parity plot: predicted vs. true SXR flux, with a 1:1 reference line."""
|
| 68 |
+
fig, ax = plt.subplots(1, 1, figsize=(4, 4))
|
| 69 |
+
|
| 70 |
+
ax.plot([self.AXIS_MIN, self.AXIS_MAX], [self.AXIS_MIN, self.AXIS_MAX],
|
| 71 |
+
color='gray', linestyle='--', linewidth=1, label='Perfect prediction')
|
| 72 |
+
ax.scatter(val_sxr, pred_sxr, color='blue', alpha=0.7,s=10, label='Predictions')
|
| 73 |
+
|
| 74 |
+
ax.set_xscale('log')
|
| 75 |
+
ax.set_yscale('log')
|
| 76 |
+
ax.set_xlim(self.AXIS_MIN, self.AXIS_MAX)
|
| 77 |
+
ax.set_ylim(self.AXIS_MIN, self.AXIS_MAX)
|
| 78 |
+
ax.set_xlabel("True SXR flux [W/m$^2$]")
|
| 79 |
+
ax.set_ylabel("Predicted SXR flux [W/m$^2$]")
|
| 80 |
+
ax.legend()
|
| 81 |
+
fig.tight_layout()
|
| 82 |
+
return fig
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class AttentionMapCallback(Callback):
|
| 86 |
+
"""
|
| 87 |
+
PyTorch Lightning callback for visualizing transformer attention maps
|
| 88 |
+
during validation epochs.
|
| 89 |
+
|
| 90 |
+
Supports CLS-token-based and local patch attention visualization.
|
| 91 |
+
"""
|
| 92 |
+
|
| 93 |
+
def __init__(self, log_every_n_epochs=1, num_samples=4, patch_size=8, use_local_attention=False):
|
| 94 |
+
"""
|
| 95 |
+
Initialize callback.
|
| 96 |
+
|
| 97 |
+
Parameters
|
| 98 |
+
----------
|
| 99 |
+
log_every_n_epochs : int
|
| 100 |
+
Frequency of logging attention maps.
|
| 101 |
+
num_samples : int
|
| 102 |
+
Number of samples to visualize per epoch.
|
| 103 |
+
patch_size : int
|
| 104 |
+
Patch size used in the Vision Transformer.
|
| 105 |
+
use_local_attention : bool
|
| 106 |
+
If True, visualize local attention patterns instead of CLS attention.
|
| 107 |
+
"""
|
| 108 |
+
super().__init__()
|
| 109 |
+
self.patch_size = patch_size
|
| 110 |
+
self.log_every_n_epochs = log_every_n_epochs
|
| 111 |
+
self.num_samples = num_samples
|
| 112 |
+
self.use_local_attention = use_local_attention
|
| 113 |
+
|
| 114 |
+
def on_validation_epoch_end(self, trainer, pl_module):
|
| 115 |
+
"""Trigger visualization of attention maps at the end of validation epochs."""
|
| 116 |
+
if trainer.current_epoch % self.log_every_n_epochs == 0:
|
| 117 |
+
self._visualize_attention(trainer, pl_module)
|
| 118 |
+
|
| 119 |
+
def _visualize_attention(self, trainer, pl_module):
|
| 120 |
+
"""Generate and log attention maps from the model's attention weights."""
|
| 121 |
+
val_dataloader = trainer.val_dataloaders
|
| 122 |
+
if val_dataloader is None:
|
| 123 |
+
return
|
| 124 |
+
|
| 125 |
+
pl_module.eval()
|
| 126 |
+
with torch.no_grad():
|
| 127 |
+
batch = next(iter(val_dataloader))
|
| 128 |
+
imgs, labels = batch
|
| 129 |
+
imgs = imgs[:self.num_samples].to(pl_module.device)
|
| 130 |
+
|
| 131 |
+
# forward(return_attention=True) -> (global_flux_raw, attention_weights, patch_flux_raw)
|
| 132 |
+
_, attention_weights, patch_flux_raw = pl_module(imgs, return_attention=True)
|
| 133 |
+
|
| 134 |
+
for sample_idx in range(min(self.num_samples, imgs.size(0))):
|
| 135 |
+
fig = self._plot_attention_map(
|
| 136 |
+
imgs[sample_idx],
|
| 137 |
+
attention_weights,
|
| 138 |
+
sample_idx,
|
| 139 |
+
trainer.current_epoch,
|
| 140 |
+
patch_size=self.patch_size,
|
| 141 |
+
patch_flux=patch_flux_raw[sample_idx] if patch_flux_raw is not None else None
|
| 142 |
+
)
|
| 143 |
+
trainer.logger.experiment.log({"Attention plots": wandb.Image(fig)})
|
| 144 |
+
plt.close(fig)
|
| 145 |
+
|
| 146 |
+
def _plot_attention_map(self, image, attention_weights, sample_idx, epoch, patch_size, patch_flux=None):
|
| 147 |
+
"""Plot and return a visualization of the attention heatmaps for a single image."""
|
| 148 |
+
img_np = image.cpu().numpy()
|
| 149 |
+
if len(img_np.shape) == 3 and img_np.shape[0] in [1, 3]:
|
| 150 |
+
img_np = np.transpose(img_np, (1, 2, 0))
|
| 151 |
+
|
| 152 |
+
H, W = img_np.shape[:2]
|
| 153 |
+
grid_h, grid_w = H // patch_size, W // patch_size
|
| 154 |
+
|
| 155 |
+
last_layer_attention = attention_weights[-1]
|
| 156 |
+
sample_attention = last_layer_attention[sample_idx]
|
| 157 |
+
avg_attention = sample_attention.mean(dim=0)
|
| 158 |
+
|
| 159 |
+
if self.use_local_attention:
|
| 160 |
+
# Spatial center of the grid, not the middle of the flattened sequence
|
| 161 |
+
# (those only coincide when grid_w == 1) — row-major flatten: idx = row*grid_w + col.
|
| 162 |
+
center_patch_idx = (grid_h // 2) * grid_w + (grid_w // 2)
|
| 163 |
+
center_attention = avg_attention[center_patch_idx, :].cpu()
|
| 164 |
+
avg_attention_map = avg_attention.mean(dim=0).cpu()
|
| 165 |
+
attention_map = avg_attention_map.reshape(grid_h, grid_w)
|
| 166 |
+
center_map = center_attention.reshape(grid_h, grid_w)
|
| 167 |
+
else:
|
| 168 |
+
cls_attention = avg_attention[0, 1:].cpu()
|
| 169 |
+
attention_map = cls_attention.reshape(grid_h, grid_w)
|
| 170 |
+
center_map = None
|
| 171 |
+
|
| 172 |
+
if len(img_np[0, 0, :]) >= 6:
|
| 173 |
+
rgb_channels = [0, 2, 4]
|
| 174 |
+
img_display = np.stack([(img_np[:, :, i] + 1) / 2 for i in rgb_channels], axis=2)
|
| 175 |
+
img_display = np.clip(img_display, 0, 1)
|
| 176 |
+
else:
|
| 177 |
+
img_display = (img_np[:, :, 0] + 1) / 2
|
| 178 |
+
img_display = np.stack([img_display] * 3, axis=2)
|
| 179 |
+
|
| 180 |
+
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
|
| 181 |
+
fig.suptitle(f'Attention Visualization - Epoch {epoch}, Sample {sample_idx}', fontsize=16)
|
| 182 |
+
|
| 183 |
+
axes[0, 0].imshow(img_display)
|
| 184 |
+
axes[0, 0].set_title('Original Image')
|
| 185 |
+
axes[0, 0].axis('off')
|
| 186 |
+
|
| 187 |
+
im1 = axes[0, 1].imshow(attention_map, cmap='hot', interpolation='nearest')
|
| 188 |
+
axes[0, 1].set_title('Attention Map')
|
| 189 |
+
axes[0, 1].axis('off')
|
| 190 |
+
plt.colorbar(im1, ax=axes[0, 1])
|
| 191 |
+
|
| 192 |
+
axes[0, 2].imshow(img_display)
|
| 193 |
+
axes[0, 2].imshow(attention_map, cmap='hot', alpha=0.6, interpolation='nearest')
|
| 194 |
+
axes[0, 2].set_title('Attention Overlay')
|
| 195 |
+
axes[0, 2].axis('off')
|
| 196 |
+
|
| 197 |
+
if center_map is not None:
|
| 198 |
+
im2 = axes[1, 0].imshow(center_map, cmap='hot', interpolation='nearest')
|
| 199 |
+
axes[1, 0].set_title('Center Patch Attention')
|
| 200 |
+
axes[1, 0].axis('off')
|
| 201 |
+
plt.colorbar(im2, ax=axes[1, 0])
|
| 202 |
+
else:
|
| 203 |
+
axes[1, 0].text(0.5, 0.5, 'Center attention\nnot available',
|
| 204 |
+
ha='center', va='center', transform=axes[1, 0].transAxes)
|
| 205 |
+
axes[1, 0].set_title('Center Patch Attention')
|
| 206 |
+
axes[1, 0].axis('off')
|
| 207 |
+
|
| 208 |
+
if patch_flux is not None:
|
| 209 |
+
patch_flux_np = patch_flux.cpu().numpy().reshape(grid_h, grid_w)
|
| 210 |
+
im3 = axes[1, 1].imshow(patch_flux_np, cmap='viridis', interpolation='nearest')
|
| 211 |
+
axes[1, 1].set_title('Patch Flux')
|
| 212 |
+
axes[1, 1].axis('off')
|
| 213 |
+
plt.colorbar(im3, ax=axes[1, 1])
|
| 214 |
+
else:
|
| 215 |
+
axes[1, 1].text(0.5, 0.5, 'Patch flux\nnot available',
|
| 216 |
+
ha='center', va='center', transform=axes[1, 1].transAxes)
|
| 217 |
+
axes[1, 1].set_title('Patch Flux')
|
| 218 |
+
axes[1, 1].axis('off')
|
| 219 |
+
|
| 220 |
+
axes[1, 2].hist(attention_map.flatten(), bins=50, alpha=0.7)
|
| 221 |
+
axes[1, 2].set_title('Attention Distribution')
|
| 222 |
+
axes[1, 2].set_xlabel('Attention Weight')
|
| 223 |
+
axes[1, 2].set_ylabel('Frequency')
|
| 224 |
+
|
| 225 |
+
plt.tight_layout()
|
| 226 |
+
return fig
|
training/train.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Training script for the AIA-GOES multimodal solar flare forecasting model using PyTorch Lightning.
|
| 3 |
+
|
| 4 |
+
This script:
|
| 5 |
+
1. Loads configuration from a YAML file with variable substitution (e.g., ${base_dir} references).
|
| 6 |
+
2. Initializes the AIA-GOES DataModule.
|
| 7 |
+
3. Configures logging with Weights & Biases.
|
| 8 |
+
4. Builds and trains a Vision Transformer (ViTLocal) model.
|
| 9 |
+
5. Optionally computes dynamic base class weights for flare categories (Quiet, C, M, X).
|
| 10 |
+
6. Saves model checkpoints (.ckpt).
|
| 11 |
+
|
| 12 |
+
Usage:
|
| 13 |
+
python training/train.py --config training/train_config.yaml
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
import os
|
| 18 |
+
import re
|
| 19 |
+
import sys
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
import torch
|
| 24 |
+
import wandb
|
| 25 |
+
import yaml
|
| 26 |
+
from pytorch_lightning import Trainer
|
| 27 |
+
from pytorch_lightning.callbacks import ModelCheckpoint
|
| 28 |
+
from pytorch_lightning.loggers import WandbLogger
|
| 29 |
+
|
| 30 |
+
PROJECT_ROOT = Path(__file__).parent.parent.absolute()
|
| 31 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 32 |
+
|
| 33 |
+
from training.callbacks import AttentionMapCallback, ImagePredictionLogger_SXR
|
| 34 |
+
from forecasting.dataset import AIAGOESDataModule
|
| 35 |
+
from forecasting.model import ViTLocal, unnormalize_sxr
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def resolve_config_variables(config_dict):
|
| 39 |
+
"""
|
| 40 |
+
Recursively resolve ${variable} references within the config.
|
| 41 |
+
|
| 42 |
+
This function processes configuration dictionaries to substitute variable
|
| 43 |
+
references of the form ${variable_name} with their actual values defined
|
| 44 |
+
elsewhere in the configuration.
|
| 45 |
+
"""
|
| 46 |
+
variables = {}
|
| 47 |
+
for key, value in config_dict.items():
|
| 48 |
+
if isinstance(value, str) and not value.startswith('${'):
|
| 49 |
+
variables[key] = value
|
| 50 |
+
|
| 51 |
+
def substitute_value(value, variables):
|
| 52 |
+
if isinstance(value, str):
|
| 53 |
+
pattern = r'\$\{([^}]+)\}'
|
| 54 |
+
for match in re.finditer(pattern, value):
|
| 55 |
+
var_name = match.group(1)
|
| 56 |
+
if var_name in variables:
|
| 57 |
+
value = value.replace(f'${{{var_name}}}', variables[var_name])
|
| 58 |
+
return value
|
| 59 |
+
|
| 60 |
+
def recursive_substitute(obj, variables):
|
| 61 |
+
if isinstance(obj, dict):
|
| 62 |
+
return {k: recursive_substitute(v, variables) for k, v in obj.items()}
|
| 63 |
+
elif isinstance(obj, list):
|
| 64 |
+
return [recursive_substitute(item, variables) for item in obj]
|
| 65 |
+
else:
|
| 66 |
+
return substitute_value(obj, variables)
|
| 67 |
+
|
| 68 |
+
return recursive_substitute(config_dict, variables)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def get_base_weights(data_module, sxr_norm):
|
| 72 |
+
"""
|
| 73 |
+
Compute inverse-frequency weights for flare classes based on training data.
|
| 74 |
+
|
| 75 |
+
The weights help balance loss contributions from imbalanced flare categories
|
| 76 |
+
by making rare classes (M/X flares) count for more in the loss.
|
| 77 |
+
|
| 78 |
+
Parameters
|
| 79 |
+
----------
|
| 80 |
+
data_module : AIAGOESDataModule
|
| 81 |
+
Initialized DataModule providing the train_dataloader.
|
| 82 |
+
sxr_norm : np.ndarray
|
| 83 |
+
Normalization parameters for SXR.
|
| 84 |
+
|
| 85 |
+
Returns
|
| 86 |
+
-------
|
| 87 |
+
dict
|
| 88 |
+
Class weights for quiet, C, M, and X classes.
|
| 89 |
+
"""
|
| 90 |
+
print("Calculating base weights from training data...")
|
| 91 |
+
c_threshold, m_threshold, x_threshold = 1e-6, 1e-5, 1e-4
|
| 92 |
+
|
| 93 |
+
quiet_count = c_count = m_count = x_count = total = 0
|
| 94 |
+
train_loader = data_module.train_dataloader()
|
| 95 |
+
print(f"Processing {len(train_loader)} batches...")
|
| 96 |
+
|
| 97 |
+
for batch_idx, (aia_batch, sxr_batch) in enumerate(train_loader):
|
| 98 |
+
if batch_idx % 50 == 0:
|
| 99 |
+
print(f"Processed {batch_idx}/{len(train_loader)} batches...")
|
| 100 |
+
|
| 101 |
+
sxr_un = unnormalize_sxr(sxr_batch, sxr_norm)
|
| 102 |
+
sxr_un_flat = sxr_un.reshape(-1)
|
| 103 |
+
|
| 104 |
+
total += len(sxr_un_flat)
|
| 105 |
+
quiet_count += (sxr_un_flat < c_threshold).sum()
|
| 106 |
+
c_count += ((sxr_un_flat >= c_threshold) & (sxr_un_flat < m_threshold)).sum()
|
| 107 |
+
m_count += ((sxr_un_flat >= m_threshold) & (sxr_un_flat < x_threshold)).sum()
|
| 108 |
+
x_count += (sxr_un_flat >= x_threshold).sum()
|
| 109 |
+
|
| 110 |
+
quiet_count, c_count, m_count, x_count = (max(c, 1) for c in (quiet_count, c_count, m_count, x_count))
|
| 111 |
+
|
| 112 |
+
weights = {
|
| 113 |
+
'quiet': total / quiet_count,
|
| 114 |
+
'c_class': total / c_count,
|
| 115 |
+
'm_class': total / m_count,
|
| 116 |
+
'x_class': total / x_count,
|
| 117 |
+
}
|
| 118 |
+
print(f"Total samples: {total}")
|
| 119 |
+
print(f"Quiet: {quiet_count} (weight {weights['quiet']:.4f}), "
|
| 120 |
+
f"C: {c_count} (weight {weights['c_class']:.4f}), "
|
| 121 |
+
f"M: {m_count} (weight {weights['m_class']:.4f}), "
|
| 122 |
+
f"X: {x_count} (weight {weights['x_class']:.4f})")
|
| 123 |
+
return weights
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def resolve_devices(gpu_config):
|
| 127 |
+
"""Resolve accelerator/devices/strategy from the gpu_ids config value."""
|
| 128 |
+
if gpu_config == -1:
|
| 129 |
+
print("Using CPU for training")
|
| 130 |
+
return "cpu", 1, "auto"
|
| 131 |
+
|
| 132 |
+
if not torch.cuda.is_available():
|
| 133 |
+
print("No GPUs available, falling back to CPU")
|
| 134 |
+
return "cpu", 1, "auto"
|
| 135 |
+
|
| 136 |
+
if gpu_config == "all":
|
| 137 |
+
print(f"Using all available GPUs ({torch.cuda.device_count()} GPUs)")
|
| 138 |
+
return "gpu", -1, "auto"
|
| 139 |
+
if isinstance(gpu_config, list):
|
| 140 |
+
print(f"Using GPUs: {gpu_config}")
|
| 141 |
+
return "gpu", gpu_config, "auto"
|
| 142 |
+
print(f"Using GPU {gpu_config}")
|
| 143 |
+
return "gpu", [gpu_config], "auto"
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def main():
|
| 147 |
+
parser = argparse.ArgumentParser(description='Train the FOXES ViTLocal model.')
|
| 148 |
+
parser.add_argument('--config', type=str, default='training/train_config.yaml', required=True,
|
| 149 |
+
help='Path to train_config.yaml')
|
| 150 |
+
args = parser.parse_args()
|
| 151 |
+
|
| 152 |
+
with open(args.config, 'r') as stream:
|
| 153 |
+
config_data = yaml.load(stream, Loader=yaml.SafeLoader)
|
| 154 |
+
config_data: dict = resolve_config_variables(config_data)
|
| 155 |
+
|
| 156 |
+
print("Resolved paths:")
|
| 157 |
+
print(f"AIA dir: {config_data['data']['aia_dir']}")
|
| 158 |
+
print(f"SXR dir: {config_data['data']['sxr_dir']}")
|
| 159 |
+
print(f"Checkpoints dir: {config_data['data']['checkpoints_dir']}")
|
| 160 |
+
|
| 161 |
+
sxr_norm = np.load(config_data['data']['sxr_norm_path'])
|
| 162 |
+
wavelengths = config_data['wavelengths']
|
| 163 |
+
|
| 164 |
+
data_module = AIAGOESDataModule(
|
| 165 |
+
aia_train_dir=config_data['data']['aia_dir'] + "/train",
|
| 166 |
+
aia_val_dir=config_data['data']['aia_dir'] + "/val",
|
| 167 |
+
aia_test_dir=config_data['data']['aia_dir'] + "/test",
|
| 168 |
+
sxr_train_dir=config_data['data']['sxr_dir'] + "/train",
|
| 169 |
+
sxr_val_dir=config_data['data']['sxr_dir'] + "/val",
|
| 170 |
+
sxr_test_dir=config_data['data']['sxr_dir'] + "/test",
|
| 171 |
+
batch_size=config_data['batch_size'],
|
| 172 |
+
num_workers=min(8, os.cpu_count()),
|
| 173 |
+
sxr_norm=sxr_norm,
|
| 174 |
+
wavelengths=wavelengths,
|
| 175 |
+
)
|
| 176 |
+
data_module.setup()
|
| 177 |
+
|
| 178 |
+
wandb_logger = WandbLogger(
|
| 179 |
+
entity=config_data['wandb']['entity'],
|
| 180 |
+
project=config_data['wandb']['project'],
|
| 181 |
+
job_type=config_data['wandb']['job_type'],
|
| 182 |
+
tags=config_data['wandb']['tags'],
|
| 183 |
+
name=config_data['wandb']['run_name'],
|
| 184 |
+
notes=config_data['wandb']['notes'],
|
| 185 |
+
config=config_data,
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
# Callbacks
|
| 189 |
+
total_n_valid = len(data_module.val_ds)
|
| 190 |
+
plot_samples = [data_module.val_ds[i] for i in range(0, total_n_valid, max(1, total_n_valid // 4))]
|
| 191 |
+
sxr_plot_callback = ImagePredictionLogger_SXR(plot_samples, sxr_norm)
|
| 192 |
+
patch_size = config_data.get('vit_architecture', {}).get('patch_size', 16)
|
| 193 |
+
attention_callback = AttentionMapCallback(patch_size=patch_size, use_local_attention=True)
|
| 194 |
+
|
| 195 |
+
base_weights = get_base_weights(data_module, sxr_norm) if config_data.get('calculate_base_weights') else None
|
| 196 |
+
model = ViTLocal(model_kwargs=config_data['vit_architecture'], sxr_norm=sxr_norm, base_weights=base_weights)
|
| 197 |
+
|
| 198 |
+
checkpoint_callback = ModelCheckpoint(
|
| 199 |
+
dirpath=config_data['data']['checkpoints_dir'],
|
| 200 |
+
monitor='val_total_loss',
|
| 201 |
+
mode='min',
|
| 202 |
+
save_top_k=10,
|
| 203 |
+
filename=f"{config_data['wandb']['run_name']}-{{epoch:02d}}-{{val_total_loss:.4f}}",
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
accelerator, devices, strategy = resolve_devices(config_data.get('gpu_ids', -1))
|
| 207 |
+
|
| 208 |
+
trainer = Trainer(
|
| 209 |
+
default_root_dir=config_data['data']['checkpoints_dir'],
|
| 210 |
+
accelerator=accelerator,
|
| 211 |
+
devices=devices,
|
| 212 |
+
strategy=strategy,
|
| 213 |
+
max_epochs=config_data['epochs'],
|
| 214 |
+
callbacks=[sxr_plot_callback, attention_callback, checkpoint_callback],
|
| 215 |
+
logger=wandb_logger,
|
| 216 |
+
log_every_n_steps=10,
|
| 217 |
+
)
|
| 218 |
+
trainer.fit(model, data_module)
|
| 219 |
+
wandb.finish()
|
| 220 |
+
print(f"Training complete. Checkpoints saved to {config_data['data']['checkpoints_dir']}")
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
if __name__ == '__main__':
|
| 224 |
+
main()
|
training/train_config.yaml
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =============================================================================
|
| 2 |
+
# FOXES Training Configuration
|
| 3 |
+
# =============================================================================
|
| 4 |
+
# Used by train.py to train the ViTLocal model on paired AIA/SXR .npy data.
|
| 5 |
+
# Expects aia_dir/sxr_dir to each have train/val/test subfolders (that's what
|
| 6 |
+
# data/build_dataset.py produces).
|
| 7 |
+
#
|
| 8 |
+
# Usage:
|
| 9 |
+
# python training/train.py --config training/train_config.yaml
|
| 10 |
+
# =============================================================================
|
| 11 |
+
wavelengths: [94, 131, 171, 193, 211, 304, 335]
|
| 12 |
+
|
| 13 |
+
# GPU configuration:
|
| 14 |
+
# 0 (or any int) - that single GPU
|
| 15 |
+
# [0, 1] - those specific GPUs
|
| 16 |
+
# "all" - every available GPU
|
| 17 |
+
# -1 - CPU only
|
| 18 |
+
gpu_ids: -1
|
| 19 |
+
|
| 20 |
+
batch_size: 6
|
| 21 |
+
epochs: 150
|
| 22 |
+
|
| 23 |
+
# Compute inverse-frequency loss weights (quiet/C/M/X) from the training data
|
| 24 |
+
# before starting. Takes one extra pass over the training set.
|
| 25 |
+
calculate_base_weights: false
|
| 26 |
+
|
| 27 |
+
vit_architecture:
|
| 28 |
+
embed_dim: 256
|
| 29 |
+
num_channels: 7
|
| 30 |
+
num_classes: 1
|
| 31 |
+
patch_size: 8
|
| 32 |
+
num_patches: 4096
|
| 33 |
+
hidden_dim: 1024
|
| 34 |
+
num_heads: 8
|
| 35 |
+
num_layers: 8
|
| 36 |
+
dropout: 0.1
|
| 37 |
+
learning_rate: 0.0001
|
| 38 |
+
# Self-attention masking - one of:
|
| 39 |
+
# inverted - the released model's behavior: each patch attends to DISTANT
|
| 40 |
+
# patches (the flipped local-attention mask it was trained with)
|
| 41 |
+
# local - true local attention: each patch attends to its neighborhood
|
| 42 |
+
# none - standard full/global attention (no masking)
|
| 43 |
+
mask_mode: inverted
|
| 44 |
+
local_window: 9 # neighbourhood side length (patches), used by inverted/local
|
| 45 |
+
|
| 46 |
+
data:
|
| 47 |
+
aia_dir: "/Volumes/T9/testing_foxes/AIA_processed"
|
| 48 |
+
sxr_dir: "/Volumes/T9/testing_foxes/SXR_processed"
|
| 49 |
+
sxr_norm_path: "/Volumes/T9/testing_foxes/normalized_sxr.npy"
|
| 50 |
+
checkpoints_dir: "/Volumes/T9/chkpt/" # update to actual checkpoint
|
| 51 |
+
|
| 52 |
+
wandb:
|
| 53 |
+
entity: "ggoodwin5-georgia-state-university" # your W&B username or team name
|
| 54 |
+
project: FOXES
|
| 55 |
+
job_type: training
|
| 56 |
+
tags:
|
| 57 |
+
- aia
|
| 58 |
+
- sxr
|
| 59 |
+
- regression
|
| 60 |
+
run_name: run_1
|
| 61 |
+
notes: AIA to SXR translation
|