checkpoints, kit and exporter
Browse files- .gitattributes +5 -0
- E05-FT-S01/best_inference_ckpt.tar +3 -0
- E11-FT-S01/best_inference_ckpt.tar +3 -0
- README.md +58 -0
- kit/configs/atm-aerofrz.yaml +39 -0
- kit/configs/atm-co2x0.8.yaml +34 -0
- kit/configs/atm-co2x1.25.yaml +34 -0
- kit/configs/atm-ctrl.yaml +31 -0
- kit/configs/atm-p4K.yaml +34 -0
- kit/export_fme_latents.py +533 -0
- kit/forcing-aerofrz/aero.2015-01.nc +3 -0
- kit/forcing-aerofrz/aero.2015-02.nc +3 -0
- kit/forcing/v3.LR.historical_0101.aigo.eam.h0.2015-01.nc +3 -0
- kit/forcing/v3.LR.historical_0101.aigo.eam.h0.2015-02.nc +3 -0
- kit/ic/E05-FT-S01.restart.2015-01-03T12.nc +3 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
kit/forcing/v3.LR.historical_0101.aigo.eam.h0.2015-01.nc filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
kit/forcing/v3.LR.historical_0101.aigo.eam.h0.2015-02.nc filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
kit/forcing-aerofrz/aero.2015-01.nc filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
kit/forcing-aerofrz/aero.2015-02.nc filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
kit/ic/E05-FT-S01.restart.2015-01-03T12.nc filter=lfs diff=lfs merge=lfs -text
|
E05-FT-S01/best_inference_ckpt.tar
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:086629bcecc824a6a4c21bb08051a972f3d34b9ed0fcc002587f86bfbadcb3d2
|
| 3 |
+
size 1824999519
|
E11-FT-S01/best_inference_ckpt.tar
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f50c886354f30076309b53fbb41e6aef33202a0aa32a774de45e08ae36cf9a3e
|
| 3 |
+
size 341473131
|
README.md
CHANGED
|
@@ -1,3 +1,61 @@
|
|
| 1 |
---
|
| 2 |
license: bsd-3-clause
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
license: bsd-3-clause
|
| 3 |
+
tags:
|
| 4 |
+
- climate
|
| 5 |
+
- emulator
|
| 6 |
+
- e3sm
|
| 7 |
+
- ace
|
| 8 |
+
- samudra
|
| 9 |
---
|
| 10 |
+
|
| 11 |
+
# E3SMv3 emulator checkpoints for the E3SM AI group hackathon, 25 September 2026
|
| 12 |
+
|
| 13 |
+
> **Not fit for forcing or scenario experiments.** Both checkpoints reproduce the E3SMv3 historical run well
|
| 14 |
+
> when the other half of the climate system is handed to them, but neither responds to a change in forcing
|
| 15 |
+
> physically: the atmosphere uses CO₂ as a clock, and the ocean reads its answer off its upward-longwave input.
|
| 16 |
+
> Use them to study how an emulator represents the climate, not what the climate does. Results from them show
|
| 17 |
+
> how the model *represents* forcing.
|
| 18 |
+
|
| 19 |
+
| Folder | Checkpoint | Fit for | Not fit for |
|
| 20 |
+
| --- | --- | --- | --- |
|
| 21 |
+
| `E05-FT-S01/` | `E05-FT.aug26.atm.A3_B16_C1_L0_O5_W0_X0.S01`, atmosphere, noise-conditioned SFNO (8 blocks × 384 channels), 1°, 6-hourly, SST prescribed | Historical runs with prescribed SST; looking inside the network | Changed CO₂ or SST, any forcing experiment; precipitation extremes (about 2× too frequent) |
|
| 22 |
+
| `E11-FT-S01/` | `E11-FT.aug26.ocn.A0_B16_C0_L0_O5_W0_X0.S01`, ocean, Samudra U-Net, 1°, 5-daily, surface fluxes prescribed | Replaying the historical run from reference fluxes; looking inside the network | Any change to the fluxes; ENSO studies; subsurface heat uptake |
|
| 23 |
+
|
| 24 |
+
These are the checkpoints the latents in
|
| 25 |
+
[E3SM-Project/aigs-hack-sep26-latents](https://huggingface.co/datasets/E3SM-Project/aigs-hack-sep26-latents)
|
| 26 |
+
were recorded from. E05-FT S01 is not the shipped atmosphere (S03): it is the one the probes behind the
|
| 27 |
+
"not fit for" column used.
|
| 28 |
+
|
| 29 |
+
## Run an experiment yourself
|
| 30 |
+
|
| 31 |
+
The `kit/` folder holds what a 2015 atmosphere run needs besides the checkpoint: the initial condition
|
| 32 |
+
(2015-01-03 12:00), two months of forcing (the inputs only), and configs for the control and each intervention.
|
| 33 |
+
One run uses about 4 GB of GPU memory.
|
| 34 |
+
|
| 35 |
+
```console
|
| 36 |
+
$ pip install "fme @ git+https://github.com/E3SM-Project/ace@e3sm/exps/hist-v2026.8.0"
|
| 37 |
+
$ hf download E3SM-Project/aigs-hack-sep26-models --local-dir aigs-models && cd aigs-models
|
| 38 |
+
$ python -m fme.ace.inference kit/configs/atm-co2x1.25.yaml
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
| Config | Intervention |
|
| 42 |
+
| --- | --- |
|
| 43 |
+
| `atm-ctrl.yaml` | none |
|
| 44 |
+
| `atm-co2x1.25.yaml`, `atm-co2x0.8.yaml` | `overwrite.multiply_scalar.global_mean_co2` |
|
| 45 |
+
| `atm-p4K.yaml` | `overwrite.add_scalar.TS: 4.0` |
|
| 46 |
+
| `atm-aerofrz.yaml` | aerosol diagnostics swapped for their 1965 values (`kit/forcing-aerofrz/`) |
|
| 47 |
+
|
| 48 |
+
Every config sets `seed: 0`. Keep it: the model is stochastic, and a shared seed is what makes two runs
|
| 49 |
+
comparable node for node. Change the intervention by editing the `overwrite` block; make it longer with
|
| 50 |
+
`n_forward_steps` (the forcing covers January and February 2015).
|
| 51 |
+
|
| 52 |
+
To record the latents as well, and to steer a layer along a feature, use the exporter in the kit
|
| 53 |
+
(`kit/export_fme_latents.py`; it needs [xaig](https://pypi.org/project/xaig/) 0.2 or later in the same
|
| 54 |
+
environment):
|
| 55 |
+
|
| 56 |
+
```console
|
| 57 |
+
$ python kit/export_fme_latents.py kit/configs/atm-ctrl.yaml --out my-run --steps 20 \
|
| 58 |
+
--steer 0:sae_L00.npz:142:3 # push SAE feature 142 of layer 0 by +3 at every node
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
It reproduces the published latents bit for bit.
|
kit/configs/atm-aerofrz.yaml
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_dir: output/atm-aerofrz
|
| 2 |
+
n_forward_steps: 20
|
| 3 |
+
forward_steps_in_memory: 20
|
| 4 |
+
checkpoint_path: E05-FT-S01/best_inference_ckpt.tar
|
| 5 |
+
logging:
|
| 6 |
+
log_to_screen: true
|
| 7 |
+
log_to_wandb: false
|
| 8 |
+
log_to_file: true
|
| 9 |
+
initial_condition:
|
| 10 |
+
path: kit/ic/E05-FT-S01.restart.2015-01-03T12.nc
|
| 11 |
+
engine: netcdf4
|
| 12 |
+
forcing_loader:
|
| 13 |
+
dataset:
|
| 14 |
+
merge:
|
| 15 |
+
- data_path: kit/forcing
|
| 16 |
+
file_pattern: v3.LR.historical_0101.aigo.eam.h0.*.nc
|
| 17 |
+
rename:
|
| 18 |
+
co2vmr: global_mean_co2
|
| 19 |
+
aerindexall: aerindexall_orig
|
| 20 |
+
colccn.3: colccn3_orig
|
| 21 |
+
reference_pressure_name: P0
|
| 22 |
+
- data_path: kit/forcing-aerofrz
|
| 23 |
+
file_pattern: aero.*.nc
|
| 24 |
+
rename:
|
| 25 |
+
aerindexall_F: aerindexall
|
| 26 |
+
colccn.3_F: colccn.3
|
| 27 |
+
num_data_workers: 2
|
| 28 |
+
data_writer:
|
| 29 |
+
save_prediction_files: true
|
| 30 |
+
save_monthly_files: false
|
| 31 |
+
names:
|
| 32 |
+
- PS
|
| 33 |
+
- TS
|
| 34 |
+
- Tat2m
|
| 35 |
+
- surface_precipitation_rate
|
| 36 |
+
- FLUT
|
| 37 |
+
- FSUTOA
|
| 38 |
+
- lwp
|
| 39 |
+
seed: 0
|
kit/configs/atm-co2x0.8.yaml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_dir: output/atm-co2x0.8
|
| 2 |
+
n_forward_steps: 20
|
| 3 |
+
forward_steps_in_memory: 20
|
| 4 |
+
checkpoint_path: E05-FT-S01/best_inference_ckpt.tar
|
| 5 |
+
logging:
|
| 6 |
+
log_to_screen: true
|
| 7 |
+
log_to_wandb: false
|
| 8 |
+
log_to_file: true
|
| 9 |
+
initial_condition:
|
| 10 |
+
path: kit/ic/E05-FT-S01.restart.2015-01-03T12.nc
|
| 11 |
+
engine: netcdf4
|
| 12 |
+
forcing_loader:
|
| 13 |
+
dataset:
|
| 14 |
+
data_path: kit/forcing
|
| 15 |
+
file_pattern: v3.LR.historical_0101.aigo.eam.h0.*.nc
|
| 16 |
+
rename:
|
| 17 |
+
co2vmr: global_mean_co2
|
| 18 |
+
reference_pressure_name: P0
|
| 19 |
+
overwrite:
|
| 20 |
+
multiply_scalar:
|
| 21 |
+
global_mean_co2: 0.8
|
| 22 |
+
num_data_workers: 2
|
| 23 |
+
data_writer:
|
| 24 |
+
save_prediction_files: true
|
| 25 |
+
save_monthly_files: false
|
| 26 |
+
names:
|
| 27 |
+
- PS
|
| 28 |
+
- TS
|
| 29 |
+
- Tat2m
|
| 30 |
+
- surface_precipitation_rate
|
| 31 |
+
- FLUT
|
| 32 |
+
- FSUTOA
|
| 33 |
+
- lwp
|
| 34 |
+
seed: 0
|
kit/configs/atm-co2x1.25.yaml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_dir: output/atm-co2x1.25
|
| 2 |
+
n_forward_steps: 20
|
| 3 |
+
forward_steps_in_memory: 20
|
| 4 |
+
checkpoint_path: E05-FT-S01/best_inference_ckpt.tar
|
| 5 |
+
logging:
|
| 6 |
+
log_to_screen: true
|
| 7 |
+
log_to_wandb: false
|
| 8 |
+
log_to_file: true
|
| 9 |
+
initial_condition:
|
| 10 |
+
path: kit/ic/E05-FT-S01.restart.2015-01-03T12.nc
|
| 11 |
+
engine: netcdf4
|
| 12 |
+
forcing_loader:
|
| 13 |
+
dataset:
|
| 14 |
+
data_path: kit/forcing
|
| 15 |
+
file_pattern: v3.LR.historical_0101.aigo.eam.h0.*.nc
|
| 16 |
+
rename:
|
| 17 |
+
co2vmr: global_mean_co2
|
| 18 |
+
reference_pressure_name: P0
|
| 19 |
+
overwrite:
|
| 20 |
+
multiply_scalar:
|
| 21 |
+
global_mean_co2: 1.25
|
| 22 |
+
num_data_workers: 2
|
| 23 |
+
data_writer:
|
| 24 |
+
save_prediction_files: true
|
| 25 |
+
save_monthly_files: false
|
| 26 |
+
names:
|
| 27 |
+
- PS
|
| 28 |
+
- TS
|
| 29 |
+
- Tat2m
|
| 30 |
+
- surface_precipitation_rate
|
| 31 |
+
- FLUT
|
| 32 |
+
- FSUTOA
|
| 33 |
+
- lwp
|
| 34 |
+
seed: 0
|
kit/configs/atm-ctrl.yaml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_dir: output/atm-ctrl
|
| 2 |
+
n_forward_steps: 20
|
| 3 |
+
forward_steps_in_memory: 20
|
| 4 |
+
checkpoint_path: E05-FT-S01/best_inference_ckpt.tar
|
| 5 |
+
logging:
|
| 6 |
+
log_to_screen: true
|
| 7 |
+
log_to_wandb: false
|
| 8 |
+
log_to_file: true
|
| 9 |
+
initial_condition:
|
| 10 |
+
path: kit/ic/E05-FT-S01.restart.2015-01-03T12.nc
|
| 11 |
+
engine: netcdf4
|
| 12 |
+
forcing_loader:
|
| 13 |
+
dataset:
|
| 14 |
+
data_path: kit/forcing
|
| 15 |
+
file_pattern: v3.LR.historical_0101.aigo.eam.h0.*.nc
|
| 16 |
+
rename:
|
| 17 |
+
co2vmr: global_mean_co2
|
| 18 |
+
reference_pressure_name: P0
|
| 19 |
+
num_data_workers: 2
|
| 20 |
+
data_writer:
|
| 21 |
+
save_prediction_files: true
|
| 22 |
+
save_monthly_files: false
|
| 23 |
+
names:
|
| 24 |
+
- PS
|
| 25 |
+
- TS
|
| 26 |
+
- Tat2m
|
| 27 |
+
- surface_precipitation_rate
|
| 28 |
+
- FLUT
|
| 29 |
+
- FSUTOA
|
| 30 |
+
- lwp
|
| 31 |
+
seed: 0
|
kit/configs/atm-p4K.yaml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
experiment_dir: output/atm-p4K
|
| 2 |
+
n_forward_steps: 20
|
| 3 |
+
forward_steps_in_memory: 20
|
| 4 |
+
checkpoint_path: E05-FT-S01/best_inference_ckpt.tar
|
| 5 |
+
logging:
|
| 6 |
+
log_to_screen: true
|
| 7 |
+
log_to_wandb: false
|
| 8 |
+
log_to_file: true
|
| 9 |
+
initial_condition:
|
| 10 |
+
path: kit/ic/E05-FT-S01.restart.2015-01-03T12.nc
|
| 11 |
+
engine: netcdf4
|
| 12 |
+
forcing_loader:
|
| 13 |
+
dataset:
|
| 14 |
+
data_path: kit/forcing
|
| 15 |
+
file_pattern: v3.LR.historical_0101.aigo.eam.h0.*.nc
|
| 16 |
+
rename:
|
| 17 |
+
co2vmr: global_mean_co2
|
| 18 |
+
reference_pressure_name: P0
|
| 19 |
+
overwrite:
|
| 20 |
+
add_scalar:
|
| 21 |
+
TS: 4.0
|
| 22 |
+
num_data_workers: 2
|
| 23 |
+
data_writer:
|
| 24 |
+
save_prediction_files: true
|
| 25 |
+
save_monthly_files: false
|
| 26 |
+
names:
|
| 27 |
+
- PS
|
| 28 |
+
- TS
|
| 29 |
+
- Tat2m
|
| 30 |
+
- surface_precipitation_rate
|
| 31 |
+
- FLUT
|
| 32 |
+
- FSUTOA
|
| 33 |
+
- lwp
|
| 34 |
+
seed: 0
|
kit/export_fme_latents.py
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Record an fme (ACE / Samudra) run's latents as an xaig latent archive.
|
| 2 |
+
|
| 3 |
+
Runs ``fme.ace.inference`` (a YAML with ``initial_condition`` + ``forcing_loader``) or
|
| 4 |
+
``fme.ace.inference.evaluator`` (a YAML with ``loader``) with forward hooks on the
|
| 5 |
+
network inside the stepper.
|
| 6 |
+
|
| 7 |
+
- SFNO (atmosphere): layer 0 is the input to the first block (encoder output plus
|
| 8 |
+
position embedding); layer L (1..num_layers) is the output of block L.
|
| 9 |
+
- Samudra U-Net (ocean): only the full-resolution levels, which xaig can map --
|
| 10 |
+
layer 0 is the first encoder block's output, layer 1 the last decoder block's.
|
| 11 |
+
|
| 12 |
+
Optionally *writes* a layer as well (steering): add ``alpha`` times a direction to
|
| 13 |
+
layer L at every step (or a window of steps). The direction is one channel, or one
|
| 14 |
+
feature of an xaig basis file (a Dictionary's unit direction scaled by its
|
| 15 |
+
``input_scale``, so ``alpha`` is in activation units; a PCA component in layer units).
|
| 16 |
+
|
| 17 |
+
Network inputs are kept too, denormalized, exactly as the network saw them (so an
|
| 18 |
+
intervention on CO2 or a frozen flux shows up in ``reference.nc``).
|
| 19 |
+
|
| 20 |
+
Needs the model's environment (torch + fme) and xaig's archive writer, which is
|
| 21 |
+
pure numpy: point ``--xaig-src`` at a checkout's ``src`` when xaig is not installed.
|
| 22 |
+
|
| 23 |
+
python export_fme_latents.py atm-ctrl.yaml --out archives/atm-E05S01-ctrl --steps 20
|
| 24 |
+
|
| 25 |
+
The archive's ``experiment`` block records the checkpoint, the source config, the
|
| 26 |
+
seed, the intervention (anything under ``--experiment``), the steering, and the
|
| 27 |
+
recorded steps.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
from __future__ import annotations
|
| 31 |
+
|
| 32 |
+
import argparse
|
| 33 |
+
import datetime as dt
|
| 34 |
+
import json
|
| 35 |
+
import logging
|
| 36 |
+
import shutil
|
| 37 |
+
import sys
|
| 38 |
+
from pathlib import Path
|
| 39 |
+
|
| 40 |
+
import numpy as np
|
| 41 |
+
import torch
|
| 42 |
+
import yaml
|
| 43 |
+
|
| 44 |
+
DEFAULT_FIELDS = [
|
| 45 |
+
"PS", "TS", "Tat2m", "Qat2m", "Uat10m", "Vat10m",
|
| 46 |
+
"surface_precipitation_rate", "FLUT", "FSUTOA", "FLUS", "FLDS", "FSNS",
|
| 47 |
+
"LHFLX", "SHFLX", "TAUX", "TAUY", "lwp", "lcc",
|
| 48 |
+
"U_1", "V_1", "T_1", "U_3", "V_3", "U_5", "V_5", "T_5", "STW_5", "U_7", "V_7", "STW_7",
|
| 49 |
+
] # fmt: skip
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
DEFAULT_INPUTS = [
|
| 53 |
+
"SOLIN", "global_mean_co2", "aerindexall", "colccn.3", "OCNFRAC", "ICEFRAC", "LANDFRAC",
|
| 54 |
+
"PHIS", "TS", "FLUS", "FLDS", "FSNS", "LHFLX", "SHFLX", "TAUX", "TAUY", "sea_surface_fraction",
|
| 55 |
+
] # fmt: skip
|
| 56 |
+
OCEAN_FIELDS = [
|
| 57 |
+
"sst", "ssh", "ocean_sea_ice_fraction", "iceVolumeTotal", "temperatureCoarsened_0",
|
| 58 |
+
"temperatureCoarsened_5", "temperatureCoarsened_10", "salinityCoarsened_0",
|
| 59 |
+
"velocityZonalCoarsened_0", "velocityMeridionalCoarsened_0",
|
| 60 |
+
] # fmt: skip
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def find_step_with_packer(stepper):
|
| 64 |
+
"""The step object that packs named inputs into the network's channels."""
|
| 65 |
+
candidates = [stepper, getattr(stepper, "_step_obj", None)]
|
| 66 |
+
for obj in list(candidates):
|
| 67 |
+
if obj is not None:
|
| 68 |
+
candidates.extend(v for v in vars(obj).values() if hasattr(v, "in_packer"))
|
| 69 |
+
for obj in candidates:
|
| 70 |
+
if obj is not None and hasattr(obj, "in_packer") and hasattr(obj, "_normalizer"):
|
| 71 |
+
return obj
|
| 72 |
+
raise RuntimeError("could not find the step that packs the network inputs")
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class SkipRescale:
|
| 76 |
+
"""A pre-hook on an SFNO's decoder that rescales one input in the big-skip half only.
|
| 77 |
+
|
| 78 |
+
The decoder reads ``cat(block output, normalized inputs)``; this changes channel
|
| 79 |
+
``embed_dim + index(name)`` from ``(x - mean) / std`` to ``(factor * x - mean) / std``,
|
| 80 |
+
so the skip sees the input scaled while every block saw it as it was."""
|
| 81 |
+
|
| 82 |
+
def __init__(self, stepper, net, name, factor):
|
| 83 |
+
if not getattr(net, "big_skip", False):
|
| 84 |
+
raise SystemExit("--skip-rescale needs an SFNO with a big skip")
|
| 85 |
+
step = find_step_with_packer(stepper)
|
| 86 |
+
packed = list(step.in_packer.names)
|
| 87 |
+
if name not in packed:
|
| 88 |
+
raise SystemExit(f"{name} is not a network input; inputs are {packed}")
|
| 89 |
+
self.channel = net.embed_dim + packed.index(name)
|
| 90 |
+
self.mean = float(step._normalizer.means[name])
|
| 91 |
+
self.std = float(step._normalizer.stds[name])
|
| 92 |
+
self.factor = factor
|
| 93 |
+
net.decoder.register_forward_pre_hook(self.hook)
|
| 94 |
+
|
| 95 |
+
def hook(self, module, args):
|
| 96 |
+
x = args[0].clone()
|
| 97 |
+
physical = x[:, self.channel] * self.std + self.mean
|
| 98 |
+
x[:, self.channel] = (self.factor * physical - self.mean) / self.std
|
| 99 |
+
return (x, *args[1:])
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class InputCapture:
|
| 103 |
+
"""A pre-hook on the network that keeps chosen inputs, denormalized, at recorded steps."""
|
| 104 |
+
|
| 105 |
+
def __init__(self, stepper, net_outer, names, recorder, n_field_times):
|
| 106 |
+
step = find_step_with_packer(stepper)
|
| 107 |
+
packed = list(step.in_packer.names)
|
| 108 |
+
missing = [n for n in names if n not in packed]
|
| 109 |
+
if missing:
|
| 110 |
+
logging.warning("inputs not among the network's inputs, skipped: %s", missing)
|
| 111 |
+
self.names = [n for n in names if n in packed]
|
| 112 |
+
self.index = {n: packed.index(n) for n in self.names}
|
| 113 |
+
self.mean = {n: float(step._normalizer.means[n]) for n in self.names}
|
| 114 |
+
self.std = {n: float(step._normalizer.stds[n]) for n in self.names}
|
| 115 |
+
self.rec = recorder
|
| 116 |
+
self.n_field_times = n_field_times
|
| 117 |
+
self.values = {}
|
| 118 |
+
net_outer.register_forward_pre_hook(self.hook) # after the recorder's counter
|
| 119 |
+
|
| 120 |
+
def hook(self, module, args):
|
| 121 |
+
k = self.rec.call
|
| 122 |
+
x = args[0]
|
| 123 |
+
if k >= self.n_field_times or x.shape[0] != 1:
|
| 124 |
+
return
|
| 125 |
+
for n in self.names:
|
| 126 |
+
v = x[0, self.index[n]].detach().float() * self.std[n] + self.mean[n]
|
| 127 |
+
v = v.reshape(-1).cpu().numpy()
|
| 128 |
+
if n not in self.values:
|
| 129 |
+
self.values[n] = np.full((self.n_field_times, v.size), np.nan, dtype=np.float32)
|
| 130 |
+
self.values[n][k] = v
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def parse_args(argv=None):
|
| 134 |
+
p = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
| 135 |
+
p.add_argument("config", help="fme inference YAML (initial_condition + forcing_loader)")
|
| 136 |
+
p.add_argument("--out", required=True, help="archive directory to write")
|
| 137 |
+
p.add_argument("--steps", type=int, required=True, help="forward steps to run")
|
| 138 |
+
p.add_argument("--every", type=int, default=1, help="record every Nth step")
|
| 139 |
+
p.add_argument("--layers", default="all", help="'all' or comma list, e.g. 0,4,8")
|
| 140 |
+
p.add_argument(
|
| 141 |
+
"--fields",
|
| 142 |
+
default=None,
|
| 143 |
+
help="output variables kept in reference.nc (default: a set per component)",
|
| 144 |
+
)
|
| 145 |
+
p.add_argument(
|
| 146 |
+
"--inputs",
|
| 147 |
+
default=",".join(DEFAULT_INPUTS),
|
| 148 |
+
help="network inputs kept in reference.nc, as the network saw them "
|
| 149 |
+
"(interventions included); named <name>_input when an output shares the name",
|
| 150 |
+
)
|
| 151 |
+
p.add_argument("--dtype", default="float16")
|
| 152 |
+
p.add_argument(
|
| 153 |
+
"--seed",
|
| 154 |
+
type=int,
|
| 155 |
+
default=0,
|
| 156 |
+
help="rollout seed; the noise-conditioned SFNO is stochastic, so runs meant "
|
| 157 |
+
"to be compared node by node must share it",
|
| 158 |
+
)
|
| 159 |
+
p.add_argument("--steer", help="L:BASIS.npz:FEATURE:ALPHA add alpha x a basis direction")
|
| 160 |
+
p.add_argument("--steer-channel", help="L:CHANNEL:AMOUNT add a constant to one channel")
|
| 161 |
+
p.add_argument(
|
| 162 |
+
"--steer-vector", help="L:VECTOR.npy:ALPHA add alpha x a saved (channels,) vector"
|
| 163 |
+
)
|
| 164 |
+
p.add_argument("--steer-window", default=None, help="FIRST:LAST forward steps to steer")
|
| 165 |
+
p.add_argument(
|
| 166 |
+
"--skip-rescale",
|
| 167 |
+
help="NAME:FACTOR SFNO with a big skip only: scale one input, in physical units, in the "
|
| 168 |
+
"copy of the inputs the skip hands the decoder, leaving what the blocks see alone",
|
| 169 |
+
)
|
| 170 |
+
p.add_argument("--model-name", default=None, help="manifest 'model' (default: run id)")
|
| 171 |
+
p.add_argument("--experiment", default="{}", help="JSON merged into the manifest's experiment")
|
| 172 |
+
p.add_argument("--xaig-src", default=None, help="path to an xaig checkout's src/")
|
| 173 |
+
p.add_argument("--keep-run", action="store_true", help="keep the fme experiment_dir")
|
| 174 |
+
return p.parse_args(argv)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def find_network(stepper):
|
| 178 |
+
"""(kind, outermost module, network, taps): taps are (module, 'pre'|'post', label)."""
|
| 179 |
+
from fme.core.models.conditional_sfno.sfnonet import SphericalFourierNeuralOperatorNet
|
| 180 |
+
|
| 181 |
+
modules = list(stepper.modules.modules())
|
| 182 |
+
sfno = [m for m in modules if isinstance(m, SphericalFourierNeuralOperatorNet)]
|
| 183 |
+
if len(sfno) == 1:
|
| 184 |
+
net = sfno[0]
|
| 185 |
+
from fme.ace.registry.stochastic_sfno import NoiseConditionedModel
|
| 186 |
+
|
| 187 |
+
outer = [m for m in modules if isinstance(m, NoiseConditionedModel)]
|
| 188 |
+
taps = [(net.blocks[0], "pre", "encoder output (input to block 1)")]
|
| 189 |
+
taps += [(b, "post", f"SFNO block {i} output") for i, b in enumerate(net.blocks, 1)]
|
| 190 |
+
return "sfno", (outer[0] if outer else net), net, taps
|
| 191 |
+
from fme.ace.models.ocean.m2lines.samudra import Samudra
|
| 192 |
+
|
| 193 |
+
samudra = [m for m in modules if isinstance(m, Samudra)]
|
| 194 |
+
if len(samudra) == 1:
|
| 195 |
+
net = samudra[0]
|
| 196 |
+
taps = [
|
| 197 |
+
(
|
| 198 |
+
net.layers[0],
|
| 199 |
+
"post",
|
| 200 |
+
f"encoder level 1 output ({net.ch_width[0]} ch, full resolution)",
|
| 201 |
+
),
|
| 202 |
+
(
|
| 203 |
+
net.layers[-2],
|
| 204 |
+
"post",
|
| 205 |
+
f"last decoder block output ({net.ch_width[0]} ch, full resolution)",
|
| 206 |
+
),
|
| 207 |
+
]
|
| 208 |
+
return "samudra", net, net, taps
|
| 209 |
+
raise RuntimeError("found neither one SFNO nor one Samudra in the stepper")
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
class Recorder:
|
| 213 |
+
"""Forward hooks that record chosen layers at chosen steps and optionally steer one."""
|
| 214 |
+
|
| 215 |
+
def __init__(
|
| 216 |
+
self, outer, taps, layers, steps, every, staging: Path, dtype, steer=None, window=None
|
| 217 |
+
):
|
| 218 |
+
self.outer = outer
|
| 219 |
+
self.taps = taps
|
| 220 |
+
self.n_layers = len(taps)
|
| 221 |
+
self.layers = (
|
| 222 |
+
list(range(self.n_layers)) if layers == "all" else [int(x) for x in layers.split(",")]
|
| 223 |
+
)
|
| 224 |
+
for layer in self.layers:
|
| 225 |
+
if not 0 <= layer < self.n_layers:
|
| 226 |
+
raise ValueError(f"layer {layer} not in 0..{self.n_layers - 1}")
|
| 227 |
+
self.recorded_steps = list(range(0, steps, every))
|
| 228 |
+
self.slot = {s: i for i, s in enumerate(self.recorded_steps)}
|
| 229 |
+
self.staging = staging
|
| 230 |
+
self.dtype = dtype
|
| 231 |
+
self.steer = steer # (layer, torch vector (C,))
|
| 232 |
+
self.window = window
|
| 233 |
+
self.arrays = {}
|
| 234 |
+
self.call = -1
|
| 235 |
+
self.shape = None
|
| 236 |
+
|
| 237 |
+
def _array(self, layer, x):
|
| 238 |
+
if layer not in self.arrays:
|
| 239 |
+
n_ch, h, w = x.shape[1:]
|
| 240 |
+
self.shape = (h, w)
|
| 241 |
+
path = self.staging / f"raw_{layer:02d}.npy"
|
| 242 |
+
self.arrays[layer] = np.lib.format.open_memmap(
|
| 243 |
+
path, mode="w+", dtype=self.dtype, shape=(len(self.recorded_steps), h * w, n_ch)
|
| 244 |
+
)
|
| 245 |
+
return self.arrays[layer]
|
| 246 |
+
|
| 247 |
+
def _steer(self, layer, x):
|
| 248 |
+
if self.steer is None or self.steer[0] != layer:
|
| 249 |
+
return x
|
| 250 |
+
if self.window is not None and not (self.window[0] <= self.call <= self.window[1]):
|
| 251 |
+
return x
|
| 252 |
+
vec = self.steer[1].to(device=x.device, dtype=x.dtype)
|
| 253 |
+
if vec.numel() < x.shape[1]: # a one-channel push, given only up to its channel
|
| 254 |
+
vec = torch.nn.functional.pad(vec, (0, x.shape[1] - vec.numel()))
|
| 255 |
+
if vec.numel() != x.shape[1]:
|
| 256 |
+
raise RuntimeError(
|
| 257 |
+
f"steering direction has {vec.numel()} channels, layer {layer} has {x.shape[1]}"
|
| 258 |
+
)
|
| 259 |
+
return x + vec.view(1, -1, 1, 1)
|
| 260 |
+
|
| 261 |
+
def _record(self, layer, x):
|
| 262 |
+
if layer in self.layers and self.call in self.slot:
|
| 263 |
+
if x.shape[0] != 1:
|
| 264 |
+
raise RuntimeError(f"recording needs batch 1, got {x.shape[0]}")
|
| 265 |
+
flat = x[0].detach().float().reshape(x.shape[1], -1).T # (nodes, channels), lat-major
|
| 266 |
+
self._array(layer, x)[self.slot[self.call]] = flat.cpu().numpy().astype(self.dtype)
|
| 267 |
+
|
| 268 |
+
def attach(self):
|
| 269 |
+
def count(module, args):
|
| 270 |
+
self.call += 1
|
| 271 |
+
|
| 272 |
+
self.outer.register_forward_pre_hook(count, prepend=True)
|
| 273 |
+
for layer, (module, kind, _) in enumerate(self.taps):
|
| 274 |
+
if kind == "pre":
|
| 275 |
+
|
| 276 |
+
def pre(module, args, layer=layer):
|
| 277 |
+
x = self._steer(layer, args[0])
|
| 278 |
+
self._record(layer, x)
|
| 279 |
+
return (x, *args[1:])
|
| 280 |
+
|
| 281 |
+
module.register_forward_pre_hook(pre)
|
| 282 |
+
else:
|
| 283 |
+
|
| 284 |
+
def post(module, args, out, layer=layer):
|
| 285 |
+
out = self._steer(layer, out)
|
| 286 |
+
self._record(layer, out)
|
| 287 |
+
return out
|
| 288 |
+
|
| 289 |
+
module.register_forward_hook(post)
|
| 290 |
+
|
| 291 |
+
def labels(self):
|
| 292 |
+
return [label for _, _, label in self.taps]
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def steering_vector(args):
|
| 296 |
+
import torch
|
| 297 |
+
|
| 298 |
+
if sum(bool(x) for x in (args.steer, args.steer_channel, args.steer_vector)) > 1:
|
| 299 |
+
raise SystemExit("give one of --steer, --steer-channel, --steer-vector")
|
| 300 |
+
if args.steer_vector:
|
| 301 |
+
layer, path, alpha = args.steer_vector.split(":")
|
| 302 |
+
vec = float(alpha) * np.load(path).astype(np.float64).ravel()
|
| 303 |
+
info = {"layer": int(layer), "vector": Path(path).name, "by": float(alpha)}
|
| 304 |
+
return int(layer), torch.as_tensor(vec, dtype=torch.float32), info
|
| 305 |
+
if args.steer_channel:
|
| 306 |
+
layer, ch, amount = args.steer_channel.split(":")
|
| 307 |
+
vec = torch.zeros(int(ch) + 1) # padded to the layer's width at first use
|
| 308 |
+
vec[int(ch)] = float(amount)
|
| 309 |
+
return int(layer), vec, {"layer": int(layer), "channel": int(ch), "by": float(amount)}
|
| 310 |
+
if args.steer:
|
| 311 |
+
from xaig.daig.latent import load_basis
|
| 312 |
+
|
| 313 |
+
layer, path, feature, alpha = args.steer.split(":")
|
| 314 |
+
basis = load_basis(path)
|
| 315 |
+
feature, alpha = int(feature), float(alpha)
|
| 316 |
+
# a Dictionary's activation a adds a * input_scale of its unit direction to the
|
| 317 |
+
# layer; a PCA component is a unit direction in the layer's own units
|
| 318 |
+
scale = float(getattr(basis, "input_scale", 1.0))
|
| 319 |
+
vec = alpha * scale * np.asarray(basis.directions()[feature], dtype=np.float64)
|
| 320 |
+
info = {
|
| 321 |
+
"layer": int(layer),
|
| 322 |
+
"basis": Path(path).name,
|
| 323 |
+
"kind": basis.kind,
|
| 324 |
+
"feature": feature,
|
| 325 |
+
"by": alpha,
|
| 326 |
+
"input_scale": scale,
|
| 327 |
+
}
|
| 328 |
+
return int(layer), torch.as_tensor(vec, dtype=torch.float32), info
|
| 329 |
+
return None, None, None
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def time_labels(ds):
|
| 333 |
+
"""Valid-time labels (ISO, 4-digit year) of the prediction file, initial condition first."""
|
| 334 |
+
t = ds["valid_time"].values if "valid_time" in ds else ds["time"].values
|
| 335 |
+
t = np.asarray(t).reshape(-1)
|
| 336 |
+
return [x.strftime("%Y-%m-%dT%H:%M:%S") if hasattr(x, "strftime") else str(x)[:19] for x in t]
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
def main(argv=None):
|
| 340 |
+
args = parse_args(argv)
|
| 341 |
+
if args.xaig_src:
|
| 342 |
+
sys.path.insert(0, args.xaig_src)
|
| 343 |
+
import fme.ace.inference.evaluator as evaluator
|
| 344 |
+
import fme.ace.inference.inference as inference
|
| 345 |
+
import xarray as xr
|
| 346 |
+
|
| 347 |
+
from xaig.adapters.latent_archive import write_archive
|
| 348 |
+
from xaig.daig.grid import Grid
|
| 349 |
+
|
| 350 |
+
out = Path(args.out).resolve()
|
| 351 |
+
staging = out.parent / f".{out.name}.work"
|
| 352 |
+
if staging.exists():
|
| 353 |
+
shutil.rmtree(staging)
|
| 354 |
+
staging.mkdir(parents=True)
|
| 355 |
+
run_dir = staging / "fme"
|
| 356 |
+
|
| 357 |
+
cfg = yaml.safe_load(Path(args.config).read_text())
|
| 358 |
+
source_experiment_dir = cfg.get("experiment_dir")
|
| 359 |
+
cfg["experiment_dir"] = str(run_dir)
|
| 360 |
+
cfg["n_forward_steps"] = args.steps
|
| 361 |
+
cfg["seed"] = args.seed
|
| 362 |
+
cfg["forward_steps_in_memory"] = min(cfg.get("forward_steps_in_memory", 20), args.steps)
|
| 363 |
+
evaluating = "loader" in cfg
|
| 364 |
+
fields = None if args.fields is None else [f for f in args.fields.split(",") if f]
|
| 365 |
+
if fields is None:
|
| 366 |
+
import torch as _torch
|
| 367 |
+
|
| 368 |
+
ckpt_cfg = _torch.load(cfg["checkpoint_path"], map_location="cpu", weights_only=False)
|
| 369 |
+
out_names = ckpt_cfg["stepper"]["config"]["step"]["config"].get("out_names", [])
|
| 370 |
+
del ckpt_cfg
|
| 371 |
+
fields = OCEAN_FIELDS if "sst" in out_names else DEFAULT_FIELDS
|
| 372 |
+
cfg["data_writer"] = {
|
| 373 |
+
"save_prediction_files": bool(fields),
|
| 374 |
+
"save_monthly_files": False,
|
| 375 |
+
**({"names": fields} if fields else {}),
|
| 376 |
+
}
|
| 377 |
+
cfg.setdefault("logging", {})["log_to_wandb"] = False
|
| 378 |
+
run_yaml = staging / "run.yaml"
|
| 379 |
+
run_yaml.write_text(yaml.safe_dump(cfg, sort_keys=False))
|
| 380 |
+
|
| 381 |
+
state = {}
|
| 382 |
+
config_class = evaluator.InferenceEvaluatorConfig if evaluating else inference.InferenceConfig
|
| 383 |
+
original_load = config_class.load_stepper
|
| 384 |
+
|
| 385 |
+
def load_stepper_with_hooks(self):
|
| 386 |
+
stepper = original_load(self)
|
| 387 |
+
kind, outer, net, taps = find_network(stepper)
|
| 388 |
+
s_layer, s_vec, s_info = steering_vector(args)
|
| 389 |
+
window = tuple(int(v) for v in args.steer_window.split(":")) if args.steer_window else None
|
| 390 |
+
rec = Recorder(
|
| 391 |
+
outer, taps, args.layers, args.steps, args.every, staging, args.dtype,
|
| 392 |
+
steer=(s_layer, s_vec) if s_vec is not None else None, window=window,
|
| 393 |
+
) # fmt: skip
|
| 394 |
+
rec.attach()
|
| 395 |
+
inputs = [n for n in args.inputs.split(",") if n]
|
| 396 |
+
if inputs:
|
| 397 |
+
state["inputs"] = InputCapture(stepper, outer, inputs, rec, args.steps + 1)
|
| 398 |
+
if args.skip_rescale:
|
| 399 |
+
if kind != "sfno":
|
| 400 |
+
raise SystemExit("--skip-rescale applies to an SFNO only")
|
| 401 |
+
name, factor = args.skip_rescale.rsplit(":", 1)
|
| 402 |
+
SkipRescale(stepper, net, name, float(factor))
|
| 403 |
+
state["skip"] = {"input": name, "factor": float(factor)}
|
| 404 |
+
state.update(rec=rec, steer=s_info, window=window, kind=kind)
|
| 405 |
+
return stepper
|
| 406 |
+
|
| 407 |
+
config_class.load_stepper = load_stepper_with_hooks
|
| 408 |
+
try:
|
| 409 |
+
(evaluator if evaluating else inference).main(str(run_yaml))
|
| 410 |
+
finally:
|
| 411 |
+
config_class.load_stepper = original_load
|
| 412 |
+
|
| 413 |
+
rec: Recorder = state["rec"]
|
| 414 |
+
if rec.call + 1 != args.steps:
|
| 415 |
+
logging.warning("model was called %d times for %d steps", rec.call + 1, args.steps)
|
| 416 |
+
|
| 417 |
+
pred = (
|
| 418 |
+
xr.open_dataset(run_dir / "autoregressive_predictions.nc", decode_timedelta=False)
|
| 419 |
+
if fields
|
| 420 |
+
else None
|
| 421 |
+
)
|
| 422 |
+
ic = xr.open_dataset(run_dir / "initial_condition.nc") if fields else None
|
| 423 |
+
if pred is None:
|
| 424 |
+
raise SystemExit(
|
| 425 |
+
"--fields '' is not supported yet: the grid and times come from the prediction file"
|
| 426 |
+
)
|
| 427 |
+
lat1 = pred["lat"].values
|
| 428 |
+
lon1 = pred["lon"].values
|
| 429 |
+
lon, lat = np.meshgrid(lon1, lat1)
|
| 430 |
+
ocean = state.get("kind") == "samudra"
|
| 431 |
+
mask = None
|
| 432 |
+
if ocean and "sst" in pred: # land holds no ocean: the first predicted SST says where
|
| 433 |
+
mask = np.isfinite(pred["sst"].values.reshape(-1, lat1.size * lon1.size)[0])
|
| 434 |
+
grid = Grid(lat=lat.ravel(), lon=lon.ravel(), shape=(lat1.size, lon1.size), mask=mask)
|
| 435 |
+
if rec.shape != (lat1.size, lon1.size):
|
| 436 |
+
raise SystemExit(
|
| 437 |
+
f"latent grid {rec.shape} differs from the output grid {(lat1.size, lon1.size)}"
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
all_times = time_labels(pred) # prediction valid times, one per step
|
| 441 |
+
ic_time = time_labels(ic)[0] if "valid_time" in ic or "time" in ic else None
|
| 442 |
+
if len(all_times) == args.steps and ic_time is not None:
|
| 443 |
+
field_times = [ic_time, *all_times]
|
| 444 |
+
else:
|
| 445 |
+
field_times = all_times
|
| 446 |
+
step_input_times = field_times[: args.steps] # the state each forward call started from
|
| 447 |
+
times = [step_input_times[s] for s in rec.recorded_steps]
|
| 448 |
+
|
| 449 |
+
field_arrays = {}
|
| 450 |
+
for name in fields:
|
| 451 |
+
if name not in pred:
|
| 452 |
+
logging.warning("field %s not in predictions; skipped", name)
|
| 453 |
+
continue
|
| 454 |
+
values = pred[name].values.reshape(-1, lat1.size * lon1.size) # (steps, nodes)
|
| 455 |
+
if len(field_times) == values.shape[0] + 1:
|
| 456 |
+
first = (
|
| 457 |
+
ic[name].values.reshape(1, -1)
|
| 458 |
+
if name in ic
|
| 459 |
+
else np.full((1, values.shape[1]), np.nan)
|
| 460 |
+
)
|
| 461 |
+
values = np.concatenate([first, values])
|
| 462 |
+
field_arrays[name] = values.astype(np.float32)
|
| 463 |
+
captured = state.get("inputs")
|
| 464 |
+
if captured is not None:
|
| 465 |
+
if len(field_times) != args.steps + 1:
|
| 466 |
+
raise SystemExit(
|
| 467 |
+
"input capture needs the initial condition's time in the prediction files"
|
| 468 |
+
)
|
| 469 |
+
for name, values in captured.values.items():
|
| 470 |
+
key = f"{name}_input" if name in field_arrays else name
|
| 471 |
+
field_arrays[key] = values
|
| 472 |
+
|
| 473 |
+
ckpt = cfg["checkpoint_path"]
|
| 474 |
+
run_id = Path(ckpt).parent.parent.name
|
| 475 |
+
experiment = {
|
| 476 |
+
"run_id": run_id,
|
| 477 |
+
"source_config": str(Path(args.config).resolve()),
|
| 478 |
+
"source_experiment_dir": source_experiment_dir,
|
| 479 |
+
"initial_condition": cfg.get("initial_condition", {}).get("path"),
|
| 480 |
+
"seed": cfg.get("seed"),
|
| 481 |
+
"recorded_steps": {"n": args.steps, "every": args.every},
|
| 482 |
+
"exported": dt.datetime.now().isoformat(timespec="seconds"),
|
| 483 |
+
**json.loads(args.experiment),
|
| 484 |
+
}
|
| 485 |
+
if state.get("skip"):
|
| 486 |
+
experiment["skip_rescale"] = state["skip"]
|
| 487 |
+
if state.get("steer"):
|
| 488 |
+
experiment["steer"] = {
|
| 489 |
+
**state["steer"],
|
| 490 |
+
**({"window": list(state["window"])} if state["window"] else {}),
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
labels = rec.labels()
|
| 494 |
+
layers = [(labels[i], rec.arrays[i]) for i in sorted(rec.arrays)]
|
| 495 |
+
if sorted(rec.arrays) != list(range(len(layers))):
|
| 496 |
+
# the reader indexes layers by position; keep the network's numbering in the label
|
| 497 |
+
layers = [(f"{labels[i]} [network layer {i}]", rec.arrays[i]) for i in sorted(rec.arrays)]
|
| 498 |
+
step_seconds = (
|
| 499 |
+
int(
|
| 500 |
+
round(
|
| 501 |
+
(np.datetime64(field_times[1]) - np.datetime64(field_times[0]))
|
| 502 |
+
/ np.timedelta64(1, "s")
|
| 503 |
+
)
|
| 504 |
+
)
|
| 505 |
+
if len(field_times) > 1
|
| 506 |
+
else None
|
| 507 |
+
)
|
| 508 |
+
write_archive(
|
| 509 |
+
out,
|
| 510 |
+
grid=grid,
|
| 511 |
+
times=times,
|
| 512 |
+
layers=layers,
|
| 513 |
+
fields=field_arrays or None,
|
| 514 |
+
field_times=field_times if field_arrays else None,
|
| 515 |
+
model=args.model_name or run_id,
|
| 516 |
+
component="ocean" if ocean else "atmosphere",
|
| 517 |
+
checkpoint=ckpt,
|
| 518 |
+
calendar=str(pred["time"].attrs.get("calendar", "noleap")) if "time" in pred else "noleap",
|
| 519 |
+
timestep_seconds=step_seconds,
|
| 520 |
+
experiment=experiment,
|
| 521 |
+
dtype=args.dtype,
|
| 522 |
+
overwrite=True,
|
| 523 |
+
)
|
| 524 |
+
if args.keep_run:
|
| 525 |
+
shutil.move(str(run_dir), str(out / "_fme_run"))
|
| 526 |
+
shutil.rmtree(staging, ignore_errors=True)
|
| 527 |
+
print(
|
| 528 |
+
f"wrote {out}: {len(times)} times x {len(layers)} layers; fields: {', '.join(field_arrays)}"
|
| 529 |
+
)
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
if __name__ == "__main__":
|
| 533 |
+
main()
|
kit/forcing-aerofrz/aero.2015-01.nc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e4852019df49bfe240e0c2c0234a2ab234aaaa425bff081dbd57713528a83719
|
| 3 |
+
size 64297664
|
kit/forcing-aerofrz/aero.2015-02.nc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b5b3201db3d9654b2e18d421aafa298ee0b91001b1082f888f4840131ce143de
|
| 3 |
+
size 58076768
|
kit/forcing/v3.LR.historical_0101.aigo.eam.h0.2015-01.nc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:93c82a6f0211c593b7b26f5c503b2c8f16b8f4ba2b7252f97f26e98906526378
|
| 3 |
+
size 134716164
|
kit/forcing/v3.LR.historical_0101.aigo.eam.h0.2015-02.nc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:198700789f49c2ef215ca3bdc61531d7ad03ced68efc916a626312d474833881
|
| 3 |
+
size 121586871
|
kit/ic/E05-FT-S01.restart.2015-01-03T12.nc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6e28123a6d0a4cfba3c701e35995ad744fe19cf6ad88caf8dd3a40c49024eebc
|
| 3 |
+
size 9896940
|