--- license: cc-by-nc-4.0 tags: - diffusion - ddpm - 3d - volumetric - monai - generative - synthetic-data - atmospheric-science --- # 3D Convective-Cloud Denoising Diffusion Probabilistic Model An unconditional **3D Denoising Diffusion Probabilistic Model (DDPM)** that generates synthetic **64×64×64 convective-cloud cells** (vertical-velocity volumes). Roughly 190M parameters. **What it is for:** producing synthetic 3D cloud cells to augment scarce atmospheric-simulation data — for example as additional training data for 3D cloud segmentation. **What it is not:** this is not a weather-forecasting model and not a physical simulation. It samples volumes resembling its training distribution; it does not solve atmospheric dynamics. **Project:** [3D Convective-Cloud Generation collection](https://huggingface.co/collections/HoqueMahmudul/3d-convective-cloud-generation-ddpm-190m-params-6a6ba230c89356ff617d481a) > **Status:** research artifact from work with an associated manuscript **in > preparation**. Methodology, training details, and evaluation are deliberately > not described here and will appear in the publication. ## Requirements This is a **MONAI** model, not a `transformers` model. You need **`monai-generative`** (which provides the `generative` package) to build the network before loading the weights: ```bash pip install monai-generative==0.2.3 monai==1.5.2 torch safetensors huggingface_hub ``` A `requirements.txt` with the verified-working versions is included in this repo: ```bash pip install -r <(curl -sL https://huggingface.co/HoqueMahmudul/convective-cloud-denoising-diffusion-probabilistic-model-3d/raw/main/requirements.txt) ``` > [!IMPORTANT] > **Use `monai-generative`, not MONAI core.** MONAI core (>= 1.4) ships its own > `DiffusionModelUNet`, `DDPMScheduler` and `DiffusionInferer`, and they look > like drop-in replacements. **They are not weight-compatible with this > checkpoint.** > > | | `monai-generative` 0.2.3 | MONAI core 1.5 | > |---|---|---| > | Attention keys | `...attentions.0.to_q.weight` | `...attentions.0.`**`attn`**`.to_q.weight` | > | Constructor arg | `num_channels=[...]` | `channels=[...]` | > | Strict load here | works | **52 missing / 52 unexpected keys** | > > Both classes have the *identical* parameter count (189,815,297), so if you hit > the key error and "fix" it with `strict=False`, you will get a model with **52 > randomly-initialised attention tensors**. It will run and produce > plausible-looking output, and it will be silently wrong. > > **Never load these weights with `strict=False`.** If a strict load fails, the > environment is wrong — fix the environment. ## Files | File | What it is | |---|---| | `best_model.safetensors` | The model weights (a plain PyTorch `state_dict`) | | `requirements.txt` | Verified-working dependency versions | ## How to load and generate a sample The configuration below is **required** — the weights are a bare `state_dict`, so the network must be constructed exactly this way before loading. ```python import torch, numpy as np from huggingface_hub import hf_hub_download from safetensors.torch import load_file from generative.networks.nets import DiffusionModelUNet from generative.networks.schedulers import DDPMScheduler from generative.inferers import DiffusionInferer REPO = "HoqueMahmudul/convective-cloud-denoising-diffusion-probabilistic-model-3d" device = "cuda" # 1. build the network with exactly this configuration model = DiffusionModelUNet( spatial_dims=3, in_channels=1, out_channels=1, num_channels=[256, 256, 512], attention_levels=[False, False, True], num_head_channels=[0, 0, 512], num_res_blocks=2, ) # 2. load the weights -- strict=True is deliberate; see the warning above weights = hf_hub_download(REPO, "best_model.safetensors") model.load_state_dict(load_file(weights), strict=True) model.to(device).eval() # 3. the sampler must use these settings scheduler = DDPMScheduler( num_train_timesteps=1000, schedule="scaled_linear_beta", beta_start=0.0005, beta_end=0.0195, ) scheduler.set_timesteps(1000) inferer = DiffusionInferer(scheduler) # 4. sample one volume (1000 reverse steps -- use a GPU) noise = torch.randn((1, 1, 64, 64, 64), device=device) with torch.no_grad(), torch.amp.autocast("cuda"): x = inferer.sample(input_noise=noise, diffusion_model=model, scheduler=scheduler) # 5. convert the [0,1] output back to raw units # clip the LOWER bound at 1, not 0: the model's background output is very # slightly negative, which would otherwise denormalise to 0 and leave you # with a background value the format does not use. blob = x[0, 0].cpu().numpy() blob = np.clip(blob * (3100 - 1) + 1, 1, 3100).astype(np.float32) # 6. enforce the value gap the data format requires (see below) gap = (blob > 1) & (blob < 300) blob[gap] = np.where(blob[gap] < 150, 1, 300) blob = np.round(blob).astype(np.uint16) # 64^3 synthetic cloud cell mask = blob >= 300 # binary cloud mask ``` ## Understanding the output You need these conventions to interpret a generated volume: | Property | Value | |---|---| | Shape / dtype | `64 × 64 × 64`, `uint16` | | Background | `1` | | Cloud voxels | `300` – `3022` (vertical velocity, scaled units) | | Value gap | nothing lies strictly between `1` and `300` | | Voxel size | 0.5 km isotropic (so 20 voxels = 10 km) | | Normalisation | `(raw - 1) / (3100 - 1)`, clipped to `[0, 1]` | The model operates in the normalised `[0, 1]` space, so step 5 above is required to return to raw units, and step 6 restores the value gap that real volumes have. ## Post-processing — extracting a single cloud cell Steps 5 and 6 of the snippet give you a valid volume, but a raw sample often contains several disconnected pieces (or none). To get a usable single cloud cell you need to threshold, find connected components, keep the largest, and blank the rest. Requires `scipy`: ```python import numpy as np from scipy import ndimage def extract_cloud(blob, min_voxels=6, max_voxels=None): """Keep only the largest 26-connected cloud component; blank everything else. Returns (cleaned, mask, size). cleaned : uint16 volume containing at most one cloud component mask : boolean cloud mask for `cleaned` size : voxel count of the kept component; 0 means "discard this sample" """ mask = blob >= 300 if not mask.any(): return blob.copy(), mask, 0 # empty sample # 26-connectivity: faces, edges AND corners count as connected labels, n = ndimage.label(mask, structure=np.ones((3, 3, 3))) sizes = ndimage.sum(mask, labels, range(1, n + 1)) keep = int(np.argmax(sizes)) + 1 largest = int(sizes.max()) cleaned = blob.copy() cleaned[labels != keep] = 1 # fragments -> background if largest < min_voxels: return cleaned, cleaned >= 300, 0 # noise only -> discard if max_voxels is not None and largest > max_voxels: return cleaned, cleaned >= 300, largest # implausibly large -> review return cleaned, cleaned >= 300, largest cleaned, mask, size = extract_cloud(blob) if size == 0: print("empty or noise-only sample -- generate another") else: print(f"single cloud cell, {size} voxels " f"({size * 0.5**3:.1f} km^3 at 0.5 km voxels)") ``` `min_voxels` rejects noise-only samples and `max_voxels` (optional) flags blobs that fill an implausible share of the volume. Both are yours to choose — pick them from the size range your application needs. Expect to discard a large fraction of samples; see the note below. ## Practical notes before you use this - **Sampling is variable and low-yield.** Only a minority of raw samples are usable single cloud cells; the rest come out empty, as noise fragments, or occasionally as a blob filling the whole volume. **Plan on generating a surplus and filtering** with `extract_cloud()` above, discarding samples where it returns `size == 0`. This is the intended workflow, not a sign of a defect. - **The size distribution is skewed.** Generated cells tend to be smaller and weaker than those in the source data, so a raw synthetic set is not a distribution-matched substitute for real data. - **One source dataset.** Behaviour outside that simulation regime is unknown. - **Cost.** Each sample takes 1000 reverse diffusion steps. A GPU is strongly recommended. ## Intended use and limitations Intended for **research and educational use** — synthetic data augmentation and generative-modelling experiments on volumetric atmospheric data. No perceptual or physical-fidelity metrics are published for this model, so **no fidelity or accuracy claims are made**. Do not use it for operational forecasting, hazard assessment, or any decision-making application. ## Data and credit Trained on convective-cloud cells extracted from atmospheric-simulation output. **The dataset is not included or redistributed here.** The underlying simulations come from **Dr. Xiaowen Li's lab at Morgan State University**; please credit that lab as the source of the simulation data if you use this model. The diffusion model in this repository was developed by Mahmudul Hoque. ## License **CC BY-NC 4.0** — [Creative Commons Attribution-NonCommercial 4.0 International](https://creativecommons.org/licenses/by-nc/4.0/). You may use, share, and adapt these weights for **non-commercial** purposes with attribution. Commercial use is not permitted. Please also credit the source of the simulation data as described above. ## References - Ho, Jain & Abbeel, *Denoising Diffusion Probabilistic Models*, NeurIPS 2020 ([arXiv:2006.11239](https://arxiv.org/abs/2006.11239)) - [MONAI GenerativeModels](https://github.com/Project-MONAI/GenerativeModels) ## Contact Mahmudul Hoque, Morgan State University —