OSCR

Divergent Mechanisms of Antidepressant Efficacy: A Unified Computational Comparison of Synaptogenesis, Stabilization, and Tonic Inhibition in a Model of Depression.

Code ↔ Paper

10 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 10 matches
  1. [1] § Materials and methods ↔ 068A_antidep_modes_v2.ipynb, lines 47–131 · score 0.70 · rapid synaptogenesis, GABAergic, global, 50 %, receptor, 20 %
  2. [2] § Materials and methods ↔ 068A_antidep_modes_v2.ipynb, lines 47–131 · score 0.70 · bounded activation, GABAergic, enhanced tonic, accumulates, adaptation, baseline
  3. [3] § Materials and methods ↔ 068_antidep_modes.ipynb, lines 37–104 · score 0.69 · bounded activation, GABAergic, enhanced tonic, accumulates, adaptation, baseline
  4. [4] § Materials and methods ↔ 068A_antidep_modes_v2.ipynb, lines 1–37 · score 0.69 · iso dose comparison, cross mechanism, parameter sweeps, proxy, norm, L1
  5. [5] § Materials and methods ↔ 068A_antidep_modes_v2.ipynb, lines 174–251 · score 0.68 · feed forward network, ReLU, tonic inhibition, neuromodulatory, hidden, layers
  6. [6] § Materials and methods ↔ 068_antidep_modes.ipynb, lines 147–224 · score 0.68 · feed forward network, ReLU, tonic inhibition, neuromodulatory, hidden, layers
  7. [7] § Materials and methods ↔ 068_antidep_modes.ipynb, lines 37–104 · score 0.67 · rapid synaptogenesis, GABAergic, global, receptor, tanh, monoaminergic
  8. [8] § Materials and methods ↔ 068A_antidep_modes_v2.ipynb, lines 174–251 · score 0.55 · feed forward network, GABAergic, tonic inhibition, modules, hidden, layer
  9. [9] § Materials and methods ↔ 068_antidep_modes.ipynb, lines 147–224 · score 0.55 · feed forward network, GABAergic, tonic inhibition, modules, hidden, layer
  10. [10] § Materials and methods ↔ 068_antidep_modes.ipynb, lines 227–342 · score 0.51 · cross entropy loss, batch, threshold, magnitude, trained, weight

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Jupyter notebook · 1,843 lines · 78 KB · no license · 5 matches

  1. # %% [markdown]
  2. # # Modes of Antidepressants
  3. # %%
  4. """
  5. ================================================================================
  6. MULTI-MECHANISM ANTIDEPRESSANT COMPARISON EXPERIMENT
  7. WITH ISO-DOSE FAIR COMPARISON PIPELINE
  8. ================================================================================
  9. This script runs only Experiment 4: comparing three antidepressant mechanisms:
  10. 1. KETAMINE-LIKE: Gradient-guided synaptogenesis
  11. 2. SSRI-LIKE: Gradual stabilization without structural changes
  12. 3. NEUROSTEROID-LIKE: Tonic inhibition enhancement
  13. All treatments start from identical pruned (depressed) network states.
  14. VERSION 2.0 ADDITIONS:
  15. - Iso-dose comparison pipeline for fair cross-mechanism comparison
  16. - L1/L2 weight change norms as mechanism-agnostic "dose" proxy
  17. - Synaptic turnover measurement
  18. - Parameter sweeps to match dose across treatments
  19. - Efficiency analysis: outcome per unit dose
  20. ================================================================================
  21. """
  22. import torch
  23. import torch.nn as nn
  24. import torch.optim as optim
  25. import numpy as np
  26. from torch.utils.data import DataLoader, TensorDataset
  27. from typing import Dict, Tuple, List, Optional, Any
  28. from dataclasses import dataclass
  29. import copy
  30. import warnings
  31. warnings.filterwarnings('ignore', category=UserWarning)
  32. # ============================================================================
  33. # REPRODUCIBILITY
  34. # ============================================================================
  35. SEED = 42
  36. torch.manual_seed(SEED)
  37. np.random.seed(SEED)
  38. DEVICE = torch.device('cpu')
  39. # ============================================================================
  40. # CONFIGURATION - Fixed version with proper syntax
  41. # ============================================================================
  42. CONFIG = {
  43. # Data generation
  44. 'n_train': 12000,
  45. 'n_test': 4000,
  46. 'n_clean_test': 2000,
  47. 'data_noise': 0.8,
  48. 'batch_size': 128,
  49. # Network architecture
  50. 'hidden_dims': [512, 512, 256],
  51. 'input_dim': 2,
  52. 'output_dim': 4,
  53. # Training hyperparameters
  54. 'baseline_epochs': 20,
  55. 'baseline_lr': 0.001,
  56. 'finetune_epochs': 15,
  57. 'finetune_lr': 0.0005,
  58. # Pruning parameters
  59. 'prune_sparsity': 0.95,
  60. # Regrowth parameters
  61. 'regrow_fraction': 0.5,
  62. 'regrow_init_scale': 0.03,
  63. 'gradient_accumulation_batches': 30,
  64. # Stress levels for evaluation
  65. 'extended_stress_levels': {
  66. 'none': 0.0,
  67. 'moderate': 0.5,
  68. 'high': 1.0,
  69. 'severe': 1.5,
  70. 'extreme': 2.5
  71. },
  72. # ========================================================================
  73. # MONOAMINERGIC (SSRI-LIKE) TREATMENT PARAMETERS
  74. # ========================================================================
  75. # Biological rationale: SSRIs increase synaptic serotonin, leading to
  76. # gradual receptor adaptations over weeks. No rapid synaptogenesis.
  77. # Network analog: Fixed sparsity, very low LR, gradual noise reduction.
  78. 'monoaminergic_epochs': 100,
  79. 'monoaminergic_lr': 1e-5,
  80. 'monoaminergic_initial_stress': 0.5,
  81. # ========================================================================
  82. # NEUROSTEROID (GABAergic) TREATMENT PARAMETERS
  83. # ========================================================================
  84. # Biological rationale: Neurosteroids enhance tonic GABA inhibition,
  85. # reducing network excitability rapidly (days, not weeks).
  86. # Network analog: Global activation damping, bounded activations.
  87. 'neurosteroid_inhibition_strength': 0.7,
  88. 'neurosteroid_use_tanh': True,
  89. 'neurosteroid_consolidation_epochs': 10,
  90. # ========================================================================
  91. # MULTI-MECHANISM COMPARISON PARAMETERS
  92. # ========================================================================
  93. 'comparison_ketamine_regrow': 0.5,
  94. 'comparison_ketamine_epochs': 15,
  95. 'comparison_ssri_epochs': 100,
  96. 'comparison_neurosteroid_strength': 0.7,
  97. 'comparison_neurosteroid_epochs': 10,
  98. # ========================================================================
  99. # ISO-DOSE COMPARISON PARAMETERS
  100. # ========================================================================
  101. 'iso_dose_norm_type': 'l1',
  102. 'iso_dose_target_doses': [0.005, 0.010, 0.020, 0.040],
  103. 'iso_dose_tolerance': 0.003,
  104. 'iso_dose_turnover_threshold': 0.10,
  105. # Parameter sweep ranges for iso-dose matching
  106. 'ketamine_regrow_sweep': [0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.40, 0.50, 0.60, 0.70, 0.80],
  107. 'ssri_epochs_sweep': [10, 20, 30, 40, 50, 60, 80, 100, 120, 150, 200],
  108. 'ssri_lr_sweep': [1e-6, 5e-6, 1e-5, 2e-5, 5e-5],
  109. 'neurosteroid_strength_sweep': [0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90],
  110. # Relapse simulation parameters
  111. 'relapse_prune_fraction': 0.40,
  112. }
  113. # ============================================================================
  114. # DATA GENERATION
  115. # ============================================================================
  116. def generate_blobs(
  117. n_samples: int = 10000,
  118. noise: float = 0.8,
  119. seed: int = None
  120. ) -> Tuple[torch.Tensor, torch.Tensor]:
  121. """Generate 4-class Gaussian blob classification data."""
  122. if seed is not None:
  123. rng = np.random.RandomState(seed)
  124. else:
  125. rng = np.random.RandomState()
  126. centers = np.array([[-3, -3], [3, 3], [-3, 3], [3, -3]])
  127. labels = rng.randint(0, 4, n_samples)
  128. data = centers[labels] + rng.randn(n_samples, 2) * noise
  129. return (
  130. torch.tensor(data, dtype=torch.float32),
  131. torch.tensor(labels, dtype=torch.long)
  132. )
  133. def create_data_loaders() -> Tuple[DataLoader, DataLoader, DataLoader]:
  134. """Create train, test, and clean test data loaders."""
  135. train_data, train_labels = generate_blobs(CONFIG['n_train'], noise=CONFIG['data_noise'], seed=100)
  136. test_data, test_labels = generate_blobs(CONFIG['n_test'], noise=CONFIG['data_noise'], seed=200)
  137. clean_test_data, clean_test_labels = generate_blobs(CONFIG['n_clean_test'], noise=0.0, seed=300)
  138. train_loader = DataLoader(TensorDataset(train_data, train_labels), batch_size=CONFIG['batch_size'], shuffle=True)
  139. test_loader = DataLoader(TensorDataset(test_data, test_labels), batch_size=1000)
  140. clean_test_loader = DataLoader(TensorDataset(clean_test_data, clean_test_labels), batch_size=1000)
  141. return train_loader, test_loader, clean_test_loader
  142. train_loader, test_loader, clean_test_loader = create_data_loaders()
  143. # ============================================================================
  144. # NETWORK ARCHITECTURE
  145. # ============================================================================
  146. class StressAwareNetwork(nn.Module):
  147. """
  148. Feed-forward network with internal noise injection and GABAergic modulation.
  149. Supports three modulation mechanisms:
  150. - stress_level: Internal noise (neuromodulatory disruption)
  151. - inhibition_strength: Multiplicative damping (tonic GABA inhibition)
  152. - use_tanh: Bounded activation (shunting inhibition)
  153. """
  154. def __init__(self, hidden_dims: List[int] = None):
  155. super().__init__()
  156. if hidden_dims is None:
  157. hidden_dims = CONFIG['hidden_dims']
  158. self.fc1 = nn.Linear(CONFIG['input_dim'], hidden_dims[0])
  159. self.fc2 = nn.Linear(hidden_dims[0], hidden_dims[1])
  160. self.fc3 = nn.Linear(hidden_dims[1], hidden_dims[2])
  161. self.fc4 = nn.Linear(hidden_dims[2], CONFIG['output_dim'])
  162. self.relu = nn.ReLU()
  163. self.tanh = nn.Tanh()
  164. # Modulation parameters
  165. self.stress_level = 0.0 # Internal noise magnitude
  166. self.inhibition_strength = 1.0 # Multiplicative damping (1.0 = none)
  167. self.use_tanh = False # Use bounded activation
  168. self.weight_layers = ['fc1', 'fc2', 'fc3', 'fc4']
  169. def set_stress(self, level: float):
  170. """Set internal noise level for stress simulation."""
  171. self.stress_level = level
  172. def set_inhibition(self, strength: float, use_tanh: bool = False):
  173. """Set GABAergic tonic inhibition parameters."""
  174. self.inhibition_strength = strength
  175. self.use_tanh = use_tanh
  176. def reduce_stress_gradually(self, epoch: int, total_epochs: int,
  177. initial_stress: float = 0.5, final_stress: float = 0.0):
  178. """Linearly reduce internal stress over epochs (SSRI-like)."""
  179. progress = epoch / max(total_epochs - 1, 1)
  180. self.stress_level = initial_stress + progress * (final_stress - initial_stress)
  181. def forward(self, x: torch.Tensor) -> torch.Tensor:
  182. """Forward pass with noise injection and inhibitory modulation."""
  183. activation = self.tanh if self.use_tanh else self.relu
  184. # Layer 1
  185. h = activation(self.fc1(x))
  186. if self.stress_level > 0:
  187. h = h + torch.randn_like(h) * self.stress_level
  188. h = h * self.inhibition_strength
  189. # Layer 2
  190. h = activation(self.fc2(h))
  191. if self.stress_level > 0:
  192. h = h + torch.randn_like(h) * self.stress_level
  193. h = h * self.inhibition_strength
  194. # Layer 3
  195. h = activation(self.fc3(h))
  196. if self.stress_level > 0:
  197. h = h + torch.randn_like(h) * self.stress_level
  198. h = h * self.inhibition_strength
  199. # Output layer (no modulation)
  200. return self.fc4(h)
  201. def count_parameters(self) -> Tuple[int, int]:
  202. """Count total and non-zero parameters."""
  203. total = sum(p.numel() for p in self.parameters())
  204. nonzero = sum((p != 0).sum().item() for p in self.parameters())
  205. return total, nonzero
  206. # ============================================================================
  207. # PRUNING MANAGER
  208. # ============================================================================
  209. class PruningManager:
  210. """Manages structured pruning and gradient-guided regrowth."""
  211. def __init__(self, model: StressAwareNetwork):
  212. self.model = model
  213. self.masks = {}
  214. self.gradient_buffer = {}
  215. for name, param in model.named_parameters():
  216. if 'weight' in name and param.dim() >= 2:
  217. self.masks[name] = torch.ones_like(param, dtype=torch.float32)
  218. self.gradient_buffer[name] = torch.zeros_like(param)
  219. def prune_by_magnitude(self, sparsity: float, per_layer: bool = True) -> Dict[str, Dict]:
  220. """Prune weights by magnitude."""
  221. stats = {}
  222. for name, param in self.model.named_parameters():
  223. if name in self.masks:
  224. weights = param.data.abs()
  225. threshold = torch.quantile(weights.flatten(), sparsity)
  226. self.masks[name] = (weights >= threshold).float()
  227. param.data *= self.masks[name]
  228. kept = self.masks[name].sum().item()
  229. total = self.masks[name].numel()
  230. stats[name] = {'kept': int(kept), 'total': total, 'actual_sparsity': 1 - kept/total}
  231. return stats
  232. def _accumulate_gradients(self, num_batches: int = 30):
  233. """Accumulate gradient magnitudes at pruned positions."""
  234. model = self.model
  235. loss_fn = nn.CrossEntropyLoss()
  236. for name in self.gradient_buffer:
  237. self.gradient_buffer[name].zero_()
  238. model.train()
  239. original_stress = model.stress_level
  240. model.set_stress(0.0)
  241. batch_count = 0
  242. for x, y in train_loader:
  243. if batch_count >= num_batches:
  244. break
  245. x, y = x.to(DEVICE), y.to(DEVICE)
  246. loss = loss_fn(model(x), y)
  247. loss.backward()
  248. with torch.no_grad():
  249. for name, param in model.named_parameters():
  250. if name in self.masks:
  251. pruned_mask = (self.masks[name] == 0).float()
  252. self.gradient_buffer[name] += param.grad.abs() * pruned_mask
  253. model.zero_grad()
  254. batch_count += 1
  255. model.set_stress(original_stress)
  256. def gradient_guided_regrow(self, regrow_fraction: float,
  257. init_scale: float = None) -> Dict[str, Dict]:
  258. """Regrow pruned connections based on gradient importance."""
  259. if init_scale is None:
  260. init_scale = CONFIG['regrow_init_scale']
  261. self._accumulate_gradients(num_batches=CONFIG['gradient_accumulation_batches'])
  262. stats = {}
  263. for name, param in self.model.named_parameters():
  264. if name not in self.masks:
  265. continue
  266. mask = self.masks[name]
  267. pruned_positions = (mask == 0)
  268. num_pruned = pruned_positions.sum().item()
  269. if num_pruned == 0:
  270. stats[name] = {'regrown': 0, 'still_pruned': 0}
  271. continue
  272. gradient_scores = self.gradient_buffer[name][pruned_positions]
  273. num_regrow = max(1, int(regrow_fraction * num_pruned))
  274. num_regrow = min(num_regrow, gradient_scores.numel())
  275. _, top_indices = torch.topk(gradient_scores.flatten(), num_regrow)
  276. flat_pruned_indices = torch.where(pruned_positions.flatten())[0]
  277. regrow_flat_indices = flat_pruned_indices[top_indices]
  278. flat_mask = mask.flatten()
  279. flat_param = param.data.flatten()
  280. flat_mask[regrow_flat_indices] = 1.0
  281. flat_param[regrow_flat_indices] = torch.randn(num_regrow) * init_scale
  282. self.masks[name] = flat_mask.view_as(mask)
  283. param.data = flat_param.view_as(param)
  284. stats[name] = {'regrown': num_regrow, 'still_pruned': int(num_pruned - num_regrow)}
  285. return stats
  286. def apply_masks(self):
  287. """Re-apply masks to maintain sparsity."""
  288. with torch.no_grad():
  289. for name, param in self.model.named_parameters():
  290. if name in self.masks:
  291. param.data *= self.masks[name]
  292. def get_sparsity(self) -> float:
  293. """Calculate overall network sparsity."""
  294. total = sum(m.numel() for m in self.masks.values())
  295. zeros = sum((m == 0).sum().item() for m in self.masks.values())
  296. return zeros / total if total > 0 else 0.0
  297. def secondary_prune(self, fraction: float) -> Dict[str, Any]:
  298. """Simulate relapse by pruning a fraction of surviving weights."""
  299. stats = {}
  300. total_pruned = 0
  301. for name, param in self.model.named_parameters():
  302. if name not in self.masks:
  303. continue
  304. mask = self.masks[name]
  305. active_positions = (mask == 1)
  306. n_active = active_positions.sum().item()
  307. if n_active == 0:
  308. continue
  309. num_to_prune = int(fraction * n_active)
  310. if num_to_prune == 0:
  311. continue
  312. weights = param.data.abs()
  313. weights_active = weights.clone()
  314. weights_active[~active_positions] = float('inf')
  315. flat_weights = weights_active.flatten()
  316. threshold = torch.kthvalue(flat_weights, num_to_prune).values.item()
  317. prune_mask = (weights <= threshold) & active_positions
  318. mask[prune_mask] = 0
  319. param.data[prune_mask] = 0
  320. pruned_count = prune_mask.sum().item()
  321. total_pruned += pruned_count
  322. stats[name] = {
  323. 'pruned': pruned_count,
  324. 'remaining': n_active - pruned_count
  325. }
  326. new_sparsity = self.get_sparsity()
  327. return {
  328. 'total_pruned': total_pruned,
  329. 'new_sparsity': new_sparsity,
  330. 'layer_stats': stats
  331. }
  332. # ============================================================================
  333. # DOSING METRICS - MECHANISM-AGNOSTIC QUANTIFICATION
  334. # ============================================================================
  335. @dataclass
  336. class DoseMetrics:
  337. """Container for all dosing quantification metrics."""
  338. l1_norm: float = 0.0
  339. l2_norm: float = 0.0
  340. synaptic_turnover: float = 0.0
  341. sparsity_change: float = 0.0
  342. @property
  343. def primary_dose(self) -> float:
  344. """Primary dose metric (L1 norm by default)."""
  345. return self.l1_norm
  346. def compute_weight_change_norm(
  347. model_pre_state: Dict[str, torch.Tensor],
  348. model_post: nn.Module,
  349. norm_type: str = 'l1'
  350. ) -> float:
  351. """
  352. Compute total weight change magnitude as mechanism-agnostic dose proxy.
  353. Returns normalized dose (total change / total parameters).
  354. """
  355. delta = 0.0
  356. total_params = 0
  357. for name, param in model_post.named_parameters():
  358. if 'weight' in name and name in model_pre_state:
  359. diff = (param.data - model_pre_state[name]).abs()
  360. total_params += param.numel()
  361. if norm_type == 'l1':
  362. delta += diff.sum().item()
  363. elif norm_type == 'l2':
  364. delta += (diff ** 2).sum().item()
  365. if norm_type == 'l2':
  366. delta = delta ** 0.5
  367. return delta / total_params if total_params > 0 else 0.0
  368. def compute_synaptic_turnover(
  369. model_pre_state: Dict[str, torch.Tensor],
  370. model_post: nn.Module,
  371. threshold: float = 0.10
  372. ) -> float:
  373. """
  374. Compute fraction of synapses with significant weight changes.
  375. Captures "how many synapses were meaningfully modified" (> threshold relative change).
  376. """
  377. changed = 0
  378. total = 0
  379. for name, param in model_post.named_parameters():
  380. if 'weight' in name and name in model_pre_state:
  381. pre_weights = model_pre_state[name]
  382. relative_change = (param.data - pre_weights).abs() / (pre_weights.abs().clamp(min=1e-8))
  383. changed += (relative_change > threshold).sum().item()
  384. total += param.numel()
  385. return changed / total if total > 0 else 0.0
  386. def compute_sparsity_change(
  387. model_pre_state: Dict[str, torch.Tensor],
  388. model_post: nn.Module
  389. ) -> float:
  390. """Compute absolute change in network sparsity."""
  391. def get_sparsity(state_dict):
  392. total = 0
  393. zeros = 0
  394. for name, tensor in state_dict.items():
  395. if 'weight' in name:
  396. total += tensor.numel()
  397. zeros += (tensor.abs() < 1e-8).sum().item()
  398. return zeros / total if total > 0 else 0.0
  399. pre_sparsity = get_sparsity(model_pre_state)
  400. post_sparsity = get_sparsity({n: p.data for n, p in model_post.named_parameters()})
  401. return abs(post_sparsity - pre_sparsity)
  402. def compute_all_dose_metrics(
  403. model_pre_state: Dict[str, torch.Tensor],
  404. model_post: nn.Module,
  405. turnover_threshold: float = None
  406. ) -> DoseMetrics:
  407. """Compute all dose quantification metrics."""
  408. if turnover_threshold is None:
  409. turnover_threshold = CONFIG['iso_dose_turnover_threshold']
  410. return DoseMetrics(
  411. l1_norm=compute_weight_change_norm(model_pre_state, model_post, 'l1'),
  412. l2_norm=compute_weight_change_norm(model_pre_state, model_post, 'l2'),
  413. synaptic_turnover=compute_synaptic_turnover(model_pre_state, model_post, turnover_threshold),
  414. sparsity_change=compute_sparsity_change(model_pre_state, model_post)
  415. )
  416. # ============================================================================
  417. # TRAINING FUNCTIONS
  418. # ============================================================================
  419. def train(model: StressAwareNetwork, epochs: int = 15, lr: float = 0.001,
  420. pruning_manager: PruningManager = None, verbose: bool = False) -> List[float]:
  421. """Standard training loop."""
  422. optimizer = optim.Adam(model.parameters(), lr=lr)
  423. loss_fn = nn.CrossEntropyLoss()
  424. losses = []
  425. original_stress = model.stress_level
  426. model.set_stress(0.0)
  427. for epoch in range(epochs):
  428. model.train()
  429. epoch_loss = 0.0
  430. for x, y in train_loader:
  431. x, y = x.to(DEVICE), y.to(DEVICE)
  432. optimizer.zero_grad()
  433. loss = loss_fn(model(x), y)
  434. loss.backward()
  435. optimizer.step()
  436. if pruning_manager:
  437. pruning_manager.apply_masks()
  438. epoch_loss += loss.item()
  439. losses.append(epoch_loss / len(train_loader))
  440. if verbose:
  441. print(f" Epoch {epoch+1}/{epochs}, Loss: {losses[-1]:.4f}")
  442. model.set_stress(original_stress)
  443. return losses
  444. def train_with_stress_schedule(model: StressAwareNetwork, epochs: int, lr: float,
  445. initial_stress: float, final_stress: float = 0.0,
  446. pruning_manager: PruningManager = None,
  447. verbose: bool = False, print_interval: int = 20) -> List[float]:
  448. """Train with gradually reducing internal stress (SSRI-like)."""
  449. optimizer = optim.Adam(model.parameters(), lr=lr)
  450. loss_fn = nn.CrossEntropyLoss()
  451. losses = []
  452. for epoch in range(epochs):
  453. model.reduce_stress_gradually(epoch, epochs, initial_stress, final_stress)
  454. model.train()
  455. epoch_loss = 0.0
  456. for x, y in train_loader:
  457. x, y = x.to(DEVICE), y.to(DEVICE)
  458. optimizer.zero_grad()
  459. loss = loss_fn(model(x), y)
  460. loss.backward()
  461. optimizer.step()
  462. if pruning_manager:
  463. pruning_manager.apply_masks()
  464. epoch_loss += loss.item()
  465. losses.append(epoch_loss / len(train_loader))
  466. if verbose and (epoch + 1) % print_interval == 0:
  467. print(f" SSRI epoch {epoch+1}/{epochs}, stress: {model.stress_level:.3f}, loss: {losses[-1]:.4f}")
  468. model.set_stress(0.0)
  469. return losses
  470. # ============================================================================
  471. # EVALUATION FUNCTIONS
  472. # ============================================================================
  473. def evaluate(model: StressAwareNetwork, loader: DataLoader,
  474. input_noise: float = 0.0, internal_stress: float = 0.0) -> float:
  475. """Evaluate model accuracy."""
  476. model.eval()
  477. model.set_stress(internal_stress)
  478. correct, total = 0, 0
  479. with torch.no_grad():
  480. for x, y in loader:
  481. x, y = x.to(DEVICE), y.to(DEVICE)
  482. if input_noise > 0:
  483. x = x + torch.randn_like(x) * input_noise
  484. correct += (model(x).argmax(dim=1) == y).sum().item()
  485. total += y.size(0)
  486. model.set_stress(0.0)
  487. return 100.0 * correct / total
  488. def evaluate_with_neurosteroid(model: StressAwareNetwork, loader: DataLoader,
  489. input_noise: float = 0.0, internal_stress: float = 0.0) -> float:
  490. """Evaluate with neurosteroid modulation ACTIVE (inhibition settings preserved)."""
  491. model.eval()
  492. model.set_stress(internal_stress)
  493. correct, total = 0, 0
  494. with torch.no_grad():
  495. for x, y in loader:
  496. x, y = x.to(DEVICE), y.to(DEVICE)
  497. if input_noise > 0:
  498. x = x + torch.randn_like(x) * input_noise
  499. correct += (model(x).argmax(dim=1) == y).sum().item()
  500. total += y.size(0)
  501. model.set_stress(0.0)
  502. return 100.0 * correct / total
  503. # ============================================================================
  504. # TREATMENT PROTOCOLS
  505. # ============================================================================
  506. def ketamine_treatment(model: StressAwareNetwork, pruning_mgr: PruningManager,
  507. regrow_fraction: float = None, consolidation_epochs: int = None,
  508. verbose: bool = True) -> Dict:
  509. """
  510. KETAMINE-LIKE TREATMENT: Gradient-guided synaptogenesis.
  511. Biological model:
  512. - NMDA antagonism → BDNF release → mTOR activation → new spine formation
  513. - Activity-dependent targeting of new synapses
  514. - Brief consolidation to strengthen useful connections
  515. Key feature: ADDS NEW SYNAPSES (reduces sparsity)
  516. """
  517. if regrow_fraction is None:
  518. regrow_fraction = CONFIG['comparison_ketamine_regrow']
  519. if consolidation_epochs is None:
  520. consolidation_epochs = CONFIG['comparison_ketamine_epochs']
  521. if verbose:
  522. print(f"\n KETAMINE-LIKE TREATMENT:")
  523. print(f" Regrowth fraction: {regrow_fraction*100:.0f}%")
  524. print(f" Consolidation: {consolidation_epochs} epochs")
  525. print(f" Estimating gradient importance...")
  526. regrow_stats = pruning_mgr.gradient_guided_regrow(regrow_fraction=regrow_fraction)
  527. total_regrown = sum(s['regrown'] for s in regrow_stats.values())
  528. if verbose:
  529. print(f" Restored {total_regrown:,} synapses")
  530. print(f" Consolidating new synapses...")
  531. consolidation_losses = train(model, epochs=consolidation_epochs,
  532. lr=CONFIG['finetune_lr'], pruning_manager=pruning_mgr)
  533. final_sparsity = pruning_mgr.get_sparsity()
  534. if verbose:
  535. print(f" Final sparsity: {final_sparsity*100:.1f}%")
  536. return {'regrow_stats': regrow_stats, 'final_sparsity': final_sparsity}
  537. def ssri_treatment(model: StressAwareNetwork, pruning_mgr: PruningManager,
  538. epochs: int = None, learning_rate: float = None,
  539. initial_stress: float = None, verbose: bool = True,
  540. print_interval: int = 25) -> Dict:
  541. """
  542. SSRI-LIKE TREATMENT: Gradual stabilization without structural changes.
  543. Biological model:
  544. - Increased synaptic serotonin → gradual receptor adaptations
  545. - 5-HT1A autoreceptor desensitization over weeks
  546. - Improved signal-to-noise in existing circuits
  547. Key feature: NO NEW SYNAPSES (sparsity unchanged)
  548. """
  549. if epochs is None:
  550. epochs = CONFIG['monoaminergic_epochs']
  551. if learning_rate is None:
  552. learning_rate = CONFIG['monoaminergic_lr']
  553. if initial_stress is None:
  554. initial_stress = CONFIG['monoaminergic_initial_stress']
  555. if verbose:
  556. print(f"\n SSRI-LIKE TREATMENT:")
  557. print(f" Duration: {epochs} epochs (gradual)")
  558. print(f" Learning rate: {learning_rate} (very low)")
  559. print(f" Internal stress: {initial_stress} → 0.0")
  560. print(f" Note: NO structural changes (fixed sparsity)")
  561. initial_sparsity = pruning_mgr.get_sparsity()
  562. losses = train_with_stress_schedule(model, epochs=epochs, lr=learning_rate,
  563. initial_stress=initial_stress, final_stress=0.0,
  564. pruning_manager=pruning_mgr, verbose=verbose,
  565. print_interval=print_interval)
  566. final_sparsity = pruning_mgr.get_sparsity()
  567. if verbose:
  568. print(f" Final sparsity: {final_sparsity*100:.1f}% (unchanged)")
  569. return {'final_sparsity': final_sparsity, 'training_losses': losses}
  570. def neurosteroid_treatment(model: StressAwareNetwork, pruning_mgr: PruningManager,
  571. inhibition_strength: float = None, use_tanh: bool = None,
  572. consolidation_epochs: int = None, verbose: bool = True) -> Dict:
  573. """
  574. NEUROSTEROID-LIKE TREATMENT: Enhanced tonic inhibition.
  575. Biological model:
  576. - Enhanced extrasynaptic GABA-A receptor activation
  577. - Tonic (sustained) inhibition reduces network excitability
  578. - Rapid onset (days, not weeks)
  579. Key features:
  580. - NO NEW SYNAPSES (sparsity unchanged)
  581. - Works by DAMPING activity rather than building structure
  582. - Medication-dependent (effects reverse when stopped)
  583. """
  584. if inhibition_strength is None:
  585. inhibition_strength = CONFIG['neurosteroid_inhibition_strength']
  586. if use_tanh is None:
  587. use_tanh = CONFIG['neurosteroid_use_tanh']
  588. if consolidation_epochs is None:
  589. consolidation_epochs = CONFIG['neurosteroid_consolidation_epochs']
  590. if verbose:
  591. print(f"\n NEUROSTEROID-LIKE TREATMENT:")
  592. print(f" Inhibition strength: {inhibition_strength} ({(1-inhibition_strength)*100:.0f}% damping)")
  593. print(f" Bounded activation (tanh): {use_tanh}")
  594. print(f" Consolidation: {consolidation_epochs} epochs")
  595. print(f" Note: NO structural changes (fixed sparsity)")
  596. # Apply tonic inhibition modulation
  597. model.set_inhibition(inhibition_strength, use_tanh)
  598. if verbose:
  599. print(f" Applied tonic inhibition modulation...")
  600. print(f" Adapting to new activity dynamics...")
  601. consolidation_losses = train(model, epochs=consolidation_epochs,
  602. lr=CONFIG['finetune_lr'], pruning_manager=pruning_mgr)
  603. final_sparsity = pruning_mgr.get_sparsity()
  604. if verbose:
  605. print(f" Final sparsity: {final_sparsity*100:.1f}% (unchanged)")
  606. return {'final_sparsity': final_sparsity, 'inhibition_strength': inhibition_strength,
  607. 'use_tanh': use_tanh}
  608. # ============================================================================
  609. # ISO-DOSE PARAMETER SWEEP FUNCTIONS
  610. # ============================================================================
  611. def clone_model_and_manager(
  612. base_state_dict: Dict[str, torch.Tensor],
  613. base_masks: Dict[str, torch.Tensor]
  614. ) -> Tuple[StressAwareNetwork, PruningManager]:
  615. """Clone model and pruning manager from saved states."""
  616. model = StressAwareNetwork().to(DEVICE)
  617. model.load_state_dict({k: v.clone() for k, v in base_state_dict.items()})
  618. mgr = PruningManager(model)
  619. mgr.masks = {k: v.clone() for k, v in base_masks.items()}
  620. mgr.apply_masks()
  621. return model, mgr
  622. def run_ketamine_sweep(
  623. base_state_dict: Dict[str, torch.Tensor],
  624. base_masks: Dict[str, torch.Tensor],
  625. untreated_combined: float,
  626. regrow_fractions: List[float] = None,
  627. verbose: bool = False
  628. ) -> List[Dict[str, Any]]:
  629. """
  630. Sweep ketamine regrow_fraction and measure dose + outcomes.
  631. Returns list of results with dose metrics and performance outcomes.
  632. """
  633. if regrow_fractions is None:
  634. regrow_fractions = CONFIG['ketamine_regrow_sweep']
  635. results = []
  636. for regrow_frac in regrow_fractions:
  637. model, mgr = clone_model_and_manager(base_state_dict, base_masks)
  638. # Capture pre-treatment state
  639. pre_state = {n: p.data.clone() for n, p in model.named_parameters() if 'weight' in n}
  640. # Apply treatment
  641. ketamine_treatment(model, mgr, regrow_fraction=regrow_frac,
  642. consolidation_epochs=CONFIG['comparison_ketamine_epochs'],
  643. verbose=False)
  644. # Compute dose metrics
  645. dose_metrics = compute_all_dose_metrics(pre_state, model)
  646. # Evaluate acute performance
  647. acute_clean = evaluate(model, clean_test_loader, 0.0, 0.0)
  648. acute_standard = evaluate(model, test_loader, 0.0, 0.0)
  649. acute_combined = evaluate(model, test_loader, 1.0, 0.5)
  650. acute_extreme = evaluate(model, test_loader, 0.0, 2.5)
  651. # Relapse simulation
  652. pre_relapse_combined = acute_combined
  653. mgr.secondary_prune(fraction=CONFIG['relapse_prune_fraction'])
  654. post_relapse_combined = evaluate(model, test_loader, 1.0, 0.5)
  655. relapse_drop = pre_relapse_combined - post_relapse_combined
  656. improvement = acute_combined - untreated_combined
  657. results.append({
  658. 'treatment': 'ketamine',
  659. 'param_name': 'regrow_fraction',
  660. 'param_value': regrow_frac,
  661. 'dose': dose_metrics,
  662. 'sparsity': mgr.get_sparsity() * 100,
  663. 'acute_clean': acute_clean,
  664. 'acute_standard': acute_standard,
  665. 'acute_combined': acute_combined,
  666. 'acute_extreme': acute_extreme,
  667. 'improvement': improvement,
  668. 'relapse_drop': relapse_drop,
  669. 'post_relapse_combined': post_relapse_combined
  670. })
  671. if verbose:
  672. print(f" regrow_frac={regrow_frac:.2f}: dose={dose_metrics.l1_norm:.6f}, "
  673. f"combined={acute_combined:.1f}%, relapse_drop={relapse_drop:.1f}%")
  674. return results
  675. def run_ssri_sweep(
  676. base_state_dict: Dict[str, torch.Tensor],
  677. base_masks: Dict[str, torch.Tensor],
  678. untreated_combined: float,
  679. epochs_list: List[int] = None,
  680. lr_list: List[float] = None,
  681. verbose: bool = False
  682. ) -> List[Dict[str, Any]]:
  683. """
  684. Sweep SSRI epochs and learning rate, measure dose + outcomes.
  685. Returns list of results with dose metrics and performance outcomes.
  686. """
  687. if epochs_list is None:
  688. epochs_list = CONFIG['ssri_epochs_sweep']
  689. if lr_list is None:
  690. lr_list = [CONFIG['monoaminergic_lr']]
  691. results = []
  692. for epochs in epochs_list:
  693. for lr in lr_list:
  694. model, mgr = clone_model_and_manager(base_state_dict, base_masks)
  695. # Capture pre-treatment state
  696. pre_state = {n: p.data.clone() for n, p in model.named_parameters() if 'weight' in n}
  697. # Apply treatment
  698. ssri_treatment(model, mgr, epochs=epochs, learning_rate=lr,
  699. initial_stress=CONFIG['monoaminergic_initial_stress'],
  700. verbose=False)
  701. # Compute dose metrics
  702. dose_metrics = compute_all_dose_metrics(pre_state, model)
  703. # Evaluate acute performance
  704. acute_clean = evaluate(model, clean_test_loader, 0.0, 0.0)
  705. acute_standard = evaluate(model, test_loader, 0.0, 0.0)
  706. acute_combined = evaluate(model, test_loader, 1.0, 0.5)
  707. acute_extreme = evaluate(model, test_loader, 0.0, 2.5)
  708. # Relapse simulation
  709. pre_relapse_combined = acute_combined
  710. mgr.secondary_prune(fraction=CONFIG['relapse_prune_fraction'])
  711. post_relapse_combined = evaluate(model, test_loader, 1.0, 0.5)
  712. relapse_drop = pre_relapse_combined - post_relapse_combined
  713. improvement = acute_combined - untreated_combined
  714. results.append({
  715. 'treatment': 'ssri',
  716. 'param_name': 'epochs',
  717. 'param_value': epochs,
  718. 'lr': lr,
  719. 'dose': dose_metrics,
  720. 'sparsity': mgr.get_sparsity() * 100,
  721. 'acute_clean': acute_clean,
  722. 'acute_standard': acute_standard,
  723. 'acute_combined': acute_combined,
  724. 'acute_extreme': acute_extreme,
  725. 'improvement': improvement,
  726. 'relapse_drop': relapse_drop,
  727. 'post_relapse_combined': post_relapse_combined
  728. })
  729. if verbose:
  730. print(f" epochs={epochs}, lr={lr:.0e}: dose={dose_metrics.l1_norm:.6f}, "
  731. f"combined={acute_combined:.1f}%, relapse_drop={relapse_drop:.1f}%")
  732. return results
  733. def run_neurosteroid_sweep(
  734. base_state_dict: Dict[str, torch.Tensor],
  735. base_masks: Dict[str, torch.Tensor],
  736. untreated_combined: float,
  737. strength_list: List[float] = None,
  738. verbose: bool = False
  739. ) -> List[Dict[str, Any]]:
  740. """
  741. Sweep neurosteroid inhibition strength, measure dose + outcomes.
  742. Returns list of results with dose metrics and performance outcomes.
  743. """
  744. if strength_list is None:
  745. strength_list = CONFIG['neurosteroid_strength_sweep']
  746. results = []
  747. for strength in strength_list:
  748. model, mgr = clone_model_and_manager(base_state_dict, base_masks)
  749. # Capture pre-treatment state
  750. pre_state = {n: p.data.clone() for n, p in model.named_parameters() if 'weight' in n}
  751. # Apply treatment
  752. neurosteroid_treatment(model, mgr, inhibition_strength=strength,
  753. use_tanh=CONFIG['neurosteroid_use_tanh'],
  754. consolidation_epochs=CONFIG['neurosteroid_consolidation_epochs'],
  755. verbose=False)
  756. # Compute dose metrics
  757. dose_metrics = compute_all_dose_metrics(pre_state, model)
  758. # Evaluate acute performance WITH modulation active
  759. acute_clean = evaluate_with_neurosteroid(model, clean_test_loader, 0.0, 0.0)
  760. acute_standard = evaluate_with_neurosteroid(model, test_loader, 0.0, 0.0)
  761. acute_combined = evaluate_with_neurosteroid(model, test_loader, 1.0, 0.5)
  762. acute_extreme = evaluate_with_neurosteroid(model, test_loader, 0.0, 2.5)
  763. # Evaluate OFF medication
  764. model.set_inhibition(1.0, False)
  765. off_med_combined = evaluate(model, test_loader, 1.0, 0.5)
  766. off_med_extreme = evaluate(model, test_loader, 0.0, 2.5)
  767. off_med_reversal = off_med_combined - acute_combined
  768. # Restore modulation for relapse test
  769. model.set_inhibition(strength, CONFIG['neurosteroid_use_tanh'])
  770. # Relapse simulation (with modulation active)
  771. pre_relapse_combined = acute_combined
  772. mgr.secondary_prune(fraction=CONFIG['relapse_prune_fraction'])
  773. post_relapse_combined = evaluate_with_neurosteroid(model, test_loader, 1.0, 0.5)
  774. relapse_drop = pre_relapse_combined - post_relapse_combined
  775. improvement = acute_combined - untreated_combined
  776. results.append({
  777. 'treatment': 'neurosteroid',
  778. 'param_name': 'strength',
  779. 'param_value': strength,
  780. 'dose': dose_metrics,
  781. 'sparsity': mgr.get_sparsity() * 100,
  782. 'acute_clean': acute_clean,
  783. 'acute_standard': acute_standard,
  784. 'acute_combined': acute_combined,
  785. 'acute_extreme': acute_extreme,
  786. 'improvement': improvement,
  787. 'off_med_combined': off_med_combined,
  788. 'off_med_extreme': off_med_extreme,
  789. 'off_med_reversal': off_med_reversal,
  790. 'relapse_drop': relapse_drop,
  791. 'post_relapse_combined': post_relapse_combined
  792. })
  793. if verbose:
  794. print(f" strength={strength:.2f}: dose={dose_metrics.l1_norm:.6f}, "
  795. f"combined={acute_combined:.1f}%, off_med={off_med_combined:.1f}%, "
  796. f"relapse_drop={relapse_drop:.1f}%")
  797. return results
  798. def find_iso_dose_match(
  799. sweep_results: List[Dict[str, Any]],
  800. target_dose: float,
  801. tolerance: float = None
  802. ) -> Optional[Dict[str, Any]]:
  803. """
  804. Find parameter configuration closest to target dose.
  805. Returns the result with dose closest to target, or None if no results.
  806. """
  807. if tolerance is None:
  808. tolerance = CONFIG['iso_dose_tolerance']
  809. if not sweep_results:
  810. return None
  811. best_match = None
  812. best_diff = float('inf')
  813. for result in sweep_results:
  814. dose = result['dose'].l1_norm
  815. diff = abs(dose - target_dose)
  816. if diff < best_diff:
  817. best_diff = diff
  818. best_match = result
  819. return best_match
  820. def compute_efficiency(improvement: float, dose: float) -> float:
  821. """Compute treatment efficiency: improvement per unit dose."""
  822. return improvement / (dose + 1e-8)
  823. # ============================================================================
  824. # ISO-DOSE COMPARISON EXPERIMENT
  825. # ============================================================================
  826. def run_iso_dose_comparison_experiment(
  827. base_state_dict: Dict[str, torch.Tensor],
  828. base_masks: Dict[str, torch.Tensor],
  829. untreated_results: Dict[str, Any]
  830. ) -> Dict[str, Any]:
  831. """
  832. Run iso-dose comparison across all three treatment mechanisms.
  833. Sweeps parameters for each treatment, measures dose (L1 weight change norm),
  834. and compares outcomes at matched dose levels.
  835. """
  836. print("\n" + "=" * 80)
  837. print(" ISO-DOSE FAIR COMPARISON EXPERIMENT")
  838. print("=" * 80)
  839. print("""
  840. ISO-DOSE COMPARISON METHODOLOGY:
  841. ┌────────────────────────────────────────────────────────────────────────────┐
  842. │ DOSE METRIC: L1 weight change norm (normalized per parameter) │
  843. │ │
  844. │ This provides a mechanism-agnostic measure of "how much the network │
  845. │ changed" regardless of whether changes came from: │
  846. │ - Structural regrowth (ketamine) │
  847. │ - Gradual weight refinement (SSRI) │
  848. │ - Consolidation under modulation (neurosteroid) │
  849. │ │
  850. │ EFFICIENCY = (Performance Improvement) / (Dose) │
  851. │ Higher efficiency = better outcome per unit of network change │
  852. └────────────────────────────────────────────────────────────────────────────┘
  853. """)
  854. untreated_combined = untreated_results['combined']
  855. results = {
  856. 'sweeps': {},
  857. 'iso_dose_comparisons': {},
  858. 'efficiency_analysis': {}
  859. }
  860. # ========================================================================
  861. # PHASE 1: Parameter Sweeps
  862. # ========================================================================
  863. print("-" * 70)
  864. print(" PHASE 1: Parameter Sweeps (Measuring Dose-Response)")
  865. print("-" * 70)
  866. print("\n [KETAMINE] Sweeping regrow_fraction...")
  867. print(f" Testing {len(CONFIG['ketamine_regrow_sweep'])} configurations...")
  868. ketamine_results = run_ketamine_sweep(
  869. base_state_dict, base_masks, untreated_combined, verbose=True
  870. )
  871. results['sweeps']['ketamine'] = ketamine_results
  872. print(f" Completed {len(ketamine_results)} configurations")
  873. print("\n [SSRI] Sweeping epochs...")
  874. print(f" Testing {len(CONFIG['ssri_epochs_sweep'])} configurations...")
  875. ssri_results = run_ssri_sweep(
  876. base_state_dict, base_masks, untreated_combined, verbose=True
  877. )
  878. results['sweeps']['ssri'] = ssri_results
  879. print(f" Completed {len(ssri_results)} configurations")
  880. print("\n [NEUROSTEROID] Sweeping inhibition strength...")
  881. print(f" Testing {len(CONFIG['neurosteroid_strength_sweep'])} configurations...")
  882. neurosteroid_results = run_neurosteroid_sweep(
  883. base_state_dict, base_masks, untreated_combined, verbose=True
  884. )
  885. results['sweeps']['neurosteroid'] = neurosteroid_results
  886. print(f" Completed {len(neurosteroid_results)} configurations")
  887. # ========================================================================
  888. # PHASE 2: Dose-Response Analysis
  889. # ========================================================================
  890. print("\n" + "-" * 70)
  891. print(" PHASE 2: Dose-Response Curves")
  892. print("-" * 70)
  893. print("\n KETAMINE DOSE-RESPONSE:")
  894. print(f" {'regrow_frac':>12} {'L1 Dose':>12} {'L2 Dose':>12} {'Turnover':>10} "
  895. f"{'Combined':>10} {'Improve':>10} {'Relapse':>10}")
  896. print(" " + "-" * 88)
  897. for r in ketamine_results:
  898. print(f" {r['param_value']:>12.2f} {r['dose'].l1_norm:>12.6f} {r['dose'].l2_norm:>12.6f} "
  899. f"{r['dose'].synaptic_turnover:>10.4f} {r['acute_combined']:>9.1f}% "
  900. f"{r['improvement']:>+9.1f}% {r['relapse_drop']:>9.1f}%")
  901. print("\n SSRI DOSE-RESPONSE:")
  902. print(f" {'epochs':>12} {'L1 Dose':>12} {'L2 Dose':>12} {'Turnover':>10} "
  903. f"{'Combined':>10} {'Improve':>10} {'Relapse':>10}")
  904. print(" " + "-" * 88)
  905. for r in ssri_results:
  906. print(f" {r['param_value']:>12} {r['dose'].l1_norm:>12.6f} {r['dose'].l2_norm:>12.6f} "
  907. f"{r['dose'].synaptic_turnover:>10.4f} {r['acute_combined']:>9.1f}% "
  908. f"{r['improvement']:>+9.1f}% {r['relapse_drop']:>9.1f}%")
  909. print("\n NEUROSTEROID DOSE-RESPONSE:")
  910. print(f" {'strength':>12} {'L1 Dose':>12} {'L2 Dose':>12} {'Turnover':>10} "
  911. f"{'Combined':>10} {'Off-Med':>10} {'Relapse':>10}")
  912. print(" " + "-" * 88)
  913. for r in neurosteroid_results:
  914. print(f" {r['param_value']:>12.2f} {r['dose'].l1_norm:>12.6f} {r['dose'].l2_norm:>12.6f} "
  915. f"{r['dose'].synaptic_turnover:>10.4f} {r['acute_combined']:>9.1f}% "
  916. f"{r['off_med_combined']:>9.1f}% {r['relapse_drop']:>9.1f}%")
  917. # ========================================================================
  918. # PHASE 3: Determine Dose Range for Iso-Dose Matching
  919. # ========================================================================
  920. print("\n" + "-" * 70)
  921. print(" PHASE 3: Iso-Dose Matching")
  922. print("-" * 70)
  923. all_doses = []
  924. for r in ketamine_results:
  925. all_doses.append(('ketamine', r['param_value'], r['dose'].l1_norm))
  926. for r in ssri_results:
  927. all_doses.append(('ssri', r['param_value'], r['dose'].l1_norm))
  928. for r in neurosteroid_results:
  929. all_doses.append(('neurosteroid', r['param_value'], r['dose'].l1_norm))
  930. dose_values = [d[2] for d in all_doses]
  931. dose_min, dose_max = min(dose_values), max(dose_values)
  932. print(f"\n Observed dose range across all treatments:")
  933. print(f" Minimum dose: {dose_min:.6f}")
  934. print(f" Maximum dose: {dose_max:.6f}")
  935. # Find overlapping dose range
  936. ket_doses = [r['dose'].l1_norm for r in ketamine_results]
  937. ssri_doses = [r['dose'].l1_norm for r in ssri_results]
  938. neuro_doses = [r['dose'].l1_norm for r in neurosteroid_results]
  939. overlap_min = max(min(ket_doses), min(ssri_doses), min(neuro_doses))
  940. overlap_max = min(max(ket_doses), max(ssri_doses), max(neuro_doses))
  941. print(f"\n Overlapping dose range (for fair comparison):")
  942. print(f" Overlap minimum: {overlap_min:.6f}")
  943. print(f" Overlap maximum: {overlap_max:.6f}")
  944. # Generate target doses within overlap
  945. if overlap_max > overlap_min:
  946. target_doses = np.linspace(overlap_min, overlap_max, 5).tolist()
  947. else:
  948. # If no overlap, use percentiles of all doses
  949. target_doses = [np.percentile(dose_values, p) for p in [20, 40, 60, 80]]
  950. print(f"\n Target doses for iso-dose comparison:")
  951. for i, td in enumerate(target_doses):
  952. print(f" Level {i+1}: {td:.6f}")
  953. # ========================================================================
  954. # PHASE 4: Iso-Dose Matched Comparisons
  955. # ========================================================================
  956. print("\n" + "-" * 70)
  957. print(" PHASE 4: Iso-Dose Matched Comparisons")
  958. print("-" * 70)
  959. for i, target_dose in enumerate(target_doses):
  960. print(f"\n ISO-DOSE LEVEL {i+1}: Target Dose = {target_dose:.6f}")
  961. print(" " + "=" * 75)
  962. ket_match = find_iso_dose_match(ketamine_results, target_dose)
  963. ssri_match = find_iso_dose_match(ssri_results, target_dose)
  964. neuro_match = find_iso_dose_match(neurosteroid_results, target_dose)
  965. iso_comparison = {
  966. 'target_dose': target_dose,
  967. 'ketamine': ket_match,
  968. 'ssri': ssri_match,
  969. 'neurosteroid': neuro_match
  970. }
  971. results['iso_dose_comparisons'][f'level_{i+1}'] = iso_comparison
  972. print(f"\n {'Treatment':<15} {'Parameter':<18} {'Actual Dose':>12} {'Dose Diff':>12} "
  973. f"{'Combined':>10} {'Improve':>10} {'Relapse':>10}")
  974. print(" " + "-" * 95)
  975. for name, match in [('Ketamine', ket_match), ('SSRI', ssri_match), ('Neurosteroid', neuro_match)]:
  976. if match:
  977. param_str = f"{match['param_name']}={match['param_value']}"
  978. actual_dose = match['dose'].l1_norm
  979. dose_diff = actual_dose - target_dose
  980. print(f" {name:<15} {param_str:<18} {actual_dose:>12.6f} {dose_diff:>+12.6f} "
  981. f"{match['acute_combined']:>9.1f}% {match['improvement']:>+9.1f}% "
  982. f"{match['relapse_drop']:>9.1f}%")
  983. else:
  984. print(f" {name:<15} {'N/A':<18} {'N/A':>12} {'N/A':>12} {'N/A':>10} {'N/A':>10} {'N/A':>10}")
  985. # Determine best treatment at this dose level
  986. valid_matches = [(n, m) for n, m in [('Ketamine', ket_match), ('SSRI', ssri_match),
  987. ('Neurosteroid', neuro_match)] if m]
  988. if valid_matches:
  989. best_combined = max(valid_matches, key=lambda x: x[1]['acute_combined'])
  990. best_relapse = min(valid_matches, key=lambda x: x[1]['relapse_drop'])
  991. print(f"\n At this dose level:")
  992. print(f" Best acute performance: {best_combined[0]} ({best_combined[1]['acute_combined']:.1f}%)")
  993. print(f" Best relapse resistance: {best_relapse[0]} (drop: {best_relapse[1]['relapse_drop']:.1f}%)")
  994. # ========================================================================
  995. # PHASE 5: Efficiency Analysis
  996. # ========================================================================
  997. print("\n" + "-" * 70)
  998. print(" PHASE 5: Treatment Efficiency Analysis")
  999. print("-" * 70)
  1000. print("\n EFFICIENCY = (Performance Improvement) / (L1 Dose)")
  1001. print(" Higher efficiency = better outcome per unit of network change")
  1002. print("\n KETAMINE EFFICIENCY:")
  1003. print(f" {'regrow_frac':>12} {'Dose':>12} {'Improvement':>12} {'Efficiency':>12}")
  1004. print(" " + "-" * 52)
  1005. ket_efficiencies = []
  1006. for r in ketamine_results:
  1007. eff = compute_efficiency(r['improvement'], r['dose'].l1_norm)
  1008. ket_efficiencies.append(eff)
  1009. print(f" {r['param_value']:>12.2f} {r['dose'].l1_norm:>12.6f} {r['improvement']:>+11.1f}% {eff:>12.2f}")
  1010. print("\n SSRI EFFICIENCY:")
  1011. print(f" {'epochs':>12} {'Dose':>12} {'Improvement':>12} {'Efficiency':>12}")
  1012. print(" " + "-" * 52)
  1013. ssri_efficiencies = []
  1014. for r in ssri_results:
  1015. eff = compute_efficiency(r['improvement'], r['dose'].l1_norm)
  1016. ssri_efficiencies.append(eff)
  1017. print(f" {r['param_value']:>12} {r['dose'].l1_norm:>12.6f} {r['improvement']:>+11.1f}% {eff:>12.2f}")
  1018. print("\n NEUROSTEROID EFFICIENCY:")
  1019. print(f" {'strength':>12} {'Dose':>12} {'Improvement':>12} {'Efficiency':>12}")
  1020. print(" " + "-" * 52)
  1021. neuro_efficiencies = []
  1022. for r in neurosteroid_results:
  1023. eff = compute_efficiency(r['improvement'], r['dose'].l1_norm)
  1024. neuro_efficiencies.append(eff)
  1025. print(f" {r['param_value']:>12.2f} {r['dose'].l1_norm:>12.6f} {r['improvement']:>+11.1f}% {eff:>12.2f}")
  1026. # Store efficiency results
  1027. results['efficiency_analysis'] = {
  1028. 'ketamine': {
  1029. 'max_efficiency': max(ket_efficiencies) if ket_efficiencies else 0,
  1030. 'mean_efficiency': np.mean(ket_efficiencies) if ket_efficiencies else 0,
  1031. 'best_config': ketamine_results[np.argmax(ket_efficiencies)]['param_value'] if ket_efficiencies else None
  1032. },
  1033. 'ssri': {
  1034. 'max_efficiency': max(ssri_efficiencies) if ssri_efficiencies else 0,
  1035. 'mean_efficiency': np.mean(ssri_efficiencies) if ssri_efficiencies else 0,
  1036. 'best_config': ssri_results[np.argmax(ssri_efficiencies)]['param_value'] if ssri_efficiencies else None
  1037. },
  1038. 'neurosteroid': {
  1039. 'max_efficiency': max(neuro_efficiencies) if neuro_efficiencies else 0,
  1040. 'mean_efficiency': np.mean(neuro_efficiencies) if neuro_efficiencies else 0,
  1041. 'best_config': neurosteroid_results[np.argmax(neuro_efficiencies)]['param_value'] if neuro_efficiencies else None
  1042. }
  1043. }
  1044. # ========================================================================
  1045. # PHASE 6: Summary Statistics
  1046. # ========================================================================
  1047. print("\n" + "-" * 70)
  1048. print(" PHASE 6: Summary Statistics")
  1049. print("-" * 70)
  1050. def compute_sweep_stats(sweep_results, treatment_name):
  1051. doses = [r['dose'].l1_norm for r in sweep_results]
  1052. turnovers = [r['dose'].synaptic_turnover for r in sweep_results]
  1053. improvements = [r['improvement'] for r in sweep_results]
  1054. relapse_drops = [r['relapse_drop'] for r in sweep_results]
  1055. efficiencies = [compute_efficiency(r['improvement'], r['dose'].l1_norm) for r in sweep_results]
  1056. return {
  1057. 'treatment': treatment_name,
  1058. 'n_configs': len(sweep_results),
  1059. 'dose_range': (min(doses), max(doses)),
  1060. 'dose_mean': np.mean(doses),
  1061. 'turnover_range': (min(turnovers), max(turnovers)),
  1062. 'best_improvement': max(improvements),
  1063. 'worst_improvement': min(improvements),
  1064. 'best_relapse': min(relapse_drops),
  1065. 'worst_relapse': max(relapse_drops),
  1066. 'max_efficiency': max(efficiencies),
  1067. 'mean_efficiency': np.mean(efficiencies)
  1068. }
  1069. ket_stats = compute_sweep_stats(ketamine_results, 'Ketamine')
  1070. ssri_stats = compute_sweep_stats(ssri_results, 'SSRI')
  1071. neuro_stats = compute_sweep_stats(neurosteroid_results, 'Neurosteroid')
  1072. results['summary_stats'] = {
  1073. 'ketamine': ket_stats,
  1074. 'ssri': ssri_stats,
  1075. 'neurosteroid': neuro_stats
  1076. }
  1077. print("\n TREATMENT SUMMARY ACROSS ALL CONFIGURATIONS:")
  1078. print(f"\n {'Metric':<25} {'Ketamine':>18} {'SSRI':>18} {'Neurosteroid':>18}")
  1079. print(" " + "-" * 80)
  1080. print(f" {'Configurations tested':<25} {ket_stats['n_configs']:>18} "
  1081. f"{ssri_stats['n_configs']:>18} {neuro_stats['n_configs']:>18}")
  1082. print(f" {'Dose range (L1)':<25} {ket_stats['dose_range'][0]:.4f}-{ket_stats['dose_range'][1]:.4f}"
  1083. f" {ssri_stats['dose_range'][0]:.4f}-{ssri_stats['dose_range'][1]:.4f}"
  1084. f" {neuro_stats['dose_range'][0]:.4f}-{neuro_stats['dose_range'][1]:.4f}")
  1085. print(f" {'Mean dose':<25} {ket_stats['dose_mean']:>18.6f} "
  1086. f"{ssri_stats['dose_mean']:>18.6f} {neuro_stats['dose_mean']:>18.6f}")
  1087. print(f" {'Best improvement':<25} {ket_stats['best_improvement']:>+17.1f}% "
  1088. f"{ssri_stats['best_improvement']:>+17.1f}% {neuro_stats['best_improvement']:>+17.1f}%")
  1089. print(f" {'Best relapse resistance':<25} {ket_stats['best_relapse']:>17.1f}% "
  1090. f"{ssri_stats['best_relapse']:>17.1f}% {neuro_stats['best_relapse']:>17.1f}%")
  1091. print(f" {'Max efficiency':<25} {ket_stats['max_efficiency']:>18.2f} "
  1092. f"{ssri_stats['max_efficiency']:>18.2f} {neuro_stats['max_efficiency']:>18.2f}")
  1093. print(f" {'Mean efficiency':<25} {ket_stats['mean_efficiency']:>18.2f} "
  1094. f"{ssri_stats['mean_efficiency']:>18.2f} {neuro_stats['mean_efficiency']:>18.2f}")
  1095. # ========================================================================
  1096. # PHASE 7: Detailed Iso-Dose Comparison Tables
  1097. # ========================================================================
  1098. print("\n" + "=" * 80)
  1099. print(" DETAILED ISO-DOSE COMPARISON RESULTS")
  1100. print("=" * 80)
  1101. for level_key, comparison in results['iso_dose_comparisons'].items():
  1102. target_dose = comparison['target_dose']
  1103. print(f"\n {level_key.upper().replace('_', ' ')}: TARGET DOSE = {target_dose:.6f}")
  1104. print(" " + "=" * 75)
  1105. print(f"\n {'Treatment':<15} {'Parameter':<20} {'Actual Dose':>12} {'Turnover':>10} "
  1106. f"{'ΔSparsity':>10}")
  1107. print(" " + "-" * 75)
  1108. for treatment_name in ['ketamine', 'ssri', 'neurosteroid']:
  1109. match = comparison.get(treatment_name)
  1110. if match:
  1111. param_str = f"{match['param_name']}={match['param_value']}"
  1112. d = match['dose']
  1113. print(f" {treatment_name.capitalize():<15} {param_str:<20} {d.l1_norm:>12.6f} "
  1114. f"{d.synaptic_turnover:>10.4f} {d.sparsity_change:>10.4f}")
  1115. else:
  1116. print(f" {treatment_name.capitalize():<15} {'N/A':<20} {'N/A':>12} {'N/A':>10} {'N/A':>10}")
  1117. print(f"\n {'Treatment':<15} {'Clean':>10} {'Standard':>10} {'Combined':>10} "
  1118. f"{'Extreme':>10} {'Improve':>10} {'Relapse':>10}")
  1119. print(" " + "-" * 85)
  1120. for treatment_name in ['ketamine', 'ssri', 'neurosteroid']:
  1121. match = comparison.get(treatment_name)
  1122. if match:
  1123. print(f" {treatment_name.capitalize():<15} {match['acute_clean']:>9.1f}% "
  1124. f"{match['acute_standard']:>9.1f}% {match['acute_combined']:>9.1f}% "
  1125. f"{match['acute_extreme']:>9.1f}% {match['improvement']:>+9.1f}% "
  1126. f"{match['relapse_drop']:>9.1f}%")
  1127. else:
  1128. print(f" {treatment_name.capitalize():<15} {'N/A':>10} {'N/A':>10} {'N/A':>10} "
  1129. f"{'N/A':>10} {'N/A':>10} {'N/A':>10}")
  1130. # Neurosteroid-specific: off-medication performance
  1131. neuro_match = comparison.get('neurosteroid')
  1132. if neuro_match and 'off_med_combined' in neuro_match:
  1133. print(f"\n NEUROSTEROID OFF-MEDICATION:")
  1134. print(f" Combined stress (on medication): {neuro_match['acute_combined']:.1f}%")
  1135. print(f" Combined stress (off medication): {neuro_match['off_med_combined']:.1f}%")
  1136. print(f" Performance reversal: {neuro_match['off_med_reversal']:+.1f}%")
  1137. # ========================================================================
  1138. # PHASE 8: Cross-Treatment Comparison at Each Dose Level
  1139. # ========================================================================
  1140. print("\n" + "=" * 80)
  1141. print(" CROSS-TREATMENT RANKINGS AT EACH DOSE LEVEL")
  1142. print("=" * 80)
  1143. for level_key, comparison in results['iso_dose_comparisons'].items():
  1144. target_dose = comparison['target_dose']
  1145. print(f"\n {level_key.upper().replace('_', ' ')}: Dose ≈ {target_dose:.6f}")
  1146. print(" " + "-" * 60)
  1147. valid_treatments = []
  1148. for treatment_name in ['ketamine', 'ssri', 'neurosteroid']:
  1149. match = comparison.get(treatment_name)
  1150. if match:
  1151. valid_treatments.append({
  1152. 'name': treatment_name.capitalize(),
  1153. 'combined': match['acute_combined'],
  1154. 'improvement': match['improvement'],
  1155. 'relapse_drop': match['relapse_drop'],
  1156. 'efficiency': compute_efficiency(match['improvement'], match['dose'].l1_norm)
  1157. })
  1158. if valid_treatments:
  1159. # Rank by combined performance
  1160. by_combined = sorted(valid_treatments, key=lambda x: x['combined'], reverse=True)
  1161. print(f"\n Ranking by Combined Stress Performance:")
  1162. for rank, t in enumerate(by_combined, 1):
  1163. print(f" {rank}. {t['name']:<15} {t['combined']:.1f}%")
  1164. # Rank by relapse resistance (lower is better)
  1165. by_relapse = sorted(valid_treatments, key=lambda x: x['relapse_drop'])
  1166. print(f"\n Ranking by Relapse Resistance (lower drop = better):")
  1167. for rank, t in enumerate(by_relapse, 1):
  1168. print(f" {rank}. {t['name']:<15} {t['relapse_drop']:.1f}% drop")
  1169. # Rank by efficiency
  1170. by_efficiency = sorted(valid_treatments, key=lambda x: x['efficiency'], reverse=True)
  1171. print(f"\n Ranking by Efficiency (improvement per dose):")
  1172. for rank, t in enumerate(by_efficiency, 1):
  1173. print(f" {rank}. {t['name']:<15} {t['efficiency']:.2f}")
  1174. return results
  1175. # ============================================================================
  1176. # MAIN EXPERIMENT
  1177. # ============================================================================
  1178. def run_multi_mechanism_experiment() -> Dict[str, Dict]:
  1179. """
  1180. Compare ketamine, SSRI, and neurosteroid treatment mechanisms.
  1181. All treatments start from identical 95% sparse (depressed) networks.
  1182. """
  1183. print("\n" + "="*80)
  1184. print(" MULTI-MECHANISM ANTIDEPRESSANT COMPARISON EXPERIMENT")
  1185. print("="*80)
  1186. print("""
  1187. COMPARING THREE ANTIDEPRESSANT MECHANISMS:
  1188. ┌─────────────────┬─────────────────────────────────────────────────────────┐
  1189. │ Mechanism │ Key Feature │
  1190. ├─────────────────┼─────────────────────────────────────────────────────────┤
  1191. │ Ketamine │ Gradient-guided synaptogenesis (↑ density) │
  1192. │ SSRI │ Gradual noise reduction (stabilizes existing weights) │
  1193. │ Neurosteroid │ Tonic inhibition (damps activity, bounds firing) │
  1194. └─────────────────┴─────────────────────────────────────────────────────────┘
  1195. All treatments start from identical 95% sparse (depressed) networks.
  1196. """)
  1197. # ========================================================================
  1198. # PREPARE BASE PRUNED MODEL
  1199. # ========================================================================
  1200. print("-"*70)
  1201. print(" Preparing shared pruned baseline...")
  1202. print("-"*70)
  1203. base_model = StressAwareNetwork().to(DEVICE)
  1204. print(f" Training full network ({CONFIG['baseline_epochs']} epochs)...")
  1205. train(base_model, epochs=CONFIG['baseline_epochs'], lr=CONFIG['baseline_lr'])
  1206. base_pruning_mgr = PruningManager(base_model)
  1207. base_pruning_mgr.prune_by_magnitude(sparsity=CONFIG['prune_sparsity'], per_layer=True)
  1208. initial_sparsity = base_pruning_mgr.get_sparsity()
  1209. print(f" Pruned to {initial_sparsity*100:.1f}% sparse")
  1210. # Evaluate untreated state
  1211. print("\n UNTREATED PRUNED STATE:")
  1212. untreated_results = {'sparsity': initial_sparsity * 100}
  1213. untreated_results['clean'] = evaluate(base_model, clean_test_loader, 0.0, 0.0)
  1214. untreated_results['standard'] = evaluate(base_model, test_loader, 0.0, 0.0)
  1215. for stress_name, stress_level in CONFIG['extended_stress_levels'].items():
  1216. untreated_results[f'stress_{stress_name}'] = evaluate(base_model, test_loader, 0.0, stress_level)
  1217. untreated_results['combined'] = evaluate(base_model, test_loader, 1.0, 0.5)
  1218. print(f" Clean: {untreated_results['clean']:.1f}%")
  1219. print(f" Standard: {untreated_results['standard']:.1f}%")
  1220. print(f" Combined stress: {untreated_results['combined']:.1f}%")
  1221. print(f" Extreme stress: {untreated_results['stress_extreme']:.1f}%")
  1222. # Save state for cloning
  1223. base_state_dict = {k: v.clone() for k, v in base_model.state_dict().items()}
  1224. base_masks = {k: v.clone() for k, v in base_pruning_mgr.masks.items()}
  1225. results = {'untreated': untreated_results}
  1226. # ========================================================================
  1227. # TREATMENT 1: KETAMINE-LIKE
  1228. # ========================================================================
  1229. print("\n" + "="*70)
  1230. print(" TREATMENT 1: KETAMINE-LIKE (Synaptogenesis)")
  1231. print("="*70)
  1232. ketamine_model = StressAwareNetwork().to(DEVICE)
  1233. ketamine_model.load_state_dict(base_state_dict)
  1234. ketamine_mgr = PruningManager(ketamine_model)
  1235. ketamine_mgr.masks = {k: v.clone() for k, v in base_masks.items()}
  1236. ketamine_mgr.apply_masks()
  1237. ketamine_stats = ketamine_treatment(ketamine_model, ketamine_mgr,
  1238. regrow_fraction=CONFIG['comparison_ketamine_regrow'],
  1239. consolidation_epochs=CONFIG['comparison_ketamine_epochs'])
  1240. print("\n POST-TREATMENT EVALUATION:")
  1241. ketamine_results = {'sparsity': ketamine_mgr.get_sparsity() * 100}
  1242. ketamine_results['clean'] = evaluate(ketamine_model, clean_test_loader, 0.0, 0.0)
  1243. ketamine_results['standard'] = evaluate(ketamine_model, test_loader, 0.0, 0.0)
  1244. for stress_name, stress_level in CONFIG['extended_stress_levels'].items():
  1245. ketamine_results[f'stress_{stress_name}'] = evaluate(ketamine_model, test_loader, 0.0, stress_level)
  1246. ketamine_results['combined'] = evaluate(ketamine_model, test_loader, 1.0, 0.5)
  1247. print(f" Clean: {ketamine_results['clean']:.1f}%")
  1248. print(f" Standard: {ketamine_results['standard']:.1f}%")
  1249. print(f" Combined stress: {ketamine_results['combined']:.1f}%")
  1250. print(f" Extreme stress: {ketamine_results['stress_extreme']:.1f}%")
  1251. # Relapse simulation
  1252. print("\n RELAPSE SIMULATION:")
  1253. pre_relapse = ketamine_results['combined']
  1254. pre_sparsity = ketamine_mgr.get_sparsity()
  1255. target_sparsity = min(pre_sparsity + (1 - pre_sparsity) * 0.40, 0.99)
  1256. ketamine_mgr.prune_by_magnitude(sparsity=target_sparsity, per_layer=True)
  1257. ketamine_mgr.apply_masks()
  1258. post_relapse = evaluate(ketamine_model, test_loader, 1.0, 0.5)
  1259. ketamine_results['relapse_drop'] = pre_relapse - post_relapse
  1260. print(f" Combined: {pre_relapse:.1f}% → {post_relapse:.1f}% (drop: {ketamine_results['relapse_drop']:.1f}%)")
  1261. results['ketamine'] = ketamine_results
  1262. # ========================================================================
  1263. # TREATMENT 2: SSRI-LIKE
  1264. # ========================================================================
  1265. print("\n" + "="*70)
  1266. print(" TREATMENT 2: SSRI-LIKE (Gradual Stabilization)")
  1267. print("="*70)
  1268. ssri_model = StressAwareNetwork().to(DEVICE)
  1269. ssri_model.load_state_dict(base_state_dict)
  1270. ssri_mgr = PruningManager(ssri_model)
  1271. ssri_mgr.masks = {k: v.clone() for k, v in base_masks.items()}
  1272. ssri_mgr.apply_masks()
  1273. ssri_stats = ssri_treatment(ssri_model, ssri_mgr,
  1274. epochs=CONFIG['comparison_ssri_epochs'],
  1275. learning_rate=CONFIG['monoaminergic_lr'],
  1276. initial_stress=CONFIG['monoaminergic_initial_stress'],
  1277. print_interval=25)
  1278. print("\n POST-TREATMENT EVALUATION:")
  1279. ssri_results = {'sparsity': ssri_mgr.get_sparsity() * 100}
  1280. ssri_results['clean'] = evaluate(ssri_model, clean_test_loader, 0.0, 0.0)
  1281. ssri_results['standard'] = evaluate(ssri_model, test_loader, 0.0, 0.0)
  1282. for stress_name, stress_level in CONFIG['extended_stress_levels'].items():
  1283. ssri_results[f'stress_{stress_name}'] = evaluate(ssri_model, test_loader, 0.0, stress_level)
  1284. ssri_results['combined'] = evaluate(ssri_model, test_loader, 1.0, 0.5)
  1285. print(f" Clean: {ssri_results['clean']:.1f}%")
  1286. print(f" Standard: {ssri_results['standard']:.1f}%")
  1287. print(f" Combined stress: {ssri_results['combined']:.1f}%")
  1288. print(f" Extreme stress: {ssri_results['stress_extreme']:.1f}%")
  1289. # Relapse simulation
  1290. print("\n RELAPSE SIMULATION:")
  1291. pre_relapse = ssri_results['combined']
  1292. pre_sparsity = ssri_mgr.get_sparsity()
  1293. target_sparsity = min(pre_sparsity + (1 - pre_sparsity) * 0.40, 0.99)
  1294. ssri_mgr.prune_by_magnitude(sparsity=target_sparsity, per_layer=True)
  1295. ssri_mgr.apply_masks()
  1296. post_relapse = evaluate(ssri_model, test_loader, 1.0, 0.5)
  1297. ssri_results['relapse_drop'] = pre_relapse - post_relapse
  1298. print(f" Combined: {pre_relapse:.1f}% → {post_relapse:.1f}% (drop: {ssri_results['relapse_drop']:.1f}%)")
  1299. results['ssri'] = ssri_results
  1300. # ========================================================================
  1301. # TREATMENT 3: NEUROSTEROID-LIKE
  1302. # ========================================================================
  1303. print("\n" + "="*70)
  1304. print(" TREATMENT 3: NEUROSTEROID-LIKE (Tonic Inhibition)")
  1305. print("="*70)
  1306. neuro_model = StressAwareNetwork().to(DEVICE)
  1307. neuro_model.load_state_dict(base_state_dict)
  1308. neuro_mgr = PruningManager(neuro_model)
  1309. neuro_mgr.masks = {k: v.clone() for k, v in base_masks.items()}
  1310. neuro_mgr.apply_masks()
  1311. neuro_stats = neurosteroid_treatment(neuro_model, neuro_mgr,
  1312. inhibition_strength=CONFIG['neurosteroid_inhibition_strength'],
  1313. use_tanh=CONFIG['neurosteroid_use_tanh'],
  1314. consolidation_epochs=CONFIG['neurosteroid_consolidation_epochs'])
  1315. # Evaluate WITH modulation active (patient on medication)
  1316. print("\n POST-TREATMENT EVALUATION (with modulation active):")
  1317. neuro_results = {'sparsity': neuro_mgr.get_sparsity() * 100}
  1318. neuro_results['clean'] = evaluate_with_neurosteroid(neuro_model, clean_test_loader, 0.0, 0.0)
  1319. neuro_results['standard'] = evaluate_with_neurosteroid(neuro_model, test_loader, 0.0, 0.0)
  1320. for stress_name, stress_level in CONFIG['extended_stress_levels'].items():
  1321. neuro_results[f'stress_{stress_name}'] = evaluate_with_neurosteroid(neuro_model, test_loader, 0.0, stress_level)
  1322. neuro_results['combined'] = evaluate_with_neurosteroid(neuro_model, test_loader, 1.0, 0.5)
  1323. print(f" Clean: {neuro_results['clean']:.1f}%")
  1324. print(f" Standard: {neuro_results['standard']:.1f}%")
  1325. print(f" Combined stress: {neuro_results['combined']:.1f}%")
  1326. print(f" Extreme stress: {neuro_results['stress_extreme']:.1f}%")
  1327. # Test WITHOUT modulation (medication discontinued)
  1328. print("\n EVALUATION WITHOUT MODULATION (medication discontinued):")
  1329. neuro_model.set_inhibition(1.0, False)
  1330. off_med_combined = evaluate(neuro_model, test_loader, 1.0, 0.5)
  1331. off_med_extreme = evaluate(neuro_model, test_loader, 0.0, 2.5)
  1332. print(f" Combined stress: {off_med_combined:.1f}%")
  1333. print(f" Extreme stress: {off_med_extreme:.1f}%")
  1334. neuro_results['off_medication_combined'] = off_med_combined
  1335. neuro_results['off_medication_extreme'] = off_med_extreme
  1336. # Restore modulation for relapse test
  1337. neuro_model.set_inhibition(CONFIG['neurosteroid_inhibition_strength'],
  1338. CONFIG['neurosteroid_use_tanh'])
  1339. # Relapse simulation
  1340. print("\n RELAPSE SIMULATION (with modulation active):")
  1341. pre_relapse = neuro_results['combined']
  1342. pre_sparsity = neuro_mgr.get_sparsity()
  1343. target_sparsity = min(pre_sparsity + (1 - pre_sparsity) * 0.40, 0.99)
  1344. neuro_mgr.prune_by_magnitude(sparsity=target_sparsity, per_layer=True)
  1345. neuro_mgr.apply_masks()
  1346. post_relapse = evaluate_with_neurosteroid(neuro_model, test_loader, 1.0, 0.5)
  1347. neuro_results['relapse_drop'] = pre_relapse - post_relapse
  1348. print(f" Combined: {pre_relapse:.1f}% → {post_relapse:.1f}% (drop: {neuro_results['relapse_drop']:.1f}%)")
  1349. results['neurosteroid'] = neuro_results
  1350. # ========================================================================
  1351. # COMPREHENSIVE COMPARISON
  1352. # ========================================================================
  1353. print("\n" + "="*80)
  1354. print(" COMPREHENSIVE COMPARISON: ALL TREATMENTS")
  1355. print("="*80)
  1356. treatments = ['untreated', 'ketamine', 'ssri', 'neurosteroid']
  1357. labels = {'untreated': 'Untreated (pruned)', 'ketamine': 'Ketamine-like',
  1358. 'ssri': 'SSRI-like', 'neurosteroid': 'Neurosteroid-like'}
  1359. print(f"\n {'Treatment':<22} {'Sparsity':>10} {'Clean':>8} {'Standard':>10} "
  1360. f"{'Combined':>10} {'Extreme':>10} {'Relapse':>10}")
  1361. print(" " + "-"*85)
  1362. for t in treatments:
  1363. r = results[t]
  1364. relapse = r.get('relapse_drop', 'N/A')
  1365. relapse_str = f"{relapse:.1f}%" if isinstance(relapse, float) else relapse
  1366. print(f" {labels[t]:<22} {r['sparsity']:>9.1f}% {r['clean']:>7.1f}% "
  1367. f"{r['standard']:>9.1f}% {r['combined']:>9.1f}% "
  1368. f"{r['stress_extreme']:>9.1f}% {relapse_str:>10}")
  1369. # Stress resilience profile
  1370. print("\n STRESS RESILIENCE PROFILE:")
  1371. print(f"\n {'Treatment':<22} {'None':>8} {'Moderate':>10} {'High':>8} {'Severe':>8} {'Extreme':>10}")
  1372. print(" " + "-"*70)
  1373. for t in treatments:
  1374. r = results[t]
  1375. print(f" {labels[t]:<22} {r['stress_none']:>7.1f}% {r['stress_moderate']:>9.1f}% "
  1376. f"{r['stress_high']:>7.1f}% {r['stress_severe']:>7.1f}% {r['stress_extreme']:>9.1f}%")
  1377. # ========================================================================
  1378. # ANALYSIS
  1379. # ========================================================================
  1380. print("\n" + "-"*80)
  1381. print(" ANALYSIS")
  1382. print("-"*80)
  1383. ket, ssri, neuro = results['ketamine'], results['ssri'], results['neurosteroid']
  1384. untreated = results['untreated']
  1385. print("\n 1. IMPROVEMENT FROM UNTREATED STATE (Combined Stress):")
  1386. print(f" Ketamine: {untreated['combined']:.1f}% → {ket['combined']:.1f}% (+{ket['combined'] - untreated['combined']:.1f}%)")
  1387. print(f" SSRI: {untreated['combined']:.1f}% → {ssri['combined']:.1f}% (+{ssri['combined'] - untreated['combined']:.1f}%)")
  1388. print(f" Neurosteroid:{untreated['combined']:.1f}% → {neuro['combined']:.1f}% (+{neuro['combined'] - untreated['combined']:.1f}%)")
  1389. print("\n 2. STRUCTURAL VS FUNCTIONAL CHANGES:")
  1390. print(f" Ketamine sparsity: {ket['sparsity']:.1f}% (REDUCED from 95%)")
  1391. print(f" SSRI sparsity: {ssri['sparsity']:.1f}% (UNCHANGED)")
  1392. print(f" Neurosteroid sparsity:{neuro['sparsity']:.1f}% (UNCHANGED)")
  1393. print("\n → Ketamine is the ONLY treatment that adds new connections")
  1394. print("\n 3. EXTREME STRESS RESILIENCE (σ=2.5):")
  1395. print(f" Ketamine: {ket['stress_extreme']:.1f}%")
  1396. print(f" SSRI: {ssri['stress_extreme']:.1f}%")
  1397. print(f" Neurosteroid:{neuro['stress_extreme']:.1f}%")
  1398. print("\n 4. NEUROSTEROID MEDICATION DEPENDENCE:")
  1399. print(f" Combined ON medication: {neuro['combined']:.1f}%")
  1400. print(f" Combined OFF medication: {neuro['off_medication_combined']:.1f}%")
  1401. print(f" Extreme ON medication: {neuro['stress_extreme']:.1f}%")
  1402. print(f" Extreme OFF medication: {neuro['off_medication_extreme']:.1f}%")
  1403. print("\n 5. RELAPSE VULNERABILITY:")
  1404. print(f" Ketamine: {ket['relapse_drop']:.1f}% drop")
  1405. print(f" SSRI: {ssri['relapse_drop']:.1f}% drop")
  1406. print(f" Neurosteroid:{neuro['relapse_drop']:.1f}% drop")
  1407. # ========================================================================
  1408. # CLINICAL INTERPRETATION
  1409. # ========================================================================
  1410. print("\n" + "-"*80)
  1411. print(" CLINICAL INTERPRETATION")
  1412. print("-"*80)
  1413. print("""
  1414. KEY FINDINGS:
  1415. 1. MECHANISM MATTERS: Different antidepressants work through distinct routes
  1416. - Ketamine REBUILDS: Adds new synapses, restores structural density
  1417. - SSRIs REFINE: Strengthen existing pathways via gradual adaptation
  1418. - Neurosteroids STABILIZE: Damp hyperexcitability, bound activity range
  1419. 2. SPEED-DURABILITY TRADEOFF:
  1420. - Ketamine: Fast onset, durable changes (new structure persists)
  1421. - Neurosteroid: Fast onset, medication-dependent (dynamics reset if stopped)
  1422. - SSRI: Slow onset, moderate durability (refined weights persist)
  1423. 3. TREATMENT SELECTION IMPLICATIONS:
  1424. ┌──────────────────────────────────────┬──────────────────────────────────┐
  1425. │ Clinical Scenario │ Suggested Mechanism │
  1426. ├──────────────────────────────────────┼──────────────────────────────────┤
  1427. │ Severe, treatment-resistant MDD │ Ketamine (structural repair) │
  1428. │ Postpartum depression, acute crisis │ Neurosteroid (rapid stabilize) │
  1429. │ Mild-moderate, first-line │ SSRI (gradual, acceptable) │
  1430. │ Recurrent with high relapse risk │ Ketamine (durable structure) │
  1431. │ Hyperexcitable/anxious component │ Neurosteroid (activity damping) │
  1432. └──────────────────────────────────────┴──────────────────────────────────┘
  1433. 4. COMBINATION THERAPY RATIONALE:
  1434. - Ketamine + SSRI: Structural + neuromodulatory benefits
  1435. - Neurosteroid + SSRI: Rapid stabilization while waiting for SSRI onset
  1436. - Ketamine + Psychotherapy: New synapses + activity-guided consolidation
  1437. """)
  1438. # ========================================================================
  1439. # ISO-DOSE COMPARISON EXPERIMENT
  1440. # ========================================================================
  1441. iso_dose_results = run_iso_dose_comparison_experiment(
  1442. base_state_dict, base_masks, untreated_results
  1443. )
  1444. results['iso_dose'] = iso_dose_results
  1445. return results
  1446. # ============================================================================
  1447. # ENTRY POINT
  1448. # ============================================================================
  1449. if __name__ == "__main__":
  1450. print("\n" + "#"*80)
  1451. print("#" + " "*78 + "#")
  1452. print("#" + " MULTI-MECHANISM ANTIDEPRESSANT COMPARISON ".center(78) + "#")
  1453. print("#" + " Ketamine vs SSRI vs Neurosteroid ".center(78) + "#")
  1454. print("#" + " WITH ISO-DOSE FAIR COMPARISON PIPELINE ".center(78) + "#")
  1455. print("#" + " "*78 + "#")
  1456. print("#"*80)
  1457. results = run_multi_mechanism_experiment()
  1458. print("\n" + "="*80)
  1459. print(" EXPERIMENT COMPLETE")
  1460. print("="*80)
  1461. # ========================================================================
  1462. # FINAL SUMMARY: ISO-DOSE FINDINGS
  1463. # ========================================================================
  1464. print("\n" + "="*80)
  1465. print(" FINAL SUMMARY: ISO-DOSE COMPARISON FINDINGS")
  1466. print("="*80)
  1467. if 'iso_dose' in results and 'summary_stats' in results['iso_dose']:
  1468. stats = results['iso_dose']['summary_stats']
  1469. print("\n DOSE CHARACTERISTICS BY TREATMENT:")
  1470. print(f"\n {'Treatment':<15} {'Dose Range':>25} {'Mean Dose':>15}")
  1471. print(" " + "-" * 60)
  1472. for treatment in ['ketamine', 'ssri', 'neurosteroid']:
  1473. s = stats[treatment]
  1474. dose_range = f"{s['dose_range'][0]:.6f} - {s['dose_range'][1]:.6f}"
  1475. print(f" {treatment.capitalize():<15} {dose_range:>25} {s['dose_mean']:>15.6f}")
  1476. print("\n PERFORMANCE RANGE BY TREATMENT:")
  1477. print(f"\n {'Treatment':<15} {'Best Improve':>15} {'Best Relapse':>15} {'Max Efficiency':>15}")
  1478. print(" " + "-" * 65)
  1479. for treatment in ['ketamine', 'ssri', 'neurosteroid']:
  1480. s = stats[treatment]
  1481. print(f" {treatment.capitalize():<15} {s['best_improvement']:>+14.1f}% "
  1482. f"{s['best_relapse']:>14.1f}% {s['max_efficiency']:>15.2f}")
  1483. if 'iso_dose' in results and 'efficiency_analysis' in results['iso_dose']:
  1484. eff = results['iso_dose']['efficiency_analysis']
  1485. print("\n EFFICIENCY COMPARISON:")
  1486. print(f"\n {'Treatment':<15} {'Max Efficiency':>15} {'Mean Efficiency':>16} {'Best Config':>15}")
  1487. print(" " + "-" * 65)
  1488. for treatment in ['ketamine', 'ssri', 'neurosteroid']:
  1489. e = eff[treatment]
  1490. config_str = str(e['best_config']) if e['best_config'] is not None else 'N/A'
  1491. print(f" {treatment.capitalize():<15} {e['max_efficiency']:>15.2f} "
  1492. f"{e['mean_efficiency']:>16.2f} {config_str:>15}")
  1493. print("\n" + "="*80 + "\n")
  1494. # %% [markdown]
  1495. # # The End

068A_antidep_modes_v2.ipynb at commit 7c25bce, no license · at the source

Overview

Authors: Ngo Cheung1
ORCID iDs: Ngo Cheung
  1. Psychiatry, Cheung Ngo Medical Limited, Hong Kong, HKG
Journal: Cureus, volume 18, issue 3, article e105040
Dates: accepted 11 March 2026; published online 11 March 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.7759/cureus.105040 · PMID 41822249 · PMCID PMC12978026 · OpenAlex W7135058070
Open access: diamond, a free copy (OpenAlex)
Status: code verified
Categories: depression (population), clinical / translational (subfield)
Methods: Single-unit activity, calcium imaging
Keywords: computational psychiatry, depression, ketamine, neurosteroid, selective serotonin reuptake inhibitors (ssris)
Topic: Treatment of Major Depression (Pharmacology, Medicine), according to OpenAlex
Citations: not cited yet (Europe PMC); 16 references in the paper

Abstract

Background: Major depressive disorder (MDD) is increasingly viewed as a disorder of impaired neural plasticity, yet the mechanisms underlying diverse antidepressant classes - glutamatergic (e.g., ketamine), monoaminergic (e.g., selective serotonin reuptake inhibitors (SSRIs)), and GABAergic (e.g., neurosteroids) - remain incompletely integrated. The objective of this study was to extend a pruning-plasticity model of depression and directly compare, from an identical severely pruned baseline state, the efficacy, stress resilience, durability, and relapse vulnerability of three mechanistically distinct interventions: ketamine-like targeted synaptogenesis, SSRI-like gradual refinement of existing connectivity, and neurosteroid-like tonic inhibition. Computational models offer a controlled means to compare these pathways, but prior work has typically examined single mechanisms.

Methods: We extended a pruning-plasticity model of depression by applying 95% magnitude-based synaptic elimination to overparameterized feed-forward networks trained on a four-class Gaussian classification task. From identical pruned states, three interventions were tested: ketamine-like gradient-guided regrowth (50% reinstatement) with consolidation; SSRI-like prolonged low-learning-rate training with gradual internal noise reduction; and neurosteroid-like global tonic inhibition (30% damping plus tanh activations) with brief consolidation. Outcomes included baseline accuracy, resilience to graded internal activation noise (up to σ = 2.5) plus input perturbation, and relapse vulnerability after an additional 40% pruning.

Results: All treatments restored near-ceiling performance on unchallenged inputs. Ketamine-like synaptogenesis uniquely reduced sparsity (to ~47%) and conferred superior stress resilience (extreme noise accuracy 84.5%) with near-zero relapse drop (−0.2%). SSRI-like refinement improved combined stress accuracy to 83.5% but showed limited extreme noise tolerance (44.0%) and substantial relapse vulnerability (10.8% drop). Neurosteroid-like inhibition achieved rapid combined stress recovery (97.5%) while active, but was state-dependent (decline upon removal) with poor extreme noise buffering (42.5%) and moderate relapse drop (4.1%).

Conclusions: These simulations demonstrate that antidepressants operate through mechanistically distinct routes-structural rebuilding (ketamine), gradual optimization of existing connectivity (SSRIs), or reversible dynamic stabilization (neurosteroids)-yielding trade-offs in onset speed, durability, and stress resilience. The findings support a multifaceted plasticity framework for depression and provide computational rationale for mechanism-based treatment selection and combination strategies.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 10 matches between paragraphs and lines of code.

cheungngo/Divergent-Mechanisms-of-Antidepressant-Efficacy

License: none: the authors keep all their rights
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: 7c25bced1dd4abb138cb9117a07dd0c03211e6d8, 29 January 2026
Languages: Jupyter (2)
Size: 5 files, 2 scripts
Software Heritage: not archived
Found in: the text, “Materials and methods”
Holds: README, CITATION.cff, 2 notebooks
Not found: license file, environment file, tests, continuous integration, documentation
Tools: NumPy (2 files), PyTorch (2 files)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
3 files

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 2 scripts, each with its path and the digest of its content;
  • 10 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 30 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 1 author, 5 keywords, 14 references.

Cite

This paper

Cheung, N. (2026). Divergent Mechanisms of Antidepressant Efficacy: A Unified Computational Comparison of Synaptogenesis, Stabilization, and Tonic Inhibition in a Model of Depression. Cureus, 18(3), e105040. https://doi.org/10.7759/cureus.105040

BibTeX

@article{cheung2026divergent,
author = {Cheung, Ngo},
title = {{Divergent Mechanisms of Antidepressant Efficacy: A Unified Computational Comparison of Synaptogenesis, Stabilization, and Tonic Inhibition in a Model of Depression}},
journal = {Cureus},
year = {2026},
month = mar,
volume = {18},
number = {3},
pages = {e105040},
publisher = {Cureus Inc.},
issn = {2168-8184},
doi = {10.7759/cureus.105040},
url = {https://doi.org/10.7759/cureus.105040},
pmid = {41822249},
pmcid = {PMC12978026}
}

RIS

TY - JOUR
AU - Cheung, Ngo
TI - Divergent Mechanisms of Antidepressant Efficacy: A Unified Computational Comparison of Synaptogenesis, Stabilization, and Tonic Inhibition in a Model of Depression
T2 - Cureus
J2 - Cureus
PY - 2026
DA - 2026/03/11
VL - 18
IS - 3
SP - e105040
SN - 2168-8184
PB - Cureus Inc.
DO - 10.7759/cureus.105040
UR - https://doi.org/10.7759/cureus.105040
LA - en
ER -

CSL-JSON

{
"id": "10.7759/cureus.105040",
"type": "article-journal",
"title": "Divergent Mechanisms of Antidepressant Efficacy: A Unified Computational Comparison of Synaptogenesis, Stabilization, and Tonic Inhibition in a Model of Depression",
"container-title": "Cureus",
"author": [
{
"family": "Cheung",
"given": "Ngo"
}
],
"container-title-short": "Cureus",
"volume": "18",
"issue": "3",
"page": "e105040",
"DOI": "10.7759/cureus.105040",
"PMID": "41822249",
"PMCID": "PMC12978026",
"ISSN": "2168-8184",
"publisher": "Cureus Inc.",
"URL": "https://doi.org/10.7759/cureus.105040",
"language": "en",
"issued": {
"date-parts": [
[
2026,
3,
11
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.3389/fpsyt.2026.1855963 [code]
Transient entanglement in minimal open XXZ spin chains: a toy-model analogy for microtubule-inspired quantum biology.
Journal: Frontiers in psychiatry
In common: clinical / translational, 1 reference, author Ngo Cheung
[2] doi:10.3389/fpsyt.2026.1752101 [code]
Case Report: Oral glutamatergic augmentation for trauma-related disorders with fluoxetine-/bupropion-potentiated dextromethorphan ± piracetam: a four-patient case series.
Journal: Frontiers in psychiatry
In common: clinical / translational, author Ngo Cheung
[3] doi:10.1038/s41467-026-77248-y [code]
The mPFC-reuniens-hippocampus pathway links brain circuitry and neural plasticity in antidepressant response.
Journal: Nature communications
In common: NumPy, depression, 2 references
[4] doi:10.1016/j.isci.2026.116825 [code]
Social hierarchy shapes behavioral and transcriptional responses to chronic stress and ketamine in male mice.
Journal: iScience
In common: PyTorch, NumPy, depression, 1 reference
[5] doi:10.1038/s41467-026-75697-z
Delayed astrocyte development impairs Sema6a-Plxna2/4-mediated astrocyte-neuron crosstalk and causes depressive-like behavior.
Journal: Nature communications
In common: depression, 2 references
[6] doi:10.1038/s41398-026-04137-9 [code]
An integrative mendelian randomisation and drug mechanism framework for target prioritisation and therapeutic repurposing in major depression.
Journal: Translational psychiatry
In common: NumPy, depression, clinical / translational, 1 reference
[7] doi:10.3389/fncom.2026.1799705
Structural synaptogenesis superior to functional modulation in a pruning-based recurrent network model of OCD.
Journal: Frontiers in computational neuroscience
In common: 2 references
[8] doi:10.1038/s42003-026-10094-2 [code]
E/I imbalance and internal noise cause weak neural representations and face recognition challenges in ASD.
Journal: Communications biology
In common: PyTorch, NumPy, 1 reference
[9] doi:10.1002/hbm.70628 [code]
EEG Biomarkers for Affective Disorders Diagnosis: An Evaluation and Validation Study.
Journal: Human brain mapping
In common: PyTorch, NumPy, depression, clinical / translational
[10] doi:10.1038/s41380-026-03691-4 [code]
Breaking the norm: population-scale deviations of brain structure in depression and anxiety.
Journal: Molecular psychiatry
In common: PyTorch, NumPy, depression, clinical / translational

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.