Download code/causalcelljepa/dynamics.py from NisargOza/CausalCellJEPA: direct link, hf CLI and curl.
- Browser
- Download file 67.3 kB
-
https://huggingface.co/NisargOza/CausalCellJEPA/resolve/main/code/causalcelljepa/dynamics.py
- Command line
-
hf download hf://NisargOza/CausalCellJEPA/code/causalcelljepa/dynamics.py
-
curl -L -o dynamics.py https://huggingface.co/NisargOza/CausalCellJEPA/resolve/main/code/causalcelljepa/dynamics.py
67.3 kB
| # Unpaired, batch-matched population sampling and the locked Stage 2 world model. | |
| # Neither cell-line IDs nor pairwise control/outcome correspondences enter the model or loss. | |
| import json | |
| import math | |
| import time | |
| from collections import Counter | |
| from copy import deepcopy | |
| from hashlib import sha256 | |
| from importlib.metadata import version | |
| from pathlib import Path | |
| import h5py | |
| import numpy as np | |
| import torch | |
| import yaml | |
| from geomloss import SamplesLoss | |
| from torch import nn | |
| from torch.nn import functional as F | |
| from torch.utils.data import Dataset | |
| from causalcelljepa.resources import file_sha256 | |
| from causalcelljepa.training import ( | |
| _data_loader, | |
| _git_state, | |
| _json_log, | |
| _runtime_environment, | |
| _runtime_source_hash, | |
| epoch_order, | |
| initial_train_state, | |
| learning_rate_schedule, | |
| load_checkpoint, | |
| save_checkpoint, | |
| seed_everything, | |
| ) | |
| class LatentPopulationDataset(Dataset): | |
| """Sample independent outcome/control sets with identical batch multiplicities.""" | |
| def __init__( | |
| self, | |
| cache_path, | |
| action_path, | |
| manifest_path, | |
| mode, | |
| population_size=32, | |
| seed=0, | |
| outcome_role=None, | |
| control_role=None, | |
| context="K562", | |
| ): | |
| self.cache_path, self.mode, self.context = Path(cache_path), mode, context | |
| self.population_size, self.seed, self.epoch = population_size, seed, 0 | |
| self.manifest = json.loads(Path(manifest_path).read_text()) | |
| with h5py.File(self.cache_path, "r") as cache: | |
| roles = cache["role"].asstr()[:] | |
| self.targets = cache["target"].asstr()[:] | |
| self.batches = cache["source_batch"].asstr()[:] | |
| contexts = cache["context"].asstr()[:] | |
| expected_targets = None | |
| if outcome_role is None: | |
| outcome_role = self.manifest["conditions"][mode]["role"] | |
| expected_targets = self.manifest["conditions"][mode]["targets"] | |
| control_role = control_role or self.manifest["conditions"]["controls"]["role"] | |
| outcome_indices = np.flatnonzero(roles == outcome_role) | |
| control_indices = np.flatnonzero(roles == control_role) | |
| assert set(contexts[outcome_indices]) == set(contexts[control_indices]) == {context} | |
| self.condition_targets = sorted(set(self.targets[outcome_indices])) | |
| assert expected_targets is None or len(self.condition_targets) == expected_targets | |
| self.outcomes = { | |
| target: outcome_indices[self.targets[outcome_indices] == target] | |
| for target in self.condition_targets | |
| } | |
| self.controls = { | |
| batch: control_indices[self.batches[control_indices] == batch] | |
| for batch in sorted(set(self.batches[control_indices])) | |
| } | |
| assert all(len(indices) >= population_size for indices in self.outcomes.values()) | |
| action = torch.load(action_path, map_location="cpu", weights_only=True) | |
| action_index = {target: index for index, target in enumerate(action["targets"])} | |
| self.action = { | |
| target: (action["embedding"][action_index[target]], action["known"][action_index[target]]) | |
| for target in self.condition_targets | |
| } | |
| normalization = self.manifest["normalization"] | |
| self.mean = np.asarray(normalization["latent_mean"], dtype=np.float32) | |
| self.scale = ( | |
| np.asarray(normalization["latent_std"], dtype=np.float32) | |
| * normalization["dimension_scale"] | |
| ) | |
| self._cache = None | |
| def __len__(self): | |
| return len(self.condition_targets) | |
| def set_epoch(self, epoch): | |
| self.epoch = epoch | |
| def sample_indices(self, index): | |
| """Expose deterministic indices for leakage and batch-matching audits.""" | |
| target = self.condition_targets[index] | |
| generator = np.random.default_rng( | |
| int.from_bytes( | |
| sha256(f"{self.seed}\0{self.mode}\0{self.epoch}\0{target}".encode()).digest()[:8], | |
| "little", | |
| ) | |
| ) | |
| outcomes = generator.choice( | |
| self.outcomes[target], self.population_size, replace=False | |
| ) | |
| controls = np.concatenate( | |
| [ | |
| generator.choice(self.controls[batch], count, replace=False) | |
| for batch, count in sorted(Counter(self.batches[outcomes]).items()) | |
| ] | |
| ) | |
| generator.shuffle(outcomes) | |
| generator.shuffle(controls) | |
| return controls, outcomes, target | |
| def __getitem__(self, index): | |
| if self._cache is None: | |
| self._cache = h5py.File(self.cache_path, "r") | |
| controls, outcomes, target = self.sample_indices(index) | |
| populations = [] | |
| for indices in (controls, outcomes): | |
| order = np.argsort(indices) | |
| latents = self._cache["latent"][indices[order]][np.argsort(order)] | |
| populations.append(torch.from_numpy((latents - self.mean) / self.scale)) | |
| action, known = self.action[target] | |
| return { | |
| "control": populations[0], | |
| "perturbed": populations[1], | |
| "action": action, | |
| "action_known": known, | |
| "target": target, | |
| } | |
| def __getstate__(self): | |
| state = self.__dict__.copy() | |
| state["_cache"] = None | |
| return state | |
| class ConditionalTransitionBlock(nn.Module): | |
| """Inject the context-action interaction into attention and feed-forward paths.""" | |
| def __init__(self, dim=256, heads=8, ffn_dim=1024, dropout=0.1): | |
| super().__init__() | |
| self.condition = nn.Linear(dim, dim) | |
| self.attention_norm = nn.LayerNorm(dim) | |
| self.attention = nn.MultiheadAttention(dim, heads, dropout=dropout, batch_first=True) | |
| self.ffn_norm = nn.LayerNorm(dim) | |
| self.ffn = nn.Sequential( | |
| nn.Linear(dim, ffn_dim), | |
| nn.GELU(), | |
| nn.Dropout(dropout), | |
| nn.Linear(ffn_dim, dim), | |
| nn.Dropout(dropout), | |
| ) | |
| def forward(self, states, interaction): | |
| condition = self.condition(interaction).unsqueeze(1) | |
| attended, _ = self.attention( | |
| self.attention_norm(states + condition), | |
| self.attention_norm(states + condition), | |
| self.attention_norm(states + condition), | |
| need_weights=False, | |
| ) | |
| states = states + attended | |
| return states + self.ffn(self.ffn_norm(states + condition)) | |
| class ModalityAttentiveActionProjection(nn.Module): | |
| """Project frozen action teachers and fuse them with sample-specific attention.""" | |
| def __init__( | |
| self, | |
| modality_dims, | |
| output_dim, | |
| modality_dropout=0.0, | |
| modality_availability=False, | |
| ): | |
| super().__init__() | |
| assert sum(modality_dims) > 0 and 0 <= modality_dropout < 1 | |
| self.modality_dims = tuple(modality_dims) | |
| self.modality_dropout = modality_dropout | |
| self.modality_availability = modality_availability | |
| self.projectors = nn.ModuleList( | |
| nn.Sequential(nn.LayerNorm(width), nn.Linear(width, output_dim)) | |
| for width in modality_dims | |
| ) | |
| self.query = nn.Parameter(torch.randn(output_dim) * 0.02) | |
| self.output = nn.LayerNorm(output_dim) | |
| def projected_and_weights(self, action): | |
| feature_width = sum(self.modality_dims) | |
| expected_width = feature_width + ( | |
| len(self.modality_dims) if self.modality_availability else 0 | |
| ) | |
| assert action.shape[1] == expected_width | |
| blocks = action[:, :feature_width].split(self.modality_dims, 1) | |
| projected = torch.stack( | |
| [ | |
| projector(block) | |
| for projector, block in zip(self.projectors, blocks, strict=True) | |
| ], | |
| 1, | |
| ) | |
| scores = (torch.tanh(projected) * self.query).sum(2) / math.sqrt(projected.shape[2]) | |
| visible = ( | |
| action[:, feature_width:].bool() | |
| if self.modality_availability | |
| else torch.ones_like(scores, dtype=torch.bool) | |
| ) | |
| if self.training and self.modality_dropout: | |
| visible &= torch.rand_like(scores) >= self.modality_dropout | |
| missing = ~visible.any(1) | |
| if missing.any(): | |
| available = ( | |
| action[:, feature_width:].bool() | |
| if self.modality_availability | |
| else torch.ones_like(visible) | |
| ) | |
| fallback = available.float().argmax(1) | |
| visible[missing, fallback[missing]] = True | |
| weights = scores.masked_fill(~visible, -torch.inf).softmax(1) | |
| return projected, weights | |
| def forward(self, action): | |
| projected, weights = self.projected_and_weights(action) | |
| return self.output((weights.unsqueeze(2) * projected).sum(1)) | |
| class ContextConditionedModalityProjection(nn.Module): | |
| """Fuse available action teachers using an identity-free control-state query.""" | |
| def __init__(self, modality_dims, output_dim, modality_dropout=0.0): | |
| super().__init__() | |
| assert sum(modality_dims) > 0 and 0 <= modality_dropout < 1 | |
| self.modality_dims = tuple(modality_dims) | |
| self.modality_dropout = modality_dropout | |
| self.projectors = nn.ModuleList( | |
| nn.Sequential(nn.LayerNorm(width), nn.Linear(width, output_dim)) | |
| for width in modality_dims | |
| ) | |
| self.query = nn.Parameter(torch.randn(output_dim) * 0.02) | |
| self.context_query = nn.Sequential(nn.LayerNorm(output_dim), nn.Linear(output_dim, output_dim)) | |
| nn.init.zeros_(self.context_query[-1].weight) | |
| nn.init.zeros_(self.context_query[-1].bias) | |
| self.output = nn.LayerNorm(output_dim) | |
| def projected_and_weights(self, action, context): | |
| feature_width = sum(self.modality_dims) | |
| assert action.shape[1] == feature_width + len(self.modality_dims) | |
| blocks = action[:, :feature_width].split(self.modality_dims, 1) | |
| availability = action[:, feature_width:].bool() | |
| projected = torch.stack( | |
| [ | |
| projector(block) | |
| for projector, block in zip(self.projectors, blocks, strict=True) | |
| ], | |
| 1, | |
| ) | |
| query = self.query.unsqueeze(0) + self.context_query(context) | |
| scores = (torch.tanh(projected) * query.unsqueeze(1)).sum(2) / math.sqrt( | |
| projected.shape[2] | |
| ) | |
| visible = availability.clone() | |
| if self.training and self.modality_dropout: | |
| visible &= torch.rand_like(scores) >= self.modality_dropout | |
| missing = ~visible.any(1) | |
| if missing.any(): | |
| fallback = availability.float().argmax(1) | |
| visible[missing, fallback[missing]] = True | |
| weights = scores.masked_fill(~visible, -torch.inf).softmax(1) | |
| return projected, weights | |
| def forward(self, action, context): | |
| projected, weights = self.projected_and_weights(action, context) | |
| return self.output((weights.unsqueeze(2) * projected).sum(1)) | |
| class PopulationDynamics(nn.Module): | |
| """Permutation-equivariant residual dynamics conditioned on baseline context and action.""" | |
| def __init__( | |
| self, | |
| cell_dim=256, | |
| action_input_dim=320, | |
| action_dim=256, | |
| context_blocks=2, | |
| transition_blocks=3, | |
| heads=8, | |
| ffn_dim=1024, | |
| dropout=0.1, | |
| context_mode="set_transformer", | |
| action_modalities=None, | |
| action_modality_dropout=0.0, | |
| action_context_conditioned=False, | |
| action_modality_availability=False, | |
| ): | |
| super().__init__() | |
| assert context_mode in {"set_transformer", "mean", "none"} | |
| availability_width = len(action_modalities or ()) if action_modality_availability else 0 | |
| assert action_modalities is None or sum(action_modalities) + availability_width == action_input_dim | |
| assert not action_context_conditioned or action_modality_availability | |
| self.context_mode = context_mode | |
| self.action_context_conditioned = action_context_conditioned | |
| context_layer = nn.TransformerEncoderLayer( | |
| cell_dim, | |
| heads, | |
| ffn_dim, | |
| dropout, | |
| activation="gelu", | |
| batch_first=True, | |
| norm_first=True, | |
| ) | |
| self.context_blocks = nn.TransformerEncoder( | |
| context_layer, | |
| context_blocks, | |
| nn.LayerNorm(cell_dim), | |
| enable_nested_tensor=False, | |
| ) | |
| self.pool_query = nn.Parameter(torch.randn(1, cell_dim) * 0.02) | |
| self.pool = nn.MultiheadAttention(cell_dim, heads, dropout=dropout, batch_first=True) | |
| self.context_output = nn.LayerNorm(cell_dim) | |
| if action_context_conditioned: | |
| self.action_projection = ContextConditionedModalityProjection( | |
| action_modalities, action_dim, action_modality_dropout | |
| ) | |
| elif action_modalities: | |
| self.action_projection = ModalityAttentiveActionProjection( | |
| action_modalities, | |
| action_dim, | |
| action_modality_dropout, | |
| action_modality_availability, | |
| ) | |
| else: | |
| self.action_projection = nn.Sequential( | |
| nn.LayerNorm(action_input_dim), nn.Linear(action_input_dim, action_dim) | |
| ) | |
| self.unknown_action = nn.Parameter(torch.randn(action_dim) * 0.02) | |
| self.interaction = nn.Sequential( | |
| nn.LayerNorm(4 * cell_dim), | |
| nn.Linear(4 * cell_dim, ffn_dim), | |
| nn.GELU(), | |
| nn.Linear(ffn_dim, cell_dim), | |
| ) | |
| self.transition = nn.ModuleList( | |
| ConditionalTransitionBlock(cell_dim, heads, ffn_dim, dropout) | |
| for _ in range(transition_blocks) | |
| ) | |
| self.delta = nn.Sequential(nn.LayerNorm(cell_dim), nn.Linear(cell_dim, cell_dim)) | |
| nn.init.zeros_(self.delta[-1].weight) | |
| nn.init.zeros_(self.delta[-1].bias) | |
| def forward(self, control, action_embedding, action_known): | |
| if self.context_mode == "set_transformer": | |
| encoded = self.context_blocks(control) | |
| query = self.pool_query.unsqueeze(0).expand(control.shape[0], -1, -1) | |
| context, _ = self.pool(query, encoded, encoded, need_weights=False) | |
| context = self.context_output(context.squeeze(1)) | |
| elif self.context_mode == "mean": | |
| context = self.context_output(control.mean(1)) | |
| else: | |
| context = torch.zeros_like(control[:, 0]) | |
| projected = ( | |
| self.action_projection(action_embedding, context) | |
| if self.action_context_conditioned | |
| else self.action_projection(action_embedding) | |
| ) | |
| action = torch.where( | |
| action_known.unsqueeze(1), projected, self.unknown_action.unsqueeze(0) | |
| ) | |
| interaction = self.interaction( | |
| torch.cat((context, action, context * action, torch.abs(context - action)), dim=1) | |
| ) | |
| states = control | |
| for block in self.transition: | |
| states = block(states, interaction) | |
| return control + self.delta(states) | |
| class FrozenLowRankEffectAnchor(nn.Module): | |
| """Apply a frozen low-rank ESM-to-latent perturbation-effect map.""" | |
| def __init__(self, checkpoint): | |
| super().__init__() | |
| assert checkpoint["format_version"] == 1 | |
| assert checkpoint["architecture"] in { | |
| "esm2_low_rank_latent_effect_ridge", | |
| "multiteacher_low_rank_latent_effect_ridge", | |
| } | |
| for name in ("x_mean", "x_std", "y_mean", "components", "weights"): | |
| value = checkpoint[name].detach().float() | |
| assert torch.isfinite(value).all() | |
| self.register_buffer(name, value) | |
| input_indices = checkpoint.get("input_indices", torch.arange(len(self.x_mean))) | |
| input_indices = input_indices.detach().long() | |
| assert input_indices.ndim == 1 and len(input_indices) == len(self.x_mean) | |
| assert len(input_indices.unique()) == len(input_indices) and int(input_indices.min()) >= 0 | |
| self.register_buffer("input_indices", input_indices) | |
| assert self.weights.shape == (len(self.x_mean), len(self.components)) | |
| assert self.components.shape[1] == len(self.y_mean) | |
| def _load_from_state_dict( | |
| self, | |
| state_dict, | |
| prefix, | |
| local_metadata, | |
| strict, | |
| missing_keys, | |
| unexpected_keys, | |
| error_msgs, | |
| ): | |
| # Checkpoints frozen before modality-subset anchors did not serialize this buffer. | |
| key = f"{prefix}input_indices" | |
| if key not in state_dict: | |
| state_dict[key] = self.input_indices | |
| super()._load_from_state_dict( | |
| state_dict, | |
| prefix, | |
| local_metadata, | |
| strict, | |
| missing_keys, | |
| unexpected_keys, | |
| error_msgs, | |
| ) | |
| def forward(self, action_embedding, action_known): | |
| selected = action_embedding.index_select(1, self.input_indices) | |
| standardized = (selected - self.x_mean) / self.x_std | |
| predicted = standardized @ self.weights @ self.components + self.y_mean | |
| return torch.where(action_known.unsqueeze(1), predicted, self.y_mean.unsqueeze(0)) | |
| class AnchoredPopulationDynamics(PopulationDynamics): | |
| """Separate a transferable mean action effect from population heterogeneity.""" | |
| def __init__( | |
| self, | |
| *args, | |
| effect_anchor, | |
| anchor_gain_max=1.0, | |
| mean_residual_max_ratio=0.0, | |
| **kwargs, | |
| ): | |
| super().__init__(*args, **kwargs) | |
| assert anchor_gain_max >= 1 | |
| assert 0 <= mean_residual_max_ratio <= 1 | |
| self.effect_anchor = FrozenLowRankEffectAnchor(effect_anchor) | |
| self.anchor_gain_max = float(anchor_gain_max) | |
| self.mean_residual_max_ratio = float(mean_residual_max_ratio) | |
| cell_dim = self.effect_anchor.y_mean.numel() | |
| ffn_dim = self.interaction[1].out_features | |
| dropout = self.transition[0].ffn[2].p | |
| self.mean_residual = nn.Sequential( | |
| nn.LayerNorm(cell_dim), | |
| nn.Linear(cell_dim, ffn_dim), | |
| nn.GELU(), | |
| nn.Dropout(dropout), | |
| nn.Linear(ffn_dim, cell_dim), | |
| ) | |
| nn.init.zeros_(self.mean_residual[-1].weight) | |
| nn.init.zeros_(self.mean_residual[-1].bias) | |
| self.anchor_gain = nn.Sequential(nn.LayerNorm(cell_dim), nn.Linear(cell_dim, 1)) | |
| nn.init.zeros_(self.anchor_gain[-1].weight) | |
| nn.init.zeros_(self.anchor_gain[-1].bias) | |
| self.minimum_anchor_norm = float( | |
| effect_anchor["report"]["training_null_effect_threshold"] | |
| ) | |
| self.residual_gate_threshold = None | |
| def configure_residual_gate(self, checkpoint): | |
| """Attach a source-control-calibrated confidence gate without changing weights.""" | |
| assert checkpoint["format_version"] == 1 | |
| assert checkpoint["architecture"] == "control_population_residual_gate" | |
| for name in ("center", "scale"): | |
| value = checkpoint[name].detach().float() | |
| assert value.shape == self.effect_anchor.y_mean.shape | |
| self.register_buffer(f"residual_gate_{name}", value, persistent=False) | |
| self.residual_gate_threshold = float(checkpoint["threshold"]) | |
| self.residual_gate_temperature = float(checkpoint["temperature"]) | |
| assert self.residual_gate_temperature > 0 | |
| def residual_gate_confidence(self, control): | |
| """Return one smooth residual-retention confidence per control population.""" | |
| assert self.residual_gate_threshold is not None | |
| score = ((control.mean(1) - self.residual_gate_center) / self.residual_gate_scale).square().mean(1) | |
| excess = torch.relu(score - self.residual_gate_threshold) | |
| return torch.exp(-excess / self.residual_gate_temperature) | |
| def _scaled_anchor(self, action, anchor): | |
| if self.anchor_gain_max == 1: | |
| return anchor | |
| log_limit = math.log(self.anchor_gain_max) | |
| gain = torch.exp(torch.tanh(self.anchor_gain(action)) * log_limit) | |
| return anchor * gain | |
| def _bounded_mean_residual(self, action, anchor): | |
| if self.mean_residual_max_ratio == 0: | |
| return torch.zeros_like(anchor) | |
| residual = self.mean_residual(action) | |
| residual_norm = torch.linalg.vector_norm(residual, dim=1, keepdim=True) | |
| anchor_norm = torch.linalg.vector_norm(anchor, dim=1, keepdim=True).clamp_min( | |
| self.minimum_anchor_norm | |
| ) | |
| maximum = self.mean_residual_max_ratio * anchor_norm | |
| scale = torch.minimum(torch.ones_like(residual_norm), maximum / residual_norm.clamp_min(1e-12)) | |
| return residual * scale | |
| def forward(self, control, action_embedding, action_known): | |
| if self.context_mode == "set_transformer": | |
| encoded = self.context_blocks(control) | |
| query = self.pool_query.unsqueeze(0).expand(control.shape[0], -1, -1) | |
| context, _ = self.pool(query, encoded, encoded, need_weights=False) | |
| context = self.context_output(context.squeeze(1)) | |
| elif self.context_mode == "mean": | |
| context = self.context_output(control.mean(1)) | |
| else: | |
| context = torch.zeros_like(control[:, 0]) | |
| projected = ( | |
| self.action_projection(action_embedding, context) | |
| if self.action_context_conditioned | |
| else self.action_projection(action_embedding) | |
| ) | |
| action = torch.where( | |
| action_known.unsqueeze(1), projected, self.unknown_action.unsqueeze(0) | |
| ) | |
| interaction = self.interaction( | |
| torch.cat((context, action, context * action, torch.abs(context - action)), dim=1) | |
| ) | |
| states = control | |
| for block in self.transition: | |
| states = block(states, interaction) | |
| population_residual = self.delta(states) | |
| population_residual = population_residual - population_residual.mean(1, keepdim=True) | |
| if self.residual_gate_threshold is not None: | |
| population_residual *= self.residual_gate_confidence(control)[:, None, None] | |
| anchor = self._scaled_anchor( | |
| action, self.effect_anchor(action_embedding, action_known) | |
| ) | |
| mean_residual = self._bounded_mean_residual(action, anchor) | |
| return control + anchor.unsqueeze(1) + mean_residual.unsqueeze(1) + population_residual | |
| def dynamics_loss(predicted, observed, control, config, median_distance, null_threshold): | |
| """Locked unpaired Sinkhorn + MMD + direction + magnitude population objective.""" | |
| blur = config["sinkhorn_blur_ratio"] * median_distance | |
| sinkhorn = SamplesLoss( | |
| "sinkhorn", | |
| p=config["sinkhorn_p"], | |
| blur=blur, | |
| debias=config["sinkhorn_debias"], | |
| backend=config["sinkhorn_backend"], | |
| )(predicted, observed).mean() | |
| bandwidth = config["mmd_bandwidth_ratio"] * median_distance | |
| xx = torch.exp(-torch.cdist(predicted, predicted).square() / (2 * bandwidth**2)).mean((1, 2)) | |
| yy = torch.exp(-torch.cdist(observed, observed).square() / (2 * bandwidth**2)).mean((1, 2)) | |
| xy = torch.exp(-torch.cdist(predicted, observed).square() / (2 * bandwidth**2)).mean((1, 2)) | |
| mmd = (xx + yy - 2 * xy).clamp_min(0).mean() | |
| true_effect = observed.mean(1) - control.mean(1) | |
| predicted_effect = predicted.mean(1) - control.mean(1) | |
| true_magnitude = torch.linalg.vector_norm(true_effect, dim=1) | |
| predicted_magnitude = torch.linalg.vector_norm(predicted_effect, dim=1) | |
| active = true_magnitude >= null_threshold | |
| direction = ( | |
| ( | |
| 1 | |
| - F.cosine_similarity( | |
| predicted_effect[active], | |
| true_effect[active], | |
| eps=max(null_threshold, 1e-8), | |
| ) | |
| ).mean() | |
| if active.any() | |
| else predicted.sum() * 0 | |
| ) | |
| magnitude = F.smooth_l1_loss(predicted_magnitude, true_magnitude) | |
| weights = config["weights"] | |
| total = ( | |
| weights["sinkhorn"] * sinkhorn | |
| + weights["mmd"] * mmd | |
| + weights["direction"] * direction | |
| + weights["magnitude"] * magnitude | |
| ) | |
| return { | |
| "loss": total, | |
| "sinkhorn": sinkhorn, | |
| "mmd": mmd, | |
| "direction": direction, | |
| "magnitude": magnitude, | |
| "direction_active": active.float().mean(), | |
| } | |
| def dynamics_objective(predicted, observed, control, config, statistics): | |
| """Switch only the training criterion for the locked pseudo-paired comparator.""" | |
| objective = config.get("objective", "unpaired_distribution") | |
| if objective == "pseudo_paired_mse": | |
| pointwise = F.mse_loss(predicted, observed) | |
| return {"loss": pointwise, "pointwise_mse": pointwise} | |
| assert objective == "unpaired_distribution" | |
| return dynamics_loss( | |
| predicted, | |
| observed, | |
| control, | |
| config["loss"], | |
| statistics["normalization"]["median_training_latent_distance"], | |
| statistics["direction"]["null_effect_threshold"], | |
| ) | |
| def build_dynamics_model(config): | |
| """Build the primary model or an explicitly declared post-primary revision.""" | |
| model = config["model"] | |
| assert model["cell_dim"] == model["action_dim"] | |
| assert model["predicted_population_size"] == config["data"]["population_size"] | |
| assert model["cell_line_id"] is False and model["stochastic_noise"] is False | |
| arguments = ( | |
| model["cell_dim"], | |
| model["action_input_dim"], | |
| model["action_dim"], | |
| model["context_blocks"], | |
| model["transition_blocks"], | |
| model["heads"], | |
| model["ffn_dim"], | |
| model["dropout"], | |
| model.get("context_mode", "set_transformer"), | |
| ) | |
| fusion = { | |
| "action_modalities": model.get("action_modalities"), | |
| "action_modality_dropout": model.get("action_modality_dropout", 0.0), | |
| "action_context_conditioned": model.get("action_context_conditioned", False), | |
| "action_modality_availability": model.get("action_modality_availability", False), | |
| } | |
| architecture = model.get("architecture", "population_dynamics") | |
| if architecture == "population_dynamics": | |
| return PopulationDynamics(*arguments, **fusion) | |
| assert architecture == "anchored_decomposed_population_dynamics" | |
| anchor = config["effect_anchor"] | |
| path = Path(anchor["output_path"]) | |
| assert (path.stat().st_size, file_sha256(path)) == ( | |
| anchor["output_bytes"], | |
| anchor["output_sha256"], | |
| ) | |
| manifest = json.loads(Path(anchor["manifest_path"]).read_text()) | |
| declared = manifest.pop("manifest_sha256") | |
| assert declared == anchor["manifest_sha256"] == sha256( | |
| json.dumps(manifest, sort_keys=True, separators=(",", ":")).encode() | |
| ).hexdigest() | |
| assert manifest["artifact"]["sha256"] == anchor["output_sha256"] | |
| checkpoint = torch.load(path, map_location="cpu", weights_only=True) | |
| return AnchoredPopulationDynamics( | |
| *arguments, | |
| effect_anchor=checkpoint, | |
| anchor_gain_max=model["anchor_gain_max"], | |
| mean_residual_max_ratio=model["mean_residual_max_ratio"], | |
| **fusion, | |
| ) | |
| def dynamics_ablation_configs(path="configs/ablations.yaml"): | |
| """Materialize the fixed matched-capacity dynamics ablations from one pinned base.""" | |
| path = Path(path) | |
| specification = yaml.safe_load(path.read_text()) | |
| base_path = Path(specification["base_config_path"]) | |
| assert file_sha256(base_path) == specification["base_config_sha256"] | |
| base = yaml.safe_load(base_path.read_text()) | |
| configs = {} | |
| for name, changes in specification["experiments"].items(): | |
| config = deepcopy(base) | |
| config["model"]["context_mode"] = changes["context_mode"] | |
| config["loss"]["weights"]["direction"] = changes["direction_weight"] | |
| config["training"]["output_directory"] = changes["output_directory"] | |
| config["training"]["resume_from"] = None | |
| config["ablation"] = { | |
| "name": name, | |
| "hypothesis": changes["hypothesis"], | |
| "base_config_path": str(base_path), | |
| "base_config_sha256": specification["base_config_sha256"], | |
| } | |
| configs[name] = config | |
| return configs, specification | |
| def dynamics_replication_configs(path="configs/stage2_replication.yaml"): | |
| """Vary only Stage 2 stochastic seeds while retaining the frozen primary protocol.""" | |
| path = Path(path) | |
| specification = yaml.safe_load(path.read_text()) | |
| base_path = Path(specification["base_config_path"]) | |
| assert file_sha256(base_path) == specification["base_config_sha256"] | |
| base = yaml.safe_load(base_path.read_text()) | |
| split = json.loads(Path(base["inputs"]["replogle_manifest_path"]).read_text())["targets"][ | |
| "split" | |
| ] | |
| assert split["seed"] == base["seed"] == specification["target_split_seed"] | |
| configs = {} | |
| for entry in specification["model_seeds"]: | |
| seed = entry["seed"] | |
| assert seed != specification["target_split_seed"] and seed not in configs | |
| config = deepcopy(base) | |
| config["seed"] = seed | |
| config["training"]["output_directory"] = entry["output_directory"] | |
| config["training"]["resume_from"] = None | |
| config["replication"] = { | |
| "model_and_sampling_seed": seed, | |
| "target_split_seed": specification["target_split_seed"], | |
| "base_config_path": str(base_path), | |
| "base_config_sha256": specification["base_config_sha256"], | |
| } | |
| configs[seed] = config | |
| return configs, specification | |
| def _normalized_role_effects(base, roles): | |
| """Compute batch-matched latent effects for explicitly named K562 roles.""" | |
| inputs = base["inputs"] | |
| manifest = json.loads(Path(inputs["dynamics_manifest_path"]).read_text()) | |
| normalization = manifest["normalization"] | |
| mean = np.asarray(normalization["latent_mean"], dtype=np.float32) | |
| scale = np.asarray(normalization["latent_std"], dtype=np.float32) * normalization[ | |
| "dimension_scale" | |
| ] | |
| with h5py.File(inputs["latent_cache_path"], "r") as cache: | |
| cache_roles = cache["role"].asstr()[:] | |
| targets = cache["target"].asstr()[:] | |
| batches = cache["source_batch"].asstr()[:] | |
| contexts = cache["context"].asstr()[:] | |
| control_indices = np.flatnonzero(cache_roles == base["data"]["control_role"]) | |
| assert set(contexts[control_indices]) == {base["data"]["context"]} == {"K562"} | |
| control_means = { | |
| batch: ((cache["latent"][control_indices[batches[control_indices] == batch]] - mean) / scale).mean(0) | |
| for batch in sorted(set(batches[control_indices])) | |
| } | |
| effects = {} | |
| target_ids = {} | |
| for role in roles: | |
| role_indices = np.flatnonzero(cache_roles == role) | |
| assert len(role_indices) and set(contexts[role_indices]) == {"K562"} | |
| target_ids[role] = sorted(set(targets[role_indices])) | |
| effects[role] = {} | |
| for target in target_ids[role]: | |
| selected = role_indices[targets[role_indices] == target] | |
| outcome = ((cache["latent"][selected] - mean) / scale).mean(0) | |
| counts = Counter(batches[selected]) | |
| matched_control = sum( | |
| count * control_means[batch] for batch, count in counts.items() | |
| ) / len(selected) | |
| effects[role][target] = (outcome - matched_control).astype(np.float32) | |
| return effects, target_ids, manifest | |
| def _action_overridden_config(base, specification): | |
| """Replace only the frozen action input from a self-hashed cache manifest.""" | |
| if "action_manifest_path" not in specification: | |
| return base | |
| manifest = json.loads(Path(specification["action_manifest_path"]).read_text()) | |
| declared = manifest.pop("manifest_sha256") | |
| assert declared == specification["action_manifest_sha256"] == sha256( | |
| json.dumps(manifest, sort_keys=True, separators=(",", ":")).encode() | |
| ).hexdigest() | |
| artifact = manifest["artifact"] | |
| assert (Path(artifact["path"]).stat().st_size, file_sha256(artifact["path"])) == ( | |
| artifact["bytes"], artifact["sha256"] | |
| ) | |
| base["inputs"].update( | |
| { | |
| "action_cache_path": artifact["path"], | |
| "action_cache_bytes": artifact["bytes"], | |
| "action_cache_sha256": artifact["sha256"], | |
| "action_manifest_path": specification["action_manifest_path"], | |
| "action_manifest_sha256": declared, | |
| } | |
| ) | |
| base["model"]["action_input_dim"] = artifact["input_dim"] | |
| base["model"]["action_modalities"] = artifact["modality_dims"] | |
| base["model"]["action_modality_availability"] = artifact.get( | |
| "modality_availability", False | |
| ) | |
| return base | |
| def effect_anchor_input_indices(action, anchor): | |
| """Select declared action modalities and their availability bits for an anchor.""" | |
| modalities = action.get("modalities") | |
| modality_dims = action.get("modality_dims") | |
| selected_names = anchor.get("input_modalities") | |
| if selected_names is None: | |
| return torch.arange(action["embedding"].shape[1]) | |
| assert modalities is not None and modality_dims is not None | |
| assert len(modalities) == len(modality_dims) | |
| assert len(selected_names) == len(set(selected_names)) | |
| selected = set(selected_names) | |
| assert selected == selected.intersection(modalities) | |
| feature_width = sum(modality_dims) | |
| availability_width = len(modality_dims) if action.get("modality_availability", False) else 0 | |
| assert action["embedding"].shape[1] == feature_width + availability_width | |
| indices, offset = [], 0 | |
| selected_positions = [] | |
| for position, (name, width) in enumerate(zip(modalities, modality_dims, strict=True)): | |
| if name in selected: | |
| indices.extend(range(offset, offset + width)) | |
| selected_positions.append(position) | |
| offset += width | |
| if availability_width: | |
| indices.extend(feature_width + position for position in selected_positions) | |
| return torch.tensor(indices, dtype=torch.long) | |
| def prepare_effect_anchor(path="configs/anchored_dynamics.yaml"): | |
| """Fit the frozen ESM-to-latent anchor without test or RPE1 outcomes.""" | |
| path = Path(path) | |
| specification = yaml.safe_load(path.read_text()) | |
| base_path = Path(specification["base_config_path"]) | |
| assert file_sha256(base_path) == specification["base_config_sha256"] | |
| base = _action_overridden_config(yaml.safe_load(base_path.read_text()), specification) | |
| anchor = specification["effect_anchor"] | |
| roles = (anchor["fit_outcome_role"], anchor["selection_outcome_role"]) | |
| assert roles == ("dynamics_train", "perturbation_ood_validation") | |
| effects, targets, dynamics_manifest = _normalized_role_effects(base, roles) | |
| action = torch.load(base["inputs"]["action_cache_path"], map_location="cpu", weights_only=True) | |
| input_indices = effect_anchor_input_indices(action, anchor) | |
| action_map = { | |
| target: ( | |
| action["embedding"][index].index_select(0, input_indices).numpy(), | |
| bool(action["known"][index]), | |
| ) | |
| for index, target in enumerate(action["targets"]) | |
| } | |
| train = effects[roles[0]] | |
| known_targets = [target for target in targets[roles[0]] if action_map[target][1]] | |
| x = np.stack([action_map[target][0] for target in known_targets]).astype(np.float64) | |
| y = np.stack([train[target] for target in known_targets]).astype(np.float64) | |
| x_mean, x_std = x.mean(0), x.std(0).clip(1e-8) | |
| y_mean = y.mean(0) | |
| _, _, components = np.linalg.svd(y - y_mean, full_matrices=False) | |
| components = components[: min(anchor["rank"], len(known_targets))] | |
| standardized = (x - x_mean) / x_std | |
| scores = (y - y_mean) @ components.T | |
| gram, cross = standardized.T @ standardized, standardized.T @ scores | |
| validation = effects[roles[1]] | |
| validation_targets = targets[roles[1]] | |
| validation_x = np.stack([action_map[target][0] for target in validation_targets]) | |
| validation_y = np.stack([validation[target] for target in validation_targets]) | |
| candidates = [] | |
| for alpha in anchor["ridge_candidates"]: | |
| weights = np.linalg.solve(gram + alpha * np.eye(gram.shape[0]), cross) | |
| prediction = ((validation_x - x_mean) / x_std) @ weights @ components + y_mean | |
| correlations = [ | |
| float(np.corrcoef(predicted, observed)[0, 1]) | |
| for predicted, observed in zip(prediction, validation_y) | |
| ] | |
| magnitude_error = np.abs( | |
| np.linalg.norm(prediction, axis=1) - np.linalg.norm(validation_y, axis=1) | |
| ) | |
| candidates.append( | |
| { | |
| "alpha": float(alpha), | |
| "mse": float(np.mean(np.square(prediction - validation_y))), | |
| "mean_effect_pearson": float(np.mean(correlations)), | |
| "mean_magnitude_absolute_error": float(np.mean(magnitude_error)), | |
| "weights": weights, | |
| } | |
| ) | |
| selected = min(candidates, key=lambda item: (item["mse"], item["alpha"])) | |
| report = { | |
| "fit_outcome_role": roles[0], | |
| "selection_outcome_role": roles[1], | |
| "fit_targets": len(train), | |
| "fit_targets_with_known_action": len(known_targets), | |
| "selection_targets": len(validation_targets), | |
| "rank": len(components), | |
| "input_modalities": anchor.get("input_modalities", action.get("modalities")), | |
| "input_dimensions": len(input_indices), | |
| "input_indices_sha256": sha256( | |
| json.dumps(input_indices.tolist(), separators=(",", ":")).encode() | |
| ).hexdigest(), | |
| "selected_ridge": selected["alpha"], | |
| "selection_mse": selected["mse"], | |
| "selection_mean_effect_pearson": selected["mean_effect_pearson"], | |
| "selection_mean_magnitude_absolute_error": selected[ | |
| "mean_magnitude_absolute_error" | |
| ], | |
| "ridge_candidates": [item["alpha"] for item in candidates], | |
| "ridge_validation_mse": [item["mse"] for item in candidates], | |
| "training_null_effect_threshold": dynamics_manifest["direction"][ | |
| "null_effect_threshold" | |
| ], | |
| "target_sha256": { | |
| role: sha256("\n".join(targets[role]).encode()).hexdigest() for role in roles | |
| }, | |
| "leakage": { | |
| "contexts": ["K562"], | |
| "sealed_test_outcomes_used": False, | |
| "rpe1_outcomes_used": False, | |
| }, | |
| } | |
| checkpoint = { | |
| "format_version": 1, | |
| "architecture": anchor.get( | |
| "checkpoint_architecture", "esm2_low_rank_latent_effect_ridge" | |
| ), | |
| "x_mean": torch.from_numpy(x_mean.astype(np.float32)), | |
| "x_std": torch.from_numpy(x_std.astype(np.float32)), | |
| "y_mean": torch.from_numpy(y_mean.astype(np.float32)), | |
| "components": torch.from_numpy(components.astype(np.float32)), | |
| "weights": torch.from_numpy(selected["weights"].astype(np.float32)), | |
| "input_indices": input_indices, | |
| "report": report, | |
| } | |
| output = Path(anchor["output_path"]) | |
| output.parent.mkdir(parents=True, exist_ok=True) | |
| torch.save(checkpoint, output) | |
| artifact = {"path": str(output), "bytes": output.stat().st_size, "sha256": file_sha256(output)} | |
| manifest = { | |
| "format_version": 1, | |
| "architecture": checkpoint["architecture"], | |
| "artifact": artifact, | |
| "fit": report, | |
| "source": { | |
| "base_config_path": str(base_path), | |
| "base_config_sha256": specification["base_config_sha256"], | |
| "latent_cache_sha256": base["inputs"]["latent_cache_sha256"], | |
| "action_cache_sha256": base["inputs"]["action_cache_sha256"], | |
| "dynamics_manifest_sha256": dynamics_manifest["manifest_sha256"], | |
| }, | |
| } | |
| manifest["manifest_sha256"] = sha256( | |
| json.dumps(manifest, sort_keys=True, separators=(",", ":")).encode() | |
| ).hexdigest() | |
| Path(anchor["manifest_path"]).write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n") | |
| return manifest | |
| def anchored_dynamics_configs(path="configs/anchored_dynamics.yaml"): | |
| """Materialize the preregistered validation-only anchored candidates.""" | |
| path = Path(path) | |
| specification = yaml.safe_load(path.read_text()) | |
| base_path = Path(specification["base_config_path"]) | |
| assert file_sha256(base_path) == specification["base_config_sha256"] | |
| base = _action_overridden_config(yaml.safe_load(base_path.read_text()), specification) | |
| anchor = specification["effect_anchor"] | |
| assert anchor["output_sha256"] != "PENDING" and anchor["manifest_sha256"] != "PENDING" | |
| artifact_path = Path(anchor["output_path"]) | |
| assert (artifact_path.stat().st_size, file_sha256(artifact_path)) == ( | |
| anchor["output_bytes"], | |
| anchor["output_sha256"], | |
| ) | |
| manifest = json.loads(Path(anchor["manifest_path"]).read_text()) | |
| declared = manifest.pop("manifest_sha256") | |
| assert declared == anchor["manifest_sha256"] == sha256( | |
| json.dumps(manifest, sort_keys=True, separators=(",", ":")).encode() | |
| ).hexdigest() | |
| configs = {} | |
| for name, experiment in specification["experiments"].items(): | |
| config = deepcopy(base) | |
| config["seed"] = specification["seed"] | |
| config["effect_anchor"] = deepcopy(anchor) | |
| config["model"]["architecture"] = "anchored_decomposed_population_dynamics" | |
| config["model"]["anchor_gain_max"] = experiment["anchor_gain_max"] | |
| config["model"]["mean_residual_max_ratio"] = experiment[ | |
| "mean_residual_max_ratio" | |
| ] | |
| config["model"]["action_modality_dropout"] = experiment.get( | |
| "action_modality_dropout", 0.0 | |
| ) | |
| config["model"]["action_context_conditioned"] = experiment.get( | |
| "action_context_conditioned", False | |
| ) | |
| config["training"]["output_directory"] = experiment["output_directory"] | |
| config["training"]["resume_from"] = None | |
| config["revision"] = { | |
| **deepcopy(specification["revision"]), | |
| "candidate": name, | |
| "anchor_gain_max": experiment["anchor_gain_max"], | |
| "mean_residual_max_ratio": experiment["mean_residual_max_ratio"], | |
| "action_modality_dropout": experiment.get("action_modality_dropout", 0.0), | |
| "action_context_conditioned": experiment.get("action_context_conditioned", False), | |
| "target_split_seed": base["seed"], | |
| } | |
| configs[name] = config | |
| return configs, specification | |
| def anchored_selected_entry(training_manifest, selection_manifest): | |
| """Resolve the frozen candidate while proving selection preceded sealed evaluation.""" | |
| assert training_manifest["protocol"]["sealed_test_outcomes_used_for_fit_or_selection"] is False | |
| assert training_manifest["protocol"]["rpe1_perturbed_outcomes_used_for_fit_or_selection"] is False | |
| assert selection_manifest["leakage"] == { | |
| "context": "K562", | |
| "outcome_role": "perturbation_ood_validation", | |
| "rpe1_outcomes_used": False, | |
| "sealed_test_outcomes_used": False, | |
| } | |
| selected = selection_manifest["selected"] | |
| entry = training_manifest["artifacts"]["candidates"][selected["candidate"]] | |
| assert entry["best_checkpoint"] == { | |
| key: selected[key] for key in ("bytes", "path", "sha256") | |
| } | |
| assert (selected["best_validation_epoch"], selected["best_validation_loss"]) == ( | |
| entry["full_run"]["best_validation_epoch"], | |
| entry["full_run"]["best_validation_loss"], | |
| ) | |
| return selected["candidate"], entry | |
| def select_multiteacher_candidate(training_manifest, specification): | |
| """Apply the frozen validation-loss margin without consulting test outcomes.""" | |
| protocol = training_manifest["protocol"] | |
| assert protocol["sealed_test_outcomes_used_for_fit_or_selection"] is False | |
| assert protocol["rpe1_perturbed_outcomes_used_for_fit_or_selection"] is False | |
| rule = specification["selection"] | |
| assert rule["viewed_test_outcomes_used"] is False | |
| assert rule["checkpoint_rule"] == "minimum_original_latent_validation_loss" | |
| fallback = rule["fallback_candidate"] | |
| candidates = training_manifest["artifacts"]["candidates"] | |
| assert fallback in candidates | |
| alternatives = [ | |
| name | |
| for name, experiment in specification["experiments"].items() | |
| if experiment.get("action_modality_dropout", 0.0) > 0 | |
| ] | |
| assert len(alternatives) == 1 and alternatives[0] in candidates | |
| alternative = alternatives[0] | |
| fallback_loss = candidates[fallback]["full_run"]["best_validation_loss"] | |
| alternative_loss = candidates[alternative]["full_run"]["best_validation_loss"] | |
| improvement = fallback_loss - alternative_loss | |
| selected = ( | |
| alternative | |
| if improvement >= rule["dropout_minimum_loss_improvement"] | |
| else fallback | |
| ) | |
| return selected, improvement | |
| def prepare_dynamics_manifest(config, config_path="configs/dynamics.yaml"): | |
| """Fit state-space statistics without reading validation, test, or RPE1 outcomes.""" | |
| inputs, data, normalization = config["inputs"], config["data"], config["normalization"] | |
| for kind in ("latent", "action"): | |
| path = Path(inputs[f"{kind}_cache_path"]) | |
| assert (path.stat().st_size, file_sha256(path)) == ( | |
| inputs[f"{kind}_cache_bytes"], | |
| inputs[f"{kind}_cache_sha256"], | |
| ) | |
| for kind in ("action", "replogle"): | |
| manifest = json.loads(Path(inputs[f"{kind}_manifest_path"]).read_text()) | |
| assert manifest["manifest_sha256"] == inputs[f"{kind}_manifest_sha256"] | |
| with h5py.File(inputs["latent_cache_path"], "r") as cache: | |
| roles = cache["role"].asstr()[:] | |
| targets = cache["target"].asstr()[:] | |
| contexts = cache["context"].asstr()[:] | |
| batches = cache["source_batch"].asstr()[:] | |
| cell_ids = cache["cell_id"].asstr()[:] | |
| fit_indices = np.flatnonzero(np.isin(roles, normalization["fit_roles"])) | |
| fit_latents = cache["latent"][fit_indices] | |
| mean = fit_latents.mean(0, dtype=np.float64) | |
| variance = np.square(fit_latents.astype(np.float64) - mean).mean(0) | |
| std = np.sqrt(variance) | |
| assert np.isfinite(mean).all() and np.isfinite(std).all() and (std > 0).all() | |
| dimension_scale = math.sqrt(fit_latents.shape[1]) | |
| rng = np.random.default_rng(config["seed"]) | |
| distance_indices = np.sort( | |
| rng.choice(fit_indices, normalization["distance_sample_cells"], replace=False) | |
| ) | |
| distance_latents = (cache["latent"][distance_indices] - mean) / std / dimension_scale | |
| distances = np.linalg.norm( | |
| distance_latents - distance_latents[rng.permutation(len(distance_latents))], axis=1 | |
| ) | |
| median_distance = float(np.median(distances)) | |
| control_indices = np.flatnonzero(roles == data["control_role"]) | |
| assert set(contexts[control_indices]) == {data["context"]} | |
| control_latents = (cache["latent"][control_indices] - mean) / std / dimension_scale | |
| control_batches = batches[control_indices] | |
| control_means = { | |
| batch: control_latents[control_batches == batch].mean(0) | |
| for batch in sorted(set(control_batches)) | |
| } | |
| train_indices = np.flatnonzero(roles == data["train_outcome_role"]) | |
| validation_indices = np.flatnonzero(roles == data["validation_outcome_role"]) | |
| assert set(contexts[train_indices]) == set(contexts[validation_indices]) == { | |
| data["context"] | |
| } | |
| train_latents = (cache["latent"][train_indices] - mean) / std / dimension_scale | |
| train_targets, train_batches = targets[train_indices], batches[train_indices] | |
| effect_norms = {} | |
| for target in sorted(set(train_targets)): | |
| selected = train_targets == target | |
| batch_counts = Counter(train_batches[selected]) | |
| matched_control = sum( | |
| count * control_means[batch] for batch, count in batch_counts.items() | |
| ) / selected.sum() | |
| effect_norms[target] = float( | |
| np.linalg.norm(train_latents[selected].mean(0) - matched_control) | |
| ) | |
| threshold = float( | |
| np.quantile( | |
| list(effect_norms.values()), normalization["null_effect_quantile"], method="linear" | |
| ) | |
| ) | |
| replogle = json.loads(Path(inputs["replogle_manifest_path"]).read_text()) | |
| split = replogle["targets"]["split"]["targets"] | |
| assert set(effect_norms) == set(split["train"]) | |
| assert set(targets[validation_indices]) == set(split["validation"]) | |
| report = { | |
| "format_version": 1, | |
| "config_sha256": file_sha256(config_path), | |
| "inputs": { | |
| key: value for key, value in inputs.items() if key.endswith(("_bytes", "_sha256")) | |
| }, | |
| "normalization": { | |
| "fit_roles": normalization["fit_roles"], | |
| "fit_cells": len(fit_indices), | |
| "method": normalization["method"], | |
| "latent_mean": mean.tolist(), | |
| "latent_std": std.tolist(), | |
| "dimension_scale": dimension_scale, | |
| "distance_sample_cells": len(distance_indices), | |
| "distance_sample_cell_ids_sha256": sha256( | |
| "\n".join(sorted(cell_ids[distance_indices])).encode() | |
| ).hexdigest(), | |
| "median_training_latent_distance": median_distance, | |
| }, | |
| "conditions": { | |
| "train": { | |
| "role": data["train_outcome_role"], | |
| "targets": len(set(targets[train_indices])), | |
| "cells": len(train_indices), | |
| "cells_per_target": dict(sorted(Counter(targets[train_indices]).items())), | |
| }, | |
| "validation": { | |
| "role": data["validation_outcome_role"], | |
| "targets": len(set(targets[validation_indices])), | |
| "cells": len(validation_indices), | |
| "cells_per_target": dict(sorted(Counter(targets[validation_indices]).items())), | |
| }, | |
| "controls": { | |
| "role": data["control_role"], | |
| "cells": len(control_indices), | |
| "batches": dict(sorted(Counter(control_batches).items())), | |
| }, | |
| }, | |
| "direction": { | |
| "null_effect_quantile": normalization["null_effect_quantile"], | |
| "null_effect_threshold": threshold, | |
| "training_effect_norms": effect_norms, | |
| "excluded_training_targets": sum(value < threshold for value in effect_norms.values()), | |
| }, | |
| "leakage": { | |
| "normalization_roles": sorted(set(roles[fit_indices])), | |
| "outcome_context": sorted(set(contexts[train_indices])), | |
| "validation_used_for_statistics": False, | |
| "rpe1_outcomes_used_for_statistics": False, | |
| "sealed_test_outcomes_used_for_statistics": False, | |
| }, | |
| "runtime": {"numpy": np.__version__, "h5py": version("h5py")}, | |
| } | |
| report["manifest_sha256"] = sha256( | |
| json.dumps(report, sort_keys=True, separators=(",", ":")).encode() | |
| ).hexdigest() | |
| Path(inputs["dynamics_manifest_path"]).write_text( | |
| json.dumps(report, indent=2, sort_keys=True) + "\n" | |
| ) | |
| return report | |
| def state_ablation_config(path): | |
| """Materialize a frozen alternative state space into the locked Stage 2 model.""" | |
| path = Path(path) | |
| specification = yaml.safe_load(path.read_text()) | |
| base_path = Path(specification["base_config_path"]) | |
| assert file_sha256(base_path) == specification["base_config_sha256"] | |
| protocol = json.loads(Path(specification["specification_manifest_path"]).read_text()) | |
| protocol_sha256 = protocol.pop("manifest_sha256") | |
| assert protocol_sha256 == specification["specification_manifest_sha256"] == sha256( | |
| json.dumps(protocol, sort_keys=True, separators=(",", ":")).encode() | |
| ).hexdigest() | |
| state_manifest_path = Path(specification["cache_manifest_path"]) | |
| state_manifest = json.loads(state_manifest_path.read_text()) | |
| declared = state_manifest.pop("manifest_sha256") | |
| assert declared == sha256( | |
| json.dumps(state_manifest, sort_keys=True, separators=(",", ":")).encode() | |
| ).hexdigest() | |
| artifact = state_manifest["artifact"] | |
| assert (Path(artifact["path"]).stat().st_size, file_sha256(artifact["path"])) == ( | |
| artifact["bytes"], | |
| artifact["sha256"], | |
| ) | |
| assert artifact["cell_dim"] == yaml.safe_load(base_path.read_text())["model"]["cell_dim"] | |
| assert state_manifest["source"]["config_sha256"] == file_sha256(path) | |
| assert ( | |
| state_manifest["source"]["specification_manifest_sha256"] | |
| == specification["specification_manifest_sha256"] | |
| ) | |
| dynamics_manifest = json.loads(Path(specification["dynamics_manifest_path"]).read_text()) | |
| dynamics_sha256 = dynamics_manifest.pop("manifest_sha256") | |
| assert dynamics_sha256 == state_manifest["source"]["dynamics_manifest_sha256"] == sha256( | |
| json.dumps(dynamics_manifest, sort_keys=True, separators=(",", ":")).encode() | |
| ).hexdigest() | |
| assert dynamics_manifest["config_sha256"] == file_sha256(path) | |
| assert dynamics_manifest["inputs"]["latent_cache_sha256"] == artifact["sha256"] | |
| config = yaml.safe_load(base_path.read_text()) | |
| config["inputs"].update( | |
| { | |
| "latent_cache_path": artifact["path"], | |
| "latent_cache_bytes": artifact["bytes"], | |
| "latent_cache_sha256": artifact["sha256"], | |
| "latent_manifest_path": str(state_manifest_path), | |
| "latent_manifest_sha256": declared, | |
| "dynamics_manifest_path": specification["dynamics_manifest_path"], | |
| } | |
| ) | |
| config["training"]["output_directory"] = specification["output_directory"] | |
| config["training"]["resume_from"] = None | |
| config["ablation"] = { | |
| "name": specification["name"], | |
| "hypothesis": specification["hypothesis"], | |
| "base_config_path": str(base_path), | |
| "base_config_sha256": specification["base_config_sha256"], | |
| "cache_manifest_sha256": declared, | |
| } | |
| return config, specification, {**state_manifest, "manifest_sha256": declared} | |
| def learned_target_id_config(path="configs/learned_target_id.yaml"): | |
| """Materialize the categorical-action comparator from its pinned cache manifest.""" | |
| path = Path(path) | |
| specification = yaml.safe_load(path.read_text()) | |
| base_path = Path(specification["base_config_path"]) | |
| assert file_sha256(base_path) == specification["base_config_sha256"] | |
| base = yaml.safe_load(base_path.read_text()) | |
| cache_manifest_path = Path(specification["cache_manifest_path"]) | |
| cache_manifest = json.loads(cache_manifest_path.read_text()) | |
| declared = cache_manifest.pop("manifest_sha256") | |
| assert declared == sha256( | |
| json.dumps(cache_manifest, sort_keys=True, separators=(",", ":")).encode() | |
| ).hexdigest() | |
| artifact = cache_manifest["artifact"] | |
| assert artifact["path"] == specification["action_cache_path"] | |
| assert Path(artifact["path"]).stat().st_size == artifact["bytes"] | |
| assert file_sha256(artifact["path"]) == artifact["sha256"] | |
| assert cache_manifest["source"]["config_sha256"] == file_sha256(path) | |
| assert ( | |
| cache_manifest["source"]["specification_manifest_sha256"] | |
| == specification["specification_manifest_sha256"] | |
| ) | |
| config = deepcopy(base) | |
| config["inputs"].update( | |
| { | |
| "action_cache_path": artifact["path"], | |
| "action_cache_bytes": artifact["bytes"], | |
| "action_cache_sha256": artifact["sha256"], | |
| "action_manifest_path": str(cache_manifest_path), | |
| "action_manifest_sha256": declared, | |
| } | |
| ) | |
| config["model"]["action_input_dim"] = artifact["input_dim"] | |
| config["training"]["output_directory"] = specification["output_directory"] | |
| config["training"]["resume_from"] = None | |
| config["ablation"] = { | |
| "name": "learned_target_id", | |
| "hypothesis": "structured ESM-2 actions enable unseen-target transfer", | |
| "base_config_path": str(base_path), | |
| "base_config_sha256": specification["base_config_sha256"], | |
| "specification_manifest_sha256": specification["specification_manifest_sha256"], | |
| "cache_manifest_sha256": declared, | |
| } | |
| return config, specification, {**cache_manifest, "manifest_sha256": declared} | |
| def dynamics_provenance(config, config_path="configs/dynamics.yaml"): | |
| """Bind checkpoints to exact caches, manifests, configuration, code, and runtime.""" | |
| inputs = config["inputs"] | |
| for kind in ("latent", "action"): | |
| assert file_sha256(inputs[f"{kind}_cache_path"]) == inputs[f"{kind}_cache_sha256"] | |
| manifests = {} | |
| for kind in ("replogle", "action", "dynamics"): | |
| path = inputs[f"{kind}_manifest_path"] | |
| payload = json.loads(Path(path).read_text()) | |
| declared = payload.pop("manifest_sha256") | |
| assert declared == sha256( | |
| json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() | |
| ).hexdigest() | |
| if f"{kind}_manifest_sha256" in inputs: | |
| assert declared == inputs[f"{kind}_manifest_sha256"] | |
| manifests[kind] = declared | |
| if "latent_manifest_path" in inputs: | |
| payload = json.loads(Path(inputs["latent_manifest_path"]).read_text()) | |
| declared = payload.pop("manifest_sha256") | |
| assert declared == inputs["latent_manifest_sha256"] == sha256( | |
| json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() | |
| ).hexdigest() | |
| manifests["latent"] = declared | |
| cache_sha256 = { | |
| "latent": inputs["latent_cache_sha256"], | |
| "action": inputs["action_cache_sha256"], | |
| } | |
| if "effect_anchor" in config: | |
| anchor = config["effect_anchor"] | |
| assert file_sha256(anchor["output_path"]) == anchor["output_sha256"] | |
| payload = json.loads(Path(anchor["manifest_path"]).read_text()) | |
| declared = payload.pop("manifest_sha256") | |
| assert declared == anchor["manifest_sha256"] == sha256( | |
| json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() | |
| ).hexdigest() | |
| manifests["effect_anchor"] = declared | |
| cache_sha256["effect_anchor"] = anchor["output_sha256"] | |
| return { | |
| "config_sha256": file_sha256(config_path), | |
| "cache_sha256": cache_sha256, | |
| "manifest_sha256": manifests, | |
| "runtime_source_sha256": _runtime_source_hash(), | |
| "runtime_environment": _runtime_environment(), | |
| "geomloss": version("geomloss"), | |
| "git": _git_state(), | |
| } | |
| def _device_batch(batch, device): | |
| non_blocking = device.type == "cuda" | |
| return { | |
| key: batch[key].to(device, non_blocking=non_blocking) | |
| for key in ("control", "perturbed", "action", "action_known") | |
| } | |
| def validate_dynamics(model, dataset, config, device, max_batches=None): | |
| """Evaluate only frozen K562 perturbation-OOD validation conditions.""" | |
| was_training = model.training | |
| model.eval() | |
| dataset.set_epoch(-1) | |
| training = config["training"] | |
| loader = _data_loader( | |
| dataset, | |
| range(len(dataset)), | |
| training["batch_size"], | |
| training["num_workers"], | |
| config["seed"] + 1, | |
| training["pin_memory"] and device.type == "cuda", | |
| training["persistent_workers"], | |
| training["prefetch_factor"], | |
| ) | |
| statistics = dataset.manifest | |
| totals, conditions = {}, 0 | |
| for index, raw_batch in enumerate(loader): | |
| if max_batches is not None and index >= max_batches: | |
| break | |
| batch = _device_batch(raw_batch, device) | |
| predicted = model(batch["control"], batch["action"], batch["action_known"]) | |
| losses = dynamics_objective( | |
| predicted, batch["perturbed"], batch["control"], config, statistics | |
| ) | |
| size = len(raw_batch["target"]) | |
| conditions += size | |
| for name, value in losses.items(): | |
| totals[name] = totals.get(name, 0.0) + float(value.detach()) * size | |
| model.train(was_training) | |
| return {name: value / conditions for name, value in totals.items()} | |
| def train_dynamics( | |
| config, | |
| device, | |
| max_steps=None, | |
| validation_batches=None, | |
| config_path="configs/dynamics.yaml", | |
| ): | |
| """Train or exactly resume condition-weighted Stage 2 dynamics.""" | |
| training, data = config["training"], config["data"] | |
| assert training["optimizer"] == "AdamW" | |
| seed_everything(config["seed"], training["deterministic_algorithms"]) | |
| datasets = { | |
| mode: LatentPopulationDataset( | |
| config["inputs"]["latent_cache_path"], | |
| config["inputs"]["action_cache_path"], | |
| config["inputs"]["dynamics_manifest_path"], | |
| mode, | |
| data["population_size"], | |
| config["seed"], | |
| ) | |
| for mode in ("train", "validation") | |
| } | |
| model = build_dynamics_model(config).to(device) | |
| optimizer = torch.optim.AdamW( | |
| model.parameters(), lr=training["learning_rate"], weight_decay=training["weight_decay"] | |
| ) | |
| steps_per_epoch = math.ceil(len(datasets["train"]) / training["batch_size"]) | |
| total_steps = training["epochs"] * steps_per_epoch | |
| scheduler = torch.optim.lr_scheduler.LambdaLR( | |
| optimizer, | |
| learning_rate_schedule( | |
| total_steps, | |
| training["warmup_fraction"], | |
| training["minimum_learning_rate_fraction"], | |
| ), | |
| ) | |
| provenance = dynamics_provenance(config, config_path) | |
| configuration = deepcopy(config) | |
| output = Path(training["output_directory"]) | |
| output.mkdir(parents=True, exist_ok=True) | |
| latest, best = output / "latest.pt", output / "best.pt" | |
| state = initial_train_state() | |
| if training["resume_from"]: | |
| state, saved_configuration = load_checkpoint( | |
| training["resume_from"], model, optimizer, scheduler, provenance, device | |
| ) | |
| saved_configuration["training"]["resume_from"] = None | |
| current = deepcopy(configuration) | |
| current["training"]["resume_from"] = None | |
| assert saved_configuration == current | |
| if state["complete"]: | |
| model.load_state_dict(torch.load(best, map_location=device, weights_only=False)["model"]) | |
| return model, state, { | |
| "train_conditions": len(datasets["train"]), | |
| "validation_conditions": len(datasets["validation"]), | |
| "elapsed_seconds": 0.0, | |
| "checkpoint": str(latest), | |
| "best_checkpoint": str(best), | |
| "already_complete": True, | |
| } | |
| statistics = datasets["train"].manifest | |
| started = time.perf_counter() | |
| model.train() | |
| for epoch in range(state["epoch"], training["epochs"]): | |
| datasets["train"].set_epoch(epoch) | |
| order = epoch_order(range(len(datasets["train"])), config["seed"], epoch) | |
| start = state["batch_in_epoch"] * training["batch_size"] | |
| loader = _data_loader( | |
| datasets["train"], | |
| order[start:], | |
| training["batch_size"], | |
| training["num_workers"], | |
| config["seed"] + 2 + epoch, | |
| training["pin_memory"] and device.type == "cuda", | |
| training["persistent_workers"], | |
| training["prefetch_factor"], | |
| ) | |
| for offset, raw_batch in enumerate(loader, start=state["batch_in_epoch"]): | |
| batch = _device_batch(raw_batch, device) | |
| optimizer.zero_grad(set_to_none=True) | |
| predicted = model(batch["control"], batch["action"], batch["action_known"]) | |
| losses = dynamics_objective( | |
| predicted, batch["perturbed"], batch["control"], config, statistics | |
| ) | |
| if not all(torch.isfinite(value) for value in losses.values()): | |
| raise FloatingPointError( | |
| f"Non-finite dynamics loss at optimizer step {state['global_step']}" | |
| ) | |
| losses["loss"].backward() | |
| gradient_norm = torch.nn.utils.clip_grad_norm_( | |
| model.parameters(), training["gradient_clip_norm"], error_if_nonfinite=True | |
| ) | |
| optimizer.step() | |
| scheduler.step() | |
| state["global_step"] += 1 | |
| state["batch_in_epoch"] = offset + 1 | |
| _json_log( | |
| output / "training.jsonl", | |
| { | |
| "event": "train_step", | |
| "epoch": epoch, | |
| "global_step": state["global_step"], | |
| "conditions": len(raw_batch["target"]), | |
| **{name: float(value.detach()) for name, value in losses.items()}, | |
| "gradient_norm": float(gradient_norm), | |
| "learning_rate": scheduler.get_last_lr()[0], | |
| }, | |
| ) | |
| if state["global_step"] % training["checkpoint_every_steps"] == 0: | |
| save_checkpoint( | |
| latest, model, optimizer, scheduler, state, configuration, provenance | |
| ) | |
| if max_steps is not None and state["global_step"] >= max_steps: | |
| save_checkpoint( | |
| latest, model, optimizer, scheduler, state, configuration, provenance | |
| ) | |
| return model, state, { | |
| "train_conditions": len(datasets["train"]), | |
| "validation_conditions": len(datasets["validation"]), | |
| "elapsed_seconds": time.perf_counter() - started, | |
| "checkpoint": str(latest), | |
| } | |
| validation = validate_dynamics( | |
| model, datasets["validation"], config, device, validation_batches | |
| ) | |
| _json_log( | |
| output / "training.jsonl", | |
| {"event": "validation", "epoch": epoch, "global_step": state["global_step"], **validation}, | |
| ) | |
| improved = validation["loss"] < state["best_validation_loss"] | |
| state["best_validation_loss"] = min(state["best_validation_loss"], validation["loss"]) | |
| if improved: | |
| state["best_validation_epoch"] = epoch | |
| state["epochs_without_improvement"] = 0 if improved else state["epochs_without_improvement"] + 1 | |
| state["epoch"], state["batch_in_epoch"] = epoch + 1, 0 | |
| save_checkpoint(latest, model, optimizer, scheduler, state, configuration, provenance) | |
| if improved: | |
| save_checkpoint(best, model, optimizer, scheduler, state, configuration, provenance) | |
| if state["epochs_without_improvement"] >= training["early_stopping_patience"]: | |
| state["complete"], state["completion_reason"] = True, "early_stopping" | |
| break | |
| else: | |
| state["complete"], state["completion_reason"] = True, "configured_epochs" | |
| save_checkpoint(latest, model, optimizer, scheduler, state, configuration, provenance) | |
| model.load_state_dict(torch.load(best, map_location=device, weights_only=False)["model"]) | |
| return model, state, { | |
| "train_conditions": len(datasets["train"]), | |
| "validation_conditions": len(datasets["validation"]), | |
| "elapsed_seconds": time.perf_counter() - started, | |
| "checkpoint": str(latest), | |
| "best_checkpoint": str(best), | |
| } | |