Datastore Explorer#
This notebook walks through the structure of the DANRA zarr datastore used in neural-lam. It covers dimensions, coordinates, time splits, and 2D visualizations of each feature.
Prerequisites: Run the Hello World notebook first to generate danra.datastore.zarr.
Note: Run from the repository root, or from
docs/notebooks/— the setup cell handles the working directory automatically.
1. Setup#
%matplotlib inline
import os
import matplotlib.pyplot as plt
import numpy as np
import xarray as xr
# Resolve repo root
while not os.path.isdir(os.path.join(os.getcwd(), "neural_lam")):
parent = os.path.dirname(os.getcwd())
if parent == os.getcwd():
raise RuntimeError("Could not find repo root (neural_lam/ not found)")
os.chdir(parent)
print("Working directory:", os.getcwd())
2. Load the Datastore#
We open the zarr archive directly for inspection, and separately load the datastore object (needed to recover the true 2-D grid shape).
ZARR_PATH = "tests/datastore_examples/mdp/danra_100m_winds/danra.datastore.zarr"
CONFIG_PATH = "tests/datastore_examples/mdp/danra_100m_winds/config.yaml"
if not os.path.exists(ZARR_PATH):
raise FileNotFoundError(
f"Zarr dataset not found at {ZARR_PATH}.\n"
"Run the Hello World notebook first to generate it."
)
ds = xr.open_zarr(ZARR_PATH, consolidated=True)
print("✅ Zarr datastore loaded")
print(ds)
from neural_lam.config import load_config_and_datastore
_, datastore = load_config_and_datastore(config_path=CONFIG_PATH)
# Recover true 2-D grid dimensions from the datastore.
# Never derive grid shape by taking the square root of grid_index — the grid
# is not guaranteed to be square.
grid_shape = datastore.grid_shape_state
grid_y, grid_x = grid_shape.y, grid_shape.x
assert grid_y * grid_x == ds.sizes["grid_index"], (
f"Grid shape mismatch: {grid_y}×{grid_x}={grid_y*grid_x} "
f"!= zarr grid_index size {ds.sizes['grid_index']}"
)
print(f"Grid shape: {grid_y} rows × {grid_x} cols = {grid_y * grid_x} points")
3. Dimensions and Coordinates#
The datastore uses a flat grid_index dimension for spatial data — a 1-D representation of the 2-D grid obtained by stacking the x and y axes.
print("=== Dimensions ===")
for dim, size in ds.sizes.items():
print(f" {dim:<25s}: {size}")
print("\n=== Coordinates ===")
for coord in ds.coords:
print(f" {coord:<35s}: {ds.coords[coord].dtype}")
print("\n=== Data Variables ===")
for var in ds.data_vars:
print(f" {var:<40s}: {list(ds[var].dims)}")
4. Time Splits#
The dataset is pre-split into train / val / test partitions along the time dimension by mllam-data-prep.
The split boundaries are stored in the splits data variable, indexed by split_name and split_part.
print("=== Time Splits ===")
for split in ["train", "val", "test"]:
da = ds.splits.sel(split_name=split)
start = da.sel(split_part="start").values.item()
end = da.sel(split_part="end").values.item()
print(f" {split:<8s}: {start} → {end}")
print(f"\n Total timesteps: {ds.sizes['time']}")
5. State Features#
State variables are the atmospheric fields the model predicts (wind components, temperature, relative humidity).
feature_names = ds.state_feature.values.tolist()
has_units = "state_feature_units" in ds.coords
has_lname = "state_feature_long_name" in ds.coords
print("=== State Features ===")
for i, feat in enumerate(feature_names):
units = ds.state_feature_units.values[i] if has_units else "—"
lname = ds.state_feature_long_name.values[i] if has_lname else feat
print(f" [{i}] {feat:<10s} units={units:<10s} ({lname})")
6. Visualize State Features#
We reshape the flat grid_index dimension back into the true 2-D grid shape using grid_shape_state from the datastore (loaded in Section 2).
n_features = ds.sizes["state_feature"]
fig, axes = plt.subplots(1, n_features, figsize=(5 * n_features, 4))
if n_features == 1:
axes = [axes]
for idx, (ax, feat_name) in enumerate(zip(axes, feature_names)):
field = ds["state"].isel(time=0, state_feature=idx).values
field_2d = field.reshape(grid_y, grid_x)
im = ax.imshow(field_2d, cmap="viridis", origin="lower")
plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
ax.set_title(f"{feat_name}\n(t=0)")
ax.set_xlabel("Grid X")
ax.set_ylabel("Grid Y")
plt.suptitle("State Features — First Timestep (2022-04-01 00:00)", fontsize=13)
plt.tight_layout()
plt.show()
7. Forcing and Static Variables#
print("=== Forcing Features ===")
for feat in ds.forcing_feature.values:
print(f" {feat}")
print("\n=== Static Features ===")
for feat in ds.static_feature.values:
print(f" {feat}")
n_static = ds.sizes["static_feature"]
fig, axes = plt.subplots(1, n_static, figsize=(5 * n_static, 4))
if n_static == 1:
axes = [axes]
for idx, (ax, feat) in enumerate(zip(axes, ds.static_feature.values)):
field = ds["static"].isel(static_feature=idx).values.reshape(grid_y, grid_x)
im = ax.imshow(field, cmap="terrain", origin="lower")
plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
ax.set_title(feat)
plt.suptitle("Static Features", fontsize=13)
plt.tight_layout()
plt.show()
8. Normalisation Statistics#
mllam-data-prep computes per-feature mean and standard deviation over the training split. These are stored in the zarr as data variables with the naming convention <category>__train__<stat>.
print("=== State Feature — Training Statistics ===")
means = ds["state__train__mean"].values
stds = ds["state__train__std"].values
for feat, mean, std in zip(feature_names, means, stds):
print(f" {feat:<10s} mean={mean:+.4f} std={std:.4f}")