Download tests/test_method_spec_gaps.py from AnonMLuser/remdm-planner-minihack: direct link, hf CLI and curl.
- Browser
- Download file 7.19 kB
-
https://huggingface.co/AnonMLuser/remdm-planner-minihack/resolve/main/tests/test_method_spec_gaps.py
- Command line
-
hf download hf://AnonMLuser/remdm-planner-minihack/tests/test_method_spec_gaps.py
-
curl -L -o test_method_spec_gaps.py https://huggingface.co/AnonMLuser/remdm-planner-minihack/resolve/main/tests/test_method_spec_gaps.py
7.19 kB
| """Literature-anchored specification tests — step-8 gap closure. | |
| Complements tests/test_method_spec.py with the surfaces the step-8 audit | |
| found untested in this repo: the reverse-posterior/remasking/carry-over | |
| chain through remdm_sample, the final greedy cleanup, decode temperature | |
| and label smoothing. Every expected value derives from a cited source or | |
| a derivation written in the docstring — never from the current output of | |
| this or the sibling repo. The craftax twin file carries the same | |
| assertions with the same inputs and tolerances. | |
| References as in tests/test_method_spec.py (MDLM arXiv:2406.07524; | |
| ReMDM arXiv:2503.00307; Holtzman arXiv:1904.09751). | |
| """ | |
| from __future__ import annotations | |
| import math | |
| from types import SimpleNamespace | |
| import numpy as np | |
| import pytest | |
| import torch | |
| from torch import nn | |
| from src.diffusion.loss import mdlm_loss | |
| from src.diffusion.sampling import remdm_sample | |
| from src.diffusion.schedules import linear_schedule, linear_schedule_deriv | |
| class _TimeCodedModel(nn.Module): | |
| """Stub whose argmax token encodes the decode time bin. | |
| token 0 for t_discrete > 90, token 1 for 50 < t_discrete <= 90, | |
| token 2 otherwise (num_diffusion_steps = 100 in the cfg below). | |
| Logit margin 30 makes categorical sampling deterministic to ~1e-13. | |
| """ | |
| def __init__(self, seq_len: int, vocab: int): | |
| super().__init__() | |
| self.seq_len = seq_len | |
| self.vocab = vocab | |
| def forward(self, local_obs, global_obs, seq, t_discrete): | |
| td = int(t_discrete[0]) | |
| idx = 0 if td > 90 else (1 if td > 50 else 2) | |
| logits = torch.full((seq.shape[0], self.seq_len, self.vocab), -30.0) | |
| logits[:, :, idx] = 30.0 | |
| return {"actions": logits, "goal_pred": torch.zeros(seq.shape[0], 2)} | |
| def _chain_cfg(eta: float) -> SimpleNamespace: | |
| return SimpleNamespace( | |
| seq_len=32, mask_token=3, action_dim=3, diffusion_steps_eval=3, | |
| temperature=1.0, top_p=1.0, eta=eta, remask_strategy="rescale", | |
| noise_schedule="linear", num_diffusion_steps=100, | |
| ) | |
| def test_posterior_remask_carryover_token_distribution(eta): | |
| """Final token distribution matches the ReMDM Alg 1 posterior chain. | |
| Source: ReMDM eq (6) masked branch, eq (7) sigma_max, Sec 4.1 | |
| rescale sigma = eta * sigma_max, MDLM Sec 3.2.3 carry-over. | |
| Derivation (linear schedule, K=3, grid t=1,2/3,1/3 with s=2/3,1/3,0; | |
| the stub decodes token 0 at t=1, token 1 at t=2/3, token 2 at | |
| t<=1/3): | |
| step 1 (t=1): alpha_t=0, alpha_s=1/3, p_unmask=1/3 -> token 0. | |
| step 2 (t=2/3): alpha_t=1/3, alpha_s=2/3, sigma_max=1, sigma=eta; | |
| committed remask w.p. eta; masked unmask w.p. | |
| (2/3-(1-eta)/3)/(2/3) = (1+eta)/2 -> token 1. | |
| step 3 (t=1/3): alpha_s=1 -> sigma_max=0 (no remask), p_unmask=1 | |
| -> every remaining mask becomes token 2. | |
| Hence P(0) = (1-eta)/3, P(1) = (1+eta)/3, P(2) = 1/3. | |
| eta=0 additionally pins carry-over (sigma=0: committed tokens are | |
| never re-decided). Same derivation and bound as the craftax twin: | |
| N = 512*32 = 16384; max sigma = 0.0039; bound 0.02 = 5.1 sigma. | |
| """ | |
| B, L = 512, 32 | |
| cfg = _chain_cfg(eta) | |
| model = _TimeCodedModel(L, 4) | |
| seq = remdm_sample( | |
| model, torch.zeros(B, 9, 9), torch.zeros(B, 21, 79), cfg, "cpu", | |
| physics_aware=False, | |
| ) | |
| freq = np.bincount(seq.numpy().ravel(), minlength=4) / (B * L) | |
| expected = np.array([(1 - eta) / 3, (1 + eta) / 3, 1 / 3, 0.0]) | |
| assert np.all(np.abs(freq - expected) < 0.02), (freq, expected) | |
| def test_final_cleanup_commits_all_remaining_masks(): | |
| """With zero denoising steps, the final greedy cleanup decodes every | |
| position at t=0 (argmax), leaving no MASK token. | |
| Source: spec-method 4.9 (final-step commit of remaining masks; the | |
| craftax twin asserts the same). The stub decodes token 2 at t=0, | |
| so the output must be all-2. | |
| """ | |
| B, L = 8, 16 | |
| cfg = _chain_cfg(0.0) | |
| cfg.seq_len = L | |
| seq = remdm_sample( | |
| _TimeCodedModel(L, 4), torch.zeros(B, 9, 9), torch.zeros(B, 21, 79), | |
| cfg, "cpu", physics_aware=False, num_steps=0, | |
| ) | |
| assert (seq.numpy() == 2).all() | |
| class _FixedLogitsModel(nn.Module): | |
| """Stub returning fixed logits [0, 1] over two real actions.""" | |
| def __init__(self, seq_len: int): | |
| super().__init__() | |
| self.seq_len = seq_len | |
| def forward(self, local_obs, global_obs, seq, t_discrete): | |
| logits = torch.zeros(seq.shape[0], self.seq_len, 3) | |
| logits[:, :, 1] = 1.0 | |
| logits[:, :, 2] = 0.0 # masked out by the sampler (>= action_dim) | |
| return {"actions": logits, "goal_pred": torch.zeros(seq.shape[0], 2)} | |
| def test_decode_temperature_scales_logits_before_sampling(): | |
| """Sampling frequencies follow softmax(logits / temperature). | |
| Source: spec-method 5.2. Derivation: logits [0, 1] at temperature | |
| 0.5 give softmax([0, 2]) = [0.1192, 0.8808] over the two real | |
| actions; a single denoising step (K=1: t=1, s=0, p_unmask=1) | |
| commits every position in one draw. Statistical: 8192 draws; | |
| sigma = sqrt(0.8808*0.1192/8192) = 0.00358; bound 0.0143 = 4 sigma. | |
| Same numbers as the craftax twin. | |
| """ | |
| B, L = 256, 32 # 8192 positions | |
| cfg = SimpleNamespace( | |
| seq_len=L, mask_token=2, action_dim=2, diffusion_steps_eval=1, | |
| temperature=0.5, top_p=1.0, eta=0.0, remask_strategy="rescale", | |
| noise_schedule="linear", num_diffusion_steps=100, | |
| ) | |
| seq = remdm_sample( | |
| _FixedLogitsModel(L), torch.zeros(B, 9, 9), torch.zeros(B, 21, 79), | |
| cfg, "cpu", physics_aware=False, | |
| ) | |
| p1 = math.exp(2) / (1 + math.exp(2)) | |
| assert abs(seq.float().mean().item() - p1) < 0.0143 | |
| def test_label_smoothing_matches_closed_form(): | |
| """Smoothed CE = -[(1-eps)+eps/V] log p_true - (eps/V) sum log p_other. | |
| Source: spec-method 3.7 (smoothing target (1-eps)*onehot + eps/V, | |
| which is exactly torch.nn.functional.cross_entropy's label_smoothing | |
| semantics; eps=0 is the exact ELBO). Derivation: model probs | |
| [0.7,0.1,0.1,0.1], true class 0, V=4, eps=0.3: coefficient on | |
| -log 0.7 is (1-0.3)+0.3/4 = 0.775; each other class gets 0.075. | |
| t pinned to 1 (linear, w=1, everything masked) makes the loss equal | |
| that CE exactly. Same inputs and expectations as the craftax twin. | |
| """ | |
| B, L, V = 4, 8, 4 | |
| logits = torch.log(torch.tensor([0.7, 0.1, 0.1, 0.1])).expand(B, L, V).clone() | |
| x0 = torch.zeros(B, L, dtype=torch.long) | |
| zt = torch.full((B, L), 5, dtype=torch.long) # mask_token=5, pad=6 | |
| t = torch.ones(B) | |
| expected = -0.775 * math.log(0.7) - 0.075 * 3 * math.log(0.1) | |
| got = mdlm_loss( | |
| logits, x0, zt, t, mask_token=5, pad_token=6, | |
| schedule_fn=linear_schedule, schedule_deriv_fn=linear_schedule_deriv, | |
| label_smoothing=0.3, | |
| ) | |
| assert abs(float(got) - expected) < 1e-5 | |
| got0 = mdlm_loss( | |
| logits, x0, zt, t, mask_token=5, pad_token=6, | |
| schedule_fn=linear_schedule, schedule_deriv_fn=linear_schedule_deriv, | |
| label_smoothing=0.0, | |
| ) | |
| assert abs(float(got0) - (-math.log(0.7))) < 1e-5 | |