Health system learning enables generalist neuroimaging models.
The 15 matches
- [1] § Methods › NeuroVFM training with Vol-JEPA ↔ neurovfm/systems/pretraining.py, lines 24–55 · score 0.80 · teacher network, student encoder, predictor module, teacher encoder, EMA, JEPA
- [2] § Methods › Vision instruction tuning for radiology report generation › NeuroVFM-LLaVA architecture ↔ neurovfm/models/vlm.py, lines 332–474 · score 0.77 · embedding space, connector module, visual tokens, concatenated, Perceiver, scan
- [3] § Methods › Diagnostic evaluation of NeuroVFM ↔ neurovfm/systems/classification.py, lines 112–193 · score 0.71 · cross entropy, class weighted, macro, thresholds, Hyperparameters, binary
- [4] § Methods › Series preprocessing ↔ neurovfm/data/preprocess.py, lines 167–283 · score 0.69 · Background masks, subdural, clipped, width, axis, blood
- [5] § Methods › Evaluation of preliminary report generation ↔ neurovfm/pipelines/interpreter.py, lines 1–22 · score 0.68 · OpenAI, 5–2025, verbosity, medium, GPT, triage
- [6] § Learning with Vol-JEPA › NeuroVFM enables preliminary report generation ↔ neurovfm/models/vlm.py, lines 477–496 · score 0.67 · visual instruction tuning, vision language models, LLaVA, style, Multimodal, NeuroImages
- [7] § Methods › Grounded diagnoses with multiple instance learning ↔ neurovfm/models/mil.py, lines 390–497 · score 0.66 · attention scores, attention weights, bag, softmax, sum, logits
- [8] § Methods › Vision instruction tuning for radiology report generation ↔ neurovfm/models/vlm.py, lines 477–496 · score 0.62 · visual instruction tuning, LLaVA, style, multimodal, architectural, LLM
- [9] § Learning with Vol-JEPA › NeuroVFM enables preliminary report generation ↔ neurovfm/systems/llm_sft.py, lines 1–20 · score 0.62 · visual instruction tuning, vision language models, fine tuned, frozen, encoder
- [10] § Methods › NeuroVFM training with Vol-JEPA ↔ neurovfm/data/preprocess.py, lines 167–283 · score 0.62 · CT scans, CT window, subdural, crop, axis, bone
- [11] § Methods › Grounded diagnoses with multiple instance learning ↔ neurovfm/models/mil.py, lines 282–326 · score 0.60 · standard AB MIL, aggregate, patch, classify, module, models
- [12] § Methods › Computational hardware and software ↔ neurovfm/pipelines/diagnostic.py, lines 16–132 · score 0.59 · automatic mixed precision, AMP, CPU, aggregate, Vol, PyTorch
- [13] § Methods › Computational hardware and software ↔ neurovfm/pipelines/encoder.py, lines 18–111 · score 0.57 · automatic mixed precision, AMP, CPU, Vol, PyTorch, batch
- [14] § Methods › NeuroVFM training with Vol-JEPA ↔ neurovfm/datasets/collators.py, lines 1–19 · score 0.56 · patch dropout, variable length, crops, sequences, batch
- [15] § Learning with Vol-JEPA › NeuroVFM enables preliminary report generation ↔ neurovfm/pipelines/interpreter.py, lines 24–102 · score 0.55 · radiologist findings, clinical indication, API, Acuity, NeuroVFM, pipeline
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
Python · 857 lines · 33 KB · MIT · 3 matches
- """
- Vision Language Model for 3D Medical Imaging
- Implements a LLaVA-style multimodal model for radiology report generation.
- Combines a pretrained vision encoder with a language model via a perceiver-based connector.
- Generation follows a JSON schema via outlines/pydantic.
- """
- import logging
- from typing import Any, Dict, List, Optional, Tuple, Union
- import outlines
- import outlines.caching as cache
- import torch
- import torch.nn as nn
- from outlines.processors.structured import JSONLogitsProcessor
- from peft import LoraConfig, TaskType, get_peft_model
- from pydantic import BaseModel, Field
- from timm.layers.mlp import Mlp
- from transformers import (
- AutoModelForCausalLM,
- AutoTokenizer,
- GenerationConfig,
- PreTrainedModel,
- PreTrainedTokenizer,
- )
- from neurovfm.models.perceiver import PerceiverResampler
- from neurovfm.models.vit import get_vit_backbone
- class ShortReport(BaseModel):
- # JSON schema for findings generation
- exam_type: str = Field(..., description="The type of imaging study.")
- findings: List[str] = Field(..., description="A list of key radiological findings.")
- class LanguageModel(nn.Module):
- """
- Wrapper for a pretrained LLM.
- """
- def __init__(
- self,
- model_name_or_path: str,
- use_gradient_checkpointing: bool = False,
- lora_params: Optional[Dict[str, Any]] = None,
- attn_implementation: str = "flash_attention_2",
- ):
- super().__init__()
- self.model_name_or_path = model_name_or_path
- self.use_gradient_checkpointing = use_gradient_checkpointing
- self.lora_params = lora_params
- # init LLM and tokenizer
- self.llm: PreTrainedModel = AutoModelForCausalLM.from_pretrained(
- model_name_or_path,
- trust_remote_code=True,
- torch_dtype=torch.bfloat16,
- low_cpu_mem_usage=True,
- attn_implementation=attn_implementation,
- )
- self.tokenizer = AutoTokenizer.from_pretrained(
- model_name_or_path,
- trust_remote_code=True,
- padding_side='left',
- )
- self.image_placeholder_token_id = self.tokenizer.convert_tokens_to_ids('<|image_pad|>') # [hardcoded] image padding token used by Qwen2/3 tokenizer
- if self.use_gradient_checkpointing:
- self.enable_gradient_checkpointing()
- if self.lora_params:
- self.enable_lora(self.lora_params)
- # init structured JSON generator
- # monkey-patch tokenizer attributes required for outlines structured generation compatibility
- self.tokenizer.vocabulary = self.tokenizer.get_vocab()
- self.tokenizer.special_tokens = self.tokenizer.all_special_tokens
- self.tokenizer.convert_token_to_string = lambda token: self.tokenizer.decode([self.tokenizer.vocabulary[token]])
- cache.disable_cache() # disable caching to prevent OverflowError with large vocabularies
- def enable_gradient_checkpointing(self):
- """Enable gradient checkpointing for memory efficiency"""
- if hasattr(self.llm, 'gradient_checkpointing_enable'):
- self.llm.gradient_checkpointing_enable()
- def get_input_embeddings(self, input_ids: torch.Tensor) -> torch.Tensor:
- """
- Get token embeddings from input_ids.
- """
- embeddings = self.llm.get_input_embeddings()(input_ids)
- return embeddings
- @property
- def hidden_size(self) -> int:
- """
- Returns the hidden size of the LLM.
- """
- return self.llm.config.hidden_size
- @property
- def device(self) -> torch.device:
- return self.llm.device
- def forward(
- self,
- input_ids: Optional[torch.Tensor] = None,
- attention_mask: Optional[torch.Tensor] = None,
- inputs_embeds: Optional[torch.Tensor] = None,
- labels: Optional[torch.Tensor] = None,
- **kwargs
- ) -> Dict[str, torch.Tensor]:
- """
- Forward pass through the LLM.
- """
- # ensure consistent dtypes
- if inputs_embeds is not None:
- inputs_embeds = inputs_embeds.to(torch.bfloat16)
- outputs = self.llm(
- input_ids=input_ids,
- attention_mask=attention_mask,
- inputs_embeds=inputs_embeds,
- labels=labels,
- return_dict=True,
- **kwargs
- )
- return outputs
- @torch.inference_mode()
- def generate(
- self,
- inputs_embeds: torch.Tensor,
- attention_mask: Optional[torch.Tensor] = None,
- **generate_kwargs
- ) -> torch.Tensor:
- """
- Generate text autoregressively.
- Args:
- inputs_embeds (torch.Tensor): Combined visual and prompt embeddings.
- Shape: (batch_size, seq_len, embed_dim)
- attention_mask (Optional[torch.Tensor]): Attention mask for inputs_embeds.
- Shape: (batch_size, seq_len)
- **generate_kwargs: Additional arguments for Hugging Face `generate` method
- (e.g., max_length, num_beams, do_sample, temperature).
- Returns:
- torch.Tensor: Generated token IDs.
- """
- # ensure consistent dtype
- inputs_embeds = inputs_embeds.to(torch.bfloat16)
- # pop custom kwarg to avoid passing it to model.generate
- schema_str = generate_kwargs.pop('generation_schema', None)
- logits_processor = None
- if schema_str:
- if schema_str == 'shortreport':
- schema = ShortReport
- else:
- raise ValueError(f"Invalid schema: {schema_str}")
- json_logits_processor = JSONLogitsProcessor(
- schema=schema,
- tokenizer=self.tokenizer,
- tensor_library_name="torch"
- )
- logits_processor = [json_logits_processor]
- # for structured generation, sampling must be disabled
- generate_kwargs['do_sample'] = False
- # create a final configuration dictionary by merging model defaults with user kwargs
- # User-provided kwargs will override the defaults.
- final_config_dict = {
- **self.llm.generation_config.to_dict(),
- **generate_kwargs
- }
- # if sampling is disabled (either explicitly or by using num_beams > 1), remove all sampling-related parameters to avoid warnings
- if not final_config_dict.get('do_sample', False):
- final_config_dict.pop('temperature', None)
- final_config_dict.pop('top_p', None)
- final_config_dict.pop('top_k', None)
- final_config_dict.pop('min_p', None)
- final_config_dict.pop('typical_p', None)
- # create the final GenerationConfig object from the clean dictionary
- generation_config = GenerationConfig.from_dict(final_config_dict)
- self.llm.generation_config = generation_config
- outputs = self.llm.generate(
- inputs_embeds=inputs_embeds,
- attention_mask=attention_mask,
- logits_processor=logits_processor,
- generation_config=generation_config
- )
- return outputs
- def enable_lora(self, lora_config: Optional[Dict[str, Any]] = None):
- lora_config['task_type'] = TaskType.CAUSAL_LM
- peft_config = LoraConfig(**lora_config)
- self.llm = get_peft_model(self.llm, peft_config)
- class VisionEncoder(nn.Module):
- """
- Wrapper for 3D volumetric vision transformer encoder.
- Wraps the pretrained VisionTransformer backbone with optional gradient
- checkpointing. Outputs per-study or per-batch embeddings.
- Note: This module expects pre-normalized inputs. Normalization should be
- handled by the pipeline or training system before calling this encoder.
- Args:
- backbone_cf (Dict): Configuration for get_vit_backbone (which, params)
- checkpoint_path (str, optional): Path to pretrained checkpoint
- use_gradient_checkpointing (bool): Enable gradient checkpointing for memory efficiency
- freeze (bool): Freeze encoder weights. Defaults to True.
- Example:
- >>> encoder_cf = {
- ... 'backbone_cf': {'which': 'vit_base', 'params': {...}},
- ... 'checkpoint_path': '/path/to/checkpoint.ckpt',
- ... 'freeze': True
- ... }
- >>> encoder = VisionEncoder(**encoder_cf)
- >>> embs, coords = encoder(batch) # batch should have normalized 'img' tokens
- """
- def __init__(
- self,
- backbone_cf: Dict[str, Any],
- use_gradient_checkpointing: bool = False,
- freeze: bool = True,
- ):
- super().__init__()
- # initialize backbone
- self.encoder = get_vit_backbone(**backbone_cf)
- self.embed_dim = self.encoder.embed_dim
- # gradient checkpointing
- self.use_gradient_checkpointing = use_gradient_checkpointing
- if self.use_gradient_checkpointing:
- self._enable_gradient_checkpointing()
- # freeze encoder if specified
- if freeze:
- for param in self.encoder.parameters():
- param.requires_grad = False
- self.encoder.eval()
- def _enable_gradient_checkpointing(self):
- """Enable gradient checkpointing for memory efficiency."""
- if hasattr(self.encoder, 'set_grad_checkpointing'):
- self.encoder.set_grad_checkpointing(True)
- elif hasattr(self.encoder, 'blocks'):
- for block in self.encoder.blocks:
- block.grad_checkpointing = True
- def forward(
- self,
- batch: Dict[str, Any],
- return_list: bool = True
- ) -> Union[Tuple[List[torch.Tensor], List[torch.Tensor]], Tuple[torch.Tensor, torch.Tensor]]:
- """
- Forward pass through vision encoder.
- Note: Expects pre-normalized 'img' tokens.
- Args:
- batch (Dict): Batch dictionary with keys:
- - img: (total_seqlen, D) normalized image tokens
- - coords: (total_seqlen, 3) tensor of 3d coordinates
- - series_masks_indices: mask indices for foreground tokens
- - series_cu_seqlens: (n_series+1,) cumulative sequence lengths
- - series_max_len: int, maximum sequence length
- - study_cu_seqlens: (n_studies+1,) study cumulative sequence lengths
- return_list (bool): If True, return list of tensors per study.
- If False, return single concatenated tensor.
- Returns:
- If return_list=True:
- Tuple[List[Tensor], List[Tensor]]: Per-study embeddings and coordinates
- If return_list=False:
- Tuple[Tensor, Tensor]: Concatenated embeddings and coordinates
- """
- # forward pass through encoder
- # output shape: (total_tokens_in_batch, embed_dim)
- emb = self.encoder.forward(
- batch["img"],
- batch["coords"],
- masks=batch["series_masks_indices"],
- cu_seqlens=batch["series_cu_seqlens"],
- max_seqlen=batch["series_max_len"]
- )
- if return_list:
- # split embedding/coordinate tensors by study
- study_embeddings = []
- study_coords_list = []
- study_cu_seqlens = batch["study_cu_seqlens"]
- for study_idx in range(len(study_cu_seqlens) - 1):
- start_idx = study_cu_seqlens[study_idx]
- end_idx = study_cu_seqlens[study_idx + 1]
- study_emb = emb[start_idx:end_idx]
- study_embeddings.append(study_emb.to(torch.bfloat16))
- study_coords = batch["coords"][start_idx:end_idx]
- study_coords_list.append(study_coords)
- return study_embeddings, study_coords_list
- else:
- return emb.to(torch.bfloat16), batch["coords"]
- class VisionConnector(nn.Module):
- """
- Connector module that projects visual tokens to the LLM embedding space using a perceiver resampler.
- Operates on a per-series (scan) basis, compressing each series to a fixed number of tokens before projection.
- Args:
- visual_embed_dim (int): Dimension of visual encoder embeddings
- llm_embed_dim (int): Dimension of LLM embeddings
- perceiver_cfg (Dict): Configuration for PerceiverResampler
- mlp_hidden_dim (int, optional): Hidden dimension for projection MLP
- mlp_drop (float): Dropout rate for MLP. Defaults to 0.0.
- mlp_act_layer (nn.Module): Activation layer for MLP. Defaults to nn.GELU.
- Example:
- >>> connector = VisionConnector(
- ... visual_embed_dim=768,
- ... llm_embed_dim=4096,
- ... perceiver_cfg={'num_queries': 64, 'num_layers': 6, 'num_heads': 8}
- ... )
- >>> projected, lengths = connector(visual_tokens, series_cu_seqlens)
- """
- def __init__(
- self,
- visual_embed_dim: int,
- llm_embed_dim: int,
- perceiver_cfg: Dict[str, Any],
- mlp_hidden_dim: Optional[int] = None,
- mlp_drop: float = 0.0,
- mlp_act_layer: nn.Module = nn.GELU,
- ):
- super().__init__()
- # build perceiver with visual embed dim
- perceiver_cfg = dict(perceiver_cfg)
- perceiver_cfg["dim"] = visual_embed_dim
- self.perceiver = PerceiverResampler(**perceiver_cfg)
- # build projection mlp
- self.in_features = visual_embed_dim
- self.hidden = mlp_hidden_dim or llm_embed_dim
- self.out_features = llm_embed_dim
- self.mlp = Mlp(
- in_features=self.in_features,
- hidden_features=self.hidden,
- out_features=self.out_features,
- drop=mlp_drop,
- act_layer=mlp_act_layer,
- )
- self.num_queries = self.perceiver.num_queries
- self.output_is_list = True
- def forward(
- self,
- visual_tokens: List[torch.Tensor],
- serie_cu_seqlens: List[torch.Tensor]
- ) -> Tuple[List[torch.Tensor], List[List[int]]]:
- """
- Compress and project visual tokens.
- Args:
- visual_tokens (List[Tensor]): List of per-study visual embeddings
- Each tensor has shape (N_tokens_for_study, D_vis)
- serie_cu_seqlens (List[Tensor]): List of per-study cumulative sequence lengths
- Each tensor has shape (N_series + 1,)
- Returns:
- Tuple[List[Tensor], List[List[int]]]:
- - List of projected tokens per study (N_series * num_queries, D_llm)
- - List of token counts per series for each study
- """
- projected_tokens_per_study: List[torch.Tensor] = []
- serie_lengths_per_study: List[List[int]] = []
- for study_tokens, cu_seqlens in zip(visual_tokens, serie_cu_seqlens):
- # study_tokens: (N_total_tokens_for_study, D_vis)
- # cu_seqlens: (N_series + 1,) tensor with cumulative lengths
- if study_tokens.numel() == 0:
- # create placeholder for empty studies (corrupted data)
- num_series = len(cu_seqlens) - 1
- if num_series > 0:
- total_tokens = num_series * self.num_queries
- placeholder_tokens = torch.zeros(
- total_tokens, self.out_features,
- device=self.perceiver.queries.device,
- dtype=self.perceiver.queries.dtype
- )
- placeholder_lengths = [self.num_queries] * num_series
- projected_tokens_per_study.append(placeholder_tokens)
- serie_lengths_per_study.append(placeholder_lengths)
- else:
- projected_tokens_per_study.append(
- torch.empty(0, self.out_features,
- device=self.perceiver.queries.device,
- dtype=self.perceiver.queries.dtype)
- )
- serie_lengths_per_study.append([])
- continue
- compressed_per_serie = []
- num_series = len(cu_seqlens) - 1
- # split tokens into series based on cu_seqlens
- for i in range(num_series):
- start_idx = cu_seqlens[i]
- end_idx = cu_seqlens[i + 1]
- serie_tokens = study_tokens[start_idx:end_idx]
- if serie_tokens.numel() == 0:
- # placeholder for empty series
- placeholder = torch.zeros(
- self.num_queries,
- self.in_features,
- device=self.perceiver.queries.device,
- dtype=self.perceiver.queries.dtype,
- )
- compressed_per_serie.append(placeholder)
- else:
- # perceiver expects (B, N, D), so unsqueeze
- comp = self.perceiver(serie_tokens.unsqueeze(0)).squeeze(0)
- compressed_per_serie.append(comp)
- # concatenate compressed tokens from all series
- if not compressed_per_serie:
- projected_tokens_per_study.append(
- torch.empty(0, self.out_features,
- device=study_tokens.device,
- dtype=study_tokens.dtype)
- )
- serie_lengths_per_study.append([])
- continue
- all_serie_compressed = torch.cat(compressed_per_serie, dim=0)
- # project to llm dimension
- projected = self.mlp(all_serie_compressed)
- projected_tokens_per_study.append(projected)
- serie_lengths_per_study.append([self.num_queries] * num_series)
- return projected_tokens_per_study, serie_lengths_per_study
- class VisionLanguageModel(nn.Module):
- """
- Multimodal LLM for neuroimaging report generation.
- Combines a pretrained vision encoder, perceiver-based connector, and a language model in a LLaVA-style architecture for visual instruction tuning.
- Args:
- vision_encoder_cf (Dict): Configuration for VisionEncoder
- vision_connector_cf (Dict): Configuration for VisionConnector
- language_model_cf (Dict): Configuration for LanguageModel
- use_gradient_checkpointing (bool): Enable gradient checkpointing
- Example:
- >>> model = NeuroLlavaModel(
- ... vision_encoder_cf={...},
- ... vision_connector_cf={...},
- ... language_model_cf={...}
- ... )
- >>> outputs = model(vision_batch, input_ids, attention_mask, labels)
- """
- def __init__(
- self,
- vision_encoder_cf: Dict[str, Any],
- vision_connector_cf: Dict[str, Any],
- language_model_cf: Dict[str, Any],
- use_gradient_checkpointing: bool = False,
- ):
- super().__init__()
- # initialize vision encoder
- self.vision_encoder = VisionEncoder(
- backbone_cf=vision_encoder_cf,
- use_gradient_checkpointing=use_gradient_checkpointing,
- freeze=True,
- ).to(torch.bfloat16)
- self.visual_embed_dim = self.vision_encoder.embed_dim
- # initialize language model
- attn_impl = language_model_cf.get("attn_implementation", "flash_attention_2")
- self.language_model = LanguageModel(
- model_name_or_path=language_model_cf['model_name_or_path'],
- use_gradient_checkpointing=use_gradient_checkpointing,
- lora_params=language_model_cf.get('lora_params', None), # optional LoRA parameters; if None, finetunes entire LLM
- attn_implementation=attn_impl,
- ).to(torch.bfloat16)
- llm_dim = self.language_model.hidden_size
- # initialize vision connector
- cfg = vision_connector_cf.get('serie_perceiver', vision_connector_cf)
- self.vision_connector = VisionConnector(
- visual_embed_dim=self.visual_embed_dim,
- llm_embed_dim=llm_dim,
- perceiver_cfg=cfg.get('perceiver_cfg', cfg),
- mlp_hidden_dim=vision_connector_cf.get('mlp_hidden_dim'),
- mlp_drop=vision_connector_cf.get('mlp_drop', 0.0),
- ).to(torch.bfloat16)
- def forward(
- self,
- vision_batch: Optional[Dict[str, Any]],
- input_ids: Optional[torch.Tensor],
- attention_mask: Optional[torch.Tensor],
- labels: Optional[torch.Tensor] = None,
- **kwargs,
- ) -> Union[Dict[str, torch.Tensor], torch.Tensor]:
- """
- Forward pass for training or generation.
- Args:
- vision_batch (Dict): Batch from StudyPreprocessor
- input_ids (Tensor): Tokenized input ids (B, S)
- attention_mask (Tensor): Attention mask (B, S)
- labels (Tensor, optional): Labels for training (B, S)
- Returns:
- CausalLMOutputWithPast for training, or generated ids for inference
- """
- # 1. get projected visual embeddings
- vision_output = self._forward_vision(vision_batch)
- # 2. get text embeddings
- text_embeds = self.language_model.get_input_embeddings(input_ids)
- # 3. splice visual and text embeddings
- inputs_embeds, attention_mask, labels = self._splice_vision_and_text(
- vision_output=vision_output,
- text_embeds=text_embeds,
- input_ids=input_ids,
- attention_mask=attention_mask,
- labels=labels,
- )
- # 4. forward through language model
- return self._forward_sft(
- inputs_embeds=inputs_embeds,
- attention_mask=attention_mask,
- labels=labels,
- )
- def generate(
- self,
- vision_batch: Optional[Dict[str, Any]],
- input_ids: Optional[torch.Tensor],
- attention_mask: Optional[torch.Tensor],
- **generate_kwargs,
- ):
- """
- Autoregressive text generation.
- Args:
- vision_batch (Dict): Batch from StudyPreprocessor
- input_ids (Tensor): Tokenized prompt ids
- attention_mask (Tensor): Attention mask for prompt
- **generate_kwargs: HuggingFace generate arguments
- Returns:
- Generated token ids or dict with hidden states if requested
- """
- # 1. get projected visual embeddings
- vision_output = self._forward_vision(vision_batch)
- # 2. get text embeddings
- text_embeds = self.language_model.get_input_embeddings(input_ids)
- # 3. splice visual and text embeddings
- inputs_embeds, attention_mask, _ = self._splice_vision_and_text(
- vision_output=vision_output,
- text_embeds=text_embeds,
- input_ids=input_ids,
- attention_mask=attention_mask,
- labels=None,
- )
- # 4. generate
- return self._forward_generate(
- inputs_embeds=inputs_embeds,
- attention_mask=attention_mask,
- **generate_kwargs,
- )
- def _splice_vision_and_text(
- self,
- vision_output,
- text_embeds,
- input_ids,
- attention_mask,
- labels=None,
- ) -> Tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor]]:
- """
- Splice visual embeddings into text at placeholder locations.
- Args:
- vision_output: Output from _forward_vision
- text_embeds (Tensor): Text embeddings (B, S_text, D)
- input_ids (Tensor): Input ids (B, S_text)
- attention_mask (Tensor): Attention mask (B, S_text)
- labels (Tensor, optional): Labels (B, S_text)
- Returns:
- Tuple of (inputs_embeds, attention_mask, labels), all left-padded
- """
- final_embeds, final_attention_mask = [], []
- final_labels = [] if labels is not None else None
- # handle multiple placeholders for per-serie vision connectors
- projected_visual_tokens, projected_visual_serie_lengths = vision_output
- for i in range(text_embeds.shape[0]):
- study_visual_tokens = projected_visual_tokens[i]
- serie_lengths = projected_visual_serie_lengths[i]
- visual_token_chunks = list(torch.split(study_visual_tokens, serie_lengths, dim=0))
- placeholder_indices = (input_ids[i] == self.language_model.image_placeholder_token_id).nonzero(as_tuple=True)[0]
- if len(placeholder_indices) != len(visual_token_chunks):
- raise ValueError(
- f"Mismatch between number of image placeholders and image chunks for sample {i}.",
- f"Number of image placeholders: {len(placeholder_indices)}",
- f"Number of image chunks: {len(visual_token_chunks)}",
- )
- spliced_embeds_parts, spliced_mask_parts = [], []
- spliced_labels_parts = [] if labels is not None else None
- last_text_idx = 0
- for j, placeholder_idx in enumerate(placeholder_indices):
- spliced_embeds_parts.append(text_embeds[i, last_text_idx:placeholder_idx])
- spliced_mask_parts.append(attention_mask[i, last_text_idx:placeholder_idx])
- if labels is not None:
- spliced_labels_parts.append(labels[i, last_text_idx:placeholder_idx])
- visual_chunk = visual_token_chunks[j]
- spliced_embeds_parts.append(visual_chunk)
- spliced_mask_parts.append(torch.ones(visual_chunk.shape[0], dtype=torch.long, device=text_embeds.device))
- if labels is not None:
- spliced_labels_parts.append(torch.full((visual_chunk.shape[0],), -100, dtype=torch.long, device=text_embeds.device))
- last_text_idx = placeholder_idx + 1
- spliced_embeds_parts.append(text_embeds[i, last_text_idx:])
- spliced_mask_parts.append(attention_mask[i, last_text_idx:])
- if labels is not None:
- spliced_labels_parts.append(labels[i, last_text_idx:])
- final_embeds.append(torch.cat(spliced_embeds_parts, dim=0))
- final_attention_mask.append(torch.cat(spliced_mask_parts, dim=0))
- if final_labels is not None:
- final_labels.append(torch.cat(spliced_labels_parts, dim=0))
- # left pad all sequences to the same length
- batch_size = len(final_embeds)
- max_len = max(len(s) for s in final_embeds)
- embed_dim = final_embeds[0].shape[-1]
- device, dtype = final_embeds[0].device, final_embeds[0].dtype
- inputs_embeds = torch.zeros(batch_size, max_len, embed_dim, device=device, dtype=dtype)
- attention_mask_padded = torch.zeros(batch_size, max_len, dtype=torch.long, device=device)
- for i, seq in enumerate(final_embeds):
- seq_len = len(seq)
- inputs_embeds[i, -seq_len:] = seq
- attention_mask_padded[i, -seq_len:] = final_attention_mask[i]
- labels_padded = None
- if final_labels is not None:
- labels_padded = torch.full((batch_size, max_len), -100, dtype=torch.long, device=device)
- for i, seq in enumerate(final_labels):
- labels_padded[i, -len(seq):] = seq
- return inputs_embeds, attention_mask_padded, labels_padded
- def _forward_sft(
- self,
- inputs_embeds,
- attention_mask,
- labels,
- ):
- """Forward pass for supervised fine-tuning."""
- if labels is None:
- raise ValueError("For training, 'labels' must be provided.")
- return self.language_model(
- inputs_embeds=inputs_embeds,
- attention_mask=attention_mask,
- labels=labels
- )
- def _forward_generate(
- self,
- inputs_embeds,
- attention_mask,
- **generate_kwargs
- ):
- """Forward pass for generation."""
- return self.language_model.generate(
- inputs_embeds=inputs_embeds,
- attention_mask=attention_mask,
- **generate_kwargs
- )
- def _forward_vision(
- self,
- vision_batch: Optional[Dict[str, Any]],
- ) -> Union[torch.Tensor, List[torch.Tensor], Tuple[List[torch.Tensor], List[List[int]]]]:
- """
- Forward pass through vision encoder and connector.
- Args:
- vision_batch (Dict): Batch from StudyPreprocessor
- Returns:
- Projected visual tokens (format depends on connector strategy)
- """
- visual_tokens, visual_coords = self.vision_encoder(vision_batch)
- # need to create a series_cu_seqlens_list, where each tensor in the list is the cumulative lengths of the series for a given study
- # this is used by the vision connector to apply perceiver in a series-wise manner
- # example:
- # study_cu_seqlens = [0, 1000, 2000, 3000]
- # series_cu_seqlens = [0, 250, 1000, 1250, 1500, 2000, 2500, 3000]
- # series_cu_seqlens_list = [[0, 250, 1000], [0, 250, 500, 1000], [0, 500, 1000]]
- study_cu_seqlens = vision_batch['study_cu_seqlens']
- series_cu_seqlens_flat = vision_batch['series_cu_seqlens']
- batch_size = len(study_cu_seqlens) - 1
- series_cu_seqlens_list = []
- start_idx_in_series_flat = 0
- for i in range(batch_size):
- study_start_offset = study_cu_seqlens[i]
- study_end_offset = study_cu_seqlens[i+1]
- # find the index of the study's end offset in the flat series tensor
- end_idx_tensor = (series_cu_seqlens_flat == study_end_offset).nonzero(as_tuple=True)[0]
- if end_idx_tensor.numel() == 0:
- raise ValueError(f"Study boundary {study_end_offset} not found in series_cu_seqlens.")
- if len(end_idx_tensor) > 1:
- raise ValueError(f"Multiple indices found for study boundary {study_end_offset} in series_cu_seqlens: {end_idx_tensor}\nstudy_cu_seqlens: {study_cu_seqlens}\nseries_cu_seqlens_flat: {series_cu_seqlens_flat}")
- end_idx_in_series_flat = end_idx_tensor.item()
- # slice the cumulative lengths for the current study
- study_series_cu_seqlens = series_cu_seqlens_flat[start_idx_in_series_flat : end_idx_in_series_flat + 1]
- # make the lengths relative to the start of the study
- study_series_cu_seqlens_relative = study_series_cu_seqlens - study_start_offset
- series_cu_seqlens_list.append(study_series_cu_seqlens_relative)
- # the start for the next study is the end of the current one
- start_idx_in_series_flat = end_idx_in_series_flat
- return self.vision_connector(visual_tokens, series_cu_seqlens_list)
- def _prepare_inputs_for_generation(
- self,
- input_ids,
- attention_mask,
- labels,
- ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, List[str]]:
- """
- Extract prompt from tokenized conversation for generation.
- Args:
- input_ids (Tensor): Full tokenized conversation (B, S)
- attention_mask (Tensor): Attention mask (B, S)
- labels (Tensor): Labels with -100 for prompt tokens (B, S)
- Returns:
- Tuple of (input_ids, attention_mask, labels, decoded_texts)
- """
- # extract prompt parts from each sample in the batch
- # since data is left-padded, we can directly slice from start to first non -100 label
- gen_input_ids_list = []
- max_prompt_len = 0
- device = input_ids.device
- for i in range(len(input_ids)):
- # find the start of the response (first non -100 label)
- labels_i = labels[i]
- first_target_idx_tensor = (labels_i != -100).nonzero(as_tuple=True)[0]
- if len(first_target_idx_tensor) > 0:
- first_target_idx = first_target_idx_tensor[0]
- # the prompt is everything up to the start of the response
- prompt_ids = input_ids[i][:first_target_idx]
- else:
- # if no target, the whole sequence is the prompt (can happen with truncation)
- prompt_ids = input_ids[i]
- gen_input_ids_list.append(prompt_ids)
- if len(prompt_ids) > max_prompt_len:
- max_prompt_len = len(prompt_ids)
- # left pad all input_ids to max_prompt_len
- padded_gen_input_ids_list = []
- for prompt_ids in gen_input_ids_list:
- pad_len = max_prompt_len - len(prompt_ids)
- padded = torch.cat([
- torch.full((pad_len,), self.language_model.tokenizer.pad_token_id, dtype=torch.long, device=device),
- prompt_ids
- ])
- padded_gen_input_ids_list.append(padded)
- # decode back to raw text (for debugging)
- generation_texts = [self.language_model.tokenizer.decode(padded, skip_special_tokens=True).strip() for padded in padded_gen_input_ids_list]
- # return the original batch with the updated values
- input_ids_new = torch.stack(padded_gen_input_ids_list)
- attention_mask_new = torch.ones_like(input_ids_new).to(device)
- labels_new = None
- return input_ids_new, attention_mask_new, labels_new, generation_texts
vlm.py at commit eb41760, under MIT · at the source
Overview
- Machine Learning in Neurosurgery Lab, University of Michigan, Ann Arbor, MI USA
- University of Michigan Computational Medicine and Bioinformatics, Ann Arbor, MI USA
- University of Michigan Computer Science and Engineering, Ann Arbor, MI USA
- University of Michigan Neurosurgery, Ann Arbor, MI USA
- University of Cologne Neurosurgery, Cologne, Germany
- University of Michigan Radiology, Ann Arbor, MI USA
Abstract
Frontier artificial intelligence (AI) models have advanced rapidly through training on internet-scale public data, yet such systems lack access to private clinical data. Neuroimaging is underrepresented in the public domain due to identifiable facial features within magnetic resonance imaging (MRI) and computed tomography (CT) scans, restricting model performance in clinical medicine. Here we show that frontier models underperform on neuroimaging tasks and that learning directly from uncurated data generated during routine clinical care at health systems, a paradigm we call ‘health system learning’, yields high-performance, generalist neuroimaging models. We introduce NeuroVFM, a visual foundation model trained on 5.24 million clinical MRI and CT volumes using a scalable volumetric predictive architecture. NeuroVFM learns comprehensive representations of brain anatomy and pathology, achieving state-of-the-art performance across multiple clinical tasks, including radiologic diagnosis and report generation. The model embeds MRI and CT scans into a shared neuroanatomic latent space and grounds diagnostic findings. When paired with open-source language models, NeuroVFM generates radiology reports that surpass frontier models in accuracy, clinical triage and expert preference. NeuroVFM reduces hallucinated findings and critical errors, offering safer clinical decision support. These results establish health system learning as a paradigm for building generalist medical AI and provide a scalable framework for clinical foundation models.
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 15 matches between paragraphs and lines of code.
MLNeurosurg/neurovfm
eb41760f200e3a542fd7bce627513d9a625f0997, 10 September 2026Availability: 1 check, the latest on 27 September 2026: the link answers
- 27 September 2026: the link answers
57 files
- examples/
cache_dataset.py , Python, 5 lines - examples/
create_metadata.py , Python, 12 lines - examples/
inference_dx_ct.py , Python, 19 lines - examples/
inference_dx_mri.py , Python, 19 lines - examples/
inference_encoder.py , Python, 17 lines - examples/
inference_generator.py , Python, 46 lines - examples/
pretrain_encoder.py , Python, 16 lines - neurovfm/
__init__.py , Python, 89 lines - neurovfm/
data/ , Python, 15 lines__init__.py - neurovfm/
data/ , Python, 400 linescache.py - neurovfm/
data/ , Python, 153 linesio.py - neurovfm/
data/ , Python, 238 linesmetadata.py - neurovfm/
data/ , Python, 388 lines, 2 matchespreprocess.py - neurovfm/
data/ , Python, 79 linestext.py - neurovfm/
data/ , Python, 238 linesutils.py - neurovfm/
datasets/ , Python, 23 lines__init__.py - neurovfm/
datasets/ , Python, 701 lines, 1 matchcollators.py - neurovfm/
datasets/ , Python, 414 linesdatamodule.py - neurovfm/
datasets/ , Python, 718 linesdataset.py - neurovfm/
models/ , Python, 63 lines__init__.py - neurovfm/
models/ , Python, 498 lines, 2 matchesmil.py - neurovfm/
models/ , Python, 100 linespatch_embed.py - neurovfm/
models/ , Python, 154 linesperceiver.py - neurovfm/
models/ , Python, 107 linespos_embed.py - neurovfm/
models/ , Python, 163 linesprojector.py - neurovfm/
models/ , Python, 1,409 linesvit.py - neurovfm/
models/ , Python, 857 lines, 3 matchesvlm.py - neurovfm/
optim/ , Python, 16 lines__init__.py - neurovfm/
optim/ , Python, 75 linescosine_schedule_warmup.p y - neurovfm/
optim/ , Python, 183 linesutils.py - neurovfm/
pipelines/ , Python, 23 lines__init__.py - neurovfm/
pipelines/ , Python, 223 lines, 1 matchdiagnostic.py - neurovfm/
pipelines/ , Python, 199 lines, 1 matchencoder.py - neurovfm/
pipelines/ , Python, 269 linesgenerator.py - neurovfm/
pipelines/ , Python, 102 lines, 2 matchesinterpreter.py - neurovfm/
pipelines/ , Python, 224 linespreprocessor.py - neurovfm/
systems/ , Python, 19 lines__init__.py - neurovfm/
systems/ , Python, 570 lines, 1 matchclassification.py - neurovfm/
systems/ , Python, 195 lines, 1 matchllm_sft.py - neurovfm/
systems/ , Python, 490 lines, 1 matchpretraining.py - neurovfm/
systems/ , Python, 133 linesutils.py - neurovfm/
train/ , Python, 2 lines__init__.py - neurovfm/
train/ , Python, 342 linestrain.py - neurovfm/
train/ , Python, 386 linestrain_llm.py - tests/
conftest.py , Python, 126 lines - tests/
test_classification_step , Python, 158 liness.py - tests/
test_classification_syst , Python, 184 linesem.py - tests/
test_data.py , Python, 74 lines - tests/
test_dataset.py , Python, 133 lines - tests/
test_encoder.py , Python, 224 lines - tests/
test_mil.py , Python, 166 lines - tests/
test_pipelines_encoder.p , Python, 125 linesy - tests/
test_pos_and_projector.p , Python, 61 linesy - tests/
test_preprocessor.py , Python, 114 lines - tests/
test_vit_config.py , Python, 60 lines - LICENSE, License, 22 lines
- README.md, Text, 111 lines
Code availability
All code was implemented in Python (version 3.10.14) using PyTorch (2.5.0) compiled with CUDA 12.4 as the primary machine learning framework. The following packages were used for data preprocessing, model training and evaluation: pydicom (2.4.4), nibabel (5.3.2), SimpleITK (2.4.0), torchvision (0.20.0), pandas (2.2.3), NumPy (2.1.2), PyTorch Lightning (2.5.0.post0), flash-attn (2.6.3), matplotlib (3.10.7), scipy (1.15.2) and scikit-learn (1.6.1). The following packages were used to load baselines: open-clip (2.23.0) and transformers (4.56.0). All code and scripts to reproduce the experiments in this study are available on GitHub at https://
Reproduced under the paper's license (CC BY), from the paper cited above.
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;
- 55 scripts, each with its path and the digest of its content;
- 15 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.
Data availability
IRB approval was obtained from the University of Michigan for MRI data collection. Restrictions apply to the availability of raw patient MRI and CT imaging data, which were used with institutional permission through IRB approval for the present study and, thus, are not publicly available. All data sharing among medical centers is regulated through data use agreements with the study authors. A similar data-sharing protocol may be established for interested investigators. Please contact the corresponding author (T.H.) for any requests for data sharing. All requests will be evaluated based on institutional and departmental policies to determine whether the data requested are subject to intellectual property or patient privacy obligations. Data can be shared only for non-commercial academic and investigational purposes.
Reproduced under the paper's license (CC BY), from the paper cited above.
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 2, 28 September 2026
- Publisher: n/a → Nature Portfolio
Version 1, 27 September 2026: the first record
Recorded: type, language, journal, volume, issue, pages, dates, 18 authors, 4 keywords, 7 MeSH terms, 4 funders, 32 references.
Cite
This paper
Kondepudi, A., Rao, A., Zhao, C., Lyu, Y., Harake, S., Banerjee, S., Ogle, J., Joshi, R., Meissner, A.-K., Hou, X., Jiang, C., Chowdury, A., Srinivasan, A., Athey, B., Gulani, V., Pandey, A., Lee, H., & Hollon, T. (2026). Health system learning enables generalist neuroimaging models. Nature medicine, 32(8), 2831-2837. https://
BibTeX
@article{kondepudi2026he
author = {Kondepudi, Akhil and Rao, Akshay and Zhao, Chenhui and Lyu, Yiwei and Harake, Samir and Banerjee, Soumyanil and Ogle, Jacob and Joshi, Rushikesh and Meissner, Anna-Katharina and Hou, Xinhai and Jiang, Cheng and Chowdury, Asadur and Srinivasan, Ashok and Athey, Brian and Gulani, Vikas and Pandey, Aditya and Lee, Honglak and Hollon, Todd},
title = {{Health system learning enables generalist neuroimaging models}},
journal = {Nature medicine},
year = {2026},
month = jul,
volume = {32},
number = {8},
pages = {2831--2837},
publisher = {Nature Portfolio},
issn = {1078-8956},
doi = {10.1038/
url = {https://
pmid = {42432292},
pmcid = {PMC13472962}
}
RIS
TY - JOUR
AU - Kondepudi, Akhil
AU - Rao, Akshay
AU - Zhao, Chenhui
AU - Lyu, Yiwei
AU - Harake, Samir
AU - Banerjee, Soumyanil
AU - Ogle, Jacob
AU - Joshi, Rushikesh
AU - Meissner, Anna-Katharina
AU - Hou, Xinhai
AU - Jiang, Cheng
AU - Chowdury, Asadur
AU - Srinivasan, Ashok
AU - Athey, Brian
AU - Gulani, Vikas
AU - Pandey, Aditya
AU - Lee, Honglak
AU - Hollon, Todd
TI - Health system learning enables generalist neuroimaging models
T2 - Nature medicine
J2 - Nat Med
PY - 2026
DA - 2026/
VL - 32
IS - 8
SP - 2831
EP - 2837
SN - 1078-8956
PB - Nature Portfolio
DO - 10.1038/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1038/
"type": "article-journal",
"title": "Health system learning enables generalist neuroimaging models",
"container-title": "Nature medicine",
"author": [
{
"family": "Kondepudi",
"given": "Akhil"
},
{
"family": "Rao",
"given": "Akshay"
},
{
"family": "Zhao",
"given": "Chenhui"
},
{
"family": "Lyu",
"given": "Yiwei"
},
{
"family": "Harake",
"given": "Samir"
},
{
"family": "Banerjee",
"given": "Soumyanil"
},
{
"family": "Ogle",
"given": "Jacob"
},
{
"family": "Joshi",
"given": "Rushikesh"
},
{
"family": "Meissner",
"given": "Anna-Katharina"
},
{
"family": "Hou",
"given": "Xinhai"
},
{
"family": "Jiang",
"given": "Cheng"
},
{
"family": "Chowdury",
"given": "Asadur"
},
{
"family": "Srinivasan",
"given": "Ashok"
},
{
"family": "Athey",
"given": "Brian"
},
{
"family": "Gulani",
"given": "Vikas"
},
{
"family": "Pandey",
"given": "Aditya"
},
{
"family": "Lee",
"given": "Honglak"
},
{
"family": "Hollon",
"given": "Todd"
}
],
"container-title-short":
"volume": "32",
"issue": "8",
"page": "2831-2837",
"DOI": "10.1038/
"PMID": "42432292",
"PMCID": "PMC13472962",
"ISSN": "1078-8956",
"publisher": "Nature Portfolio",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
10
]
]
}
}
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.1162/imag.a.1326 [code]
- RAVEN: Robust, generalizable, multi-resolution structural MRI upsampling using autoencoders.Journal: Imaging neuroscience (Cambridge, Mass.)In common: PyTorch Lightning, Hugging Face Transformers, SimpleITK, 3 other tools, structural MRI / diffusion, 1 reference
- [2] doi:10.1093/radadv/umag025 [code]
- OpenMAP-BrainAge: generalizable and interpretable brain age predictor from MRI.Journal: Radiology advancesIn common: Hugging Face Transformers, SimpleITK, PyTorch, 2 other tools, structural MRI / diffusion, 1 reference
- [3] doi:10.1016/j.patter.2026.101538 [code]
- A multi-modal foundation model for brain disease diagnosis and medical imaging.Journal: Patterns (New York, N.Y.)In common: Hugging Face Transformers, PyTorch, pandas, 1 other tool, clinical / translational, 2 references
- [4] doi:10.1038/s44387-026-00109-y [code]
- SPARROW: subtyping Parkinson's disease with agentic reasoning and robust omics workflow.Journal: NPJ artificial intelligenceIn common: Hugging Face Transformers, PyTorch, pandas, 1 other tool, clinical / translational, 2 references
- [5] doi:10.1002/hbm.70469 [code]
- VarCoNet: A Variability-Aware Self-Supervised Framework for Functional Connectome Extraction From Resting-State fMRI.Journal: Human brain mappingIn common: PyTorch Geometric, Hugging Face Transformers, PyTorch, 2 other tools, 1 reference
- [6] doi:10.1038/s43856-026-01722-3 [code]
- Local and global patterns support medical imaging as a biomarker of ageing.Journal: Communications medicineIn common: PyTorch Lightning, SimpleITK, PyTorch, 2 other tools, clinical / translational
- [7] doi:10.1016/j.isci.2026.117446 [code]
- Volume-inflation registration (INFREG) for morphometric analysis of human focal cortical dysplasia type II.Journal: iScienceIn common: PyTorch Geometric, SimpleITK, PyTorch, 2 other tools, structural MRI / diffusion
- [8] doi:10.1186/s41747-026-00735-w [code]
- Dual-conditioned diffusion model with anatomical guidance for geometric distortion correction in prostate MRI.Journal: European radiology experimentalIn common: Hugging Face Transformers, SimpleITK, PyTorch, 2 other tools, structural MRI / diffusion
- [9] doi:10.1038/s41467-026-71555-0 [code]
- A deep representation learning model to predict response to vagus nerve stimulation.Journal: Nature communicationsIn common: PyTorch Lightning, PyTorch, pandas, 1 other tool, structural MRI / diffusion, clinical / translational, 1 reference
- [10] doi:10.1038/s41598-026-53415-5 [code]
- Computational design and immunoinformatics validation of a T cell multi-epitope vaccine targeting glioblastoma stem cells.Journal: Scientific reportsIn common: PyTorch Lightning, PyTorch Geometric, PyTorch, 2 other tools
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.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 55 scripts, and 15 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:867304831e4e8125…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
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.
