OSCR

Mass spectrometry imaging-based explainable machine learning reveals the biochemical landscapes of the mouse brain.

Code ↔ Paper

4 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 4 matches
  1. [1] § Materials and methods › Modeling › Model-based selection of m/z markers for each brain region ↔ notebooks/linear.ipynb, lines 28–124 · score 0.78 · noise feature, permuted features, linear model, thresholds, selection, subsampling
  2. [2] § Materials and methods › Modeling › Model-based selection of m/z markers for each brain region ↔ msi_atlas/mapping.py, lines 19–83 · score 0.69 · permuted features, linear model, thresholds, selection, subsampling, noise
  3. [3] § Materials and methods › Visualizations › Virtual Pathology Stain (VPS) ↔ notebooks/linear.ipynb, lines 376–450 · score 0.68 · grayscale, 0–1, RGB, stain, pixels, IHC
  4. [4] § Materials and methods › Modeling › Machine learning-based classification of pixel categories ↔ msi_atlas/atlas_linear_model.py, lines 17–147 · score 0.52 · sampled weights, Pyro, uncertainty, linear, predictive, model

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 · 494 lines · 17 KB · MIT · 2 matches

  1. # %%
  2. from msi_visual.normalization import total_ion_count, spatial_total_ion_count
  3. from msi_atlas.annotations import get_dataset
  4. import numpy as np
  5. from argparse import Namespace
  6. from pathlib import Path
  7. import joblib
  8. array = np.array
  9. import os
  10. path = r"/home/jacob/Desktop/atlas_verification"
  11. extraction_args = eval(
  12. open(
  13. Path(path) /
  14. "args.txt").read())
  15. extraction_mzs = extraction_args.mzs
  16. paths = [(Path(path) / "0.npy", 1),
  17. (Path(path) / "2.npy", 0),
  18. (Path(path) / "1.npy", 2),
  19. (Path(path) / "3.npy", 3)]
  20. paths = [(str(path[0]), path[1]) for path in paths]
  21. X, y, slide_labels, label_encoder = get_dataset(r"NRL4485-s2_reannotation_23-12-24_PAHJ.json",
  22. paths, subsample=1, normalization=total_ion_count)
  23. # %%
  24. from msi_atlas.atlas_linear_model import AtlasLinearModel
  25. import torch
  26. def permute_features(X, noise_dim, random_state=None):
  27. """
  28. Creates noise features by randomly selecting columns from X and permuting their values.
  29. """
  30. rng = np.random.default_rng(random_state)
  31. n_samples, n_features = X.shape
  32. noise_indices = rng.choice(n_features, size=noise_dim, replace=True)
  33. noise = np.zeros((n_samples, noise_dim))
  34. for i, col_idx in enumerate(noise_indices):
  35. noise[:, i] = rng.permutation(X[:, col_idx])
  36. return noise
  37. def feature_stability_with_noise(X, y,
  38. num_runs=30,
  39. reg_strengths=[1e-2, 1e-3],
  40. noise_dim=20,
  41. subsample_ratio=0.8,
  42. weight_threshold=1e-4,
  43. random_state=None):
  44. """
  45. Estimates feature stability using subsampling and permuted real features for FDR control.
  46. Returns:
  47. - selected_features: boolean mask for real features
  48. - stability: array of shape (n_features + noise_dim,) with selection frequencies
  49. - noise_threshold: max stability among permuted (noise) features
  50. """
  51. rng = np.random.default_rng(random_state)
  52. n_samples, n_features = X.shape
  53. total_features = n_features + noise_dim
  54. stability = np.zeros((y.max()+1, total_features))
  55. all_weights = []
  56. balanced_accs = []
  57. accs = []
  58. for _ in range(num_runs):
  59. idx = rng.choice(n_samples, int(subsample_ratio * n_samples), replace=False)
  60. X_sub, y_sub = X[idx], y[idx]
  61. X_test, y_test = X[~idx], y[~idx]
  62. noise = permute_features(X_sub, noise_dim, random_state=rng)
  63. X_aug = np.hstack([X_sub, noise])
  64. X_test_aug = np.hstack([X_test, noise])
  65. for reg in reg_strengths:
  66. model = AtlasLinearModel(lr=0.1, C=y.max()+1)
  67. model.fit(X_aug, y_sub, early_stop_acc=0.8)
  68. # Get balanced test accuracy
  69. y_pred = model.predict(torch.tensor(X_test_aug, dtype=torch.float32).cuda())
  70. # Get overall test accuracy
  71. acc = (y_pred == y_test).mean()
  72. class_accuracies = []
  73. for c in np.unique(y_test):
  74. mask = y_test == c
  75. if mask.sum() > 0: # Only calculate if class exists in test set
  76. class_acc = (y_pred[mask] == y_test[mask]).mean()
  77. class_accuracies.append(class_acc)
  78. balanced_acc = np.mean(class_accuracies)
  79. balanced_accs.append(balanced_acc)
  80. accs.append(acc)
  81. weights = model.coef_ # shape: (total_features,)
  82. weights = weights.detach().cpu().numpy()
  83. all_weights.append(weights)
  84. max_noise_weight = np.max(weights[:, -noise_dim:], axis=-1)
  85. weight_threshold = max_noise_weight * 1.1
  86. selected = (weights > weight_threshold[:, None]).astype(float)
  87. stability += selected
  88. stability /= (num_runs * len(reg_strengths))
  89. print("Accuracy: ", np.mean(accs))
  90. print("Balanced Accuracy: ", np.mean(balanced_accs))
  91. return stability, all_weights
  92. y = np.array(y)
  93. stability, all_weights_specific = feature_stability_with_noise(X, y, 200, subsample_ratio=0.5, reg_strengths=[0], noise_dim=50)
  94. import joblib
  95. # # Save stability and weights
  96. # joblib.dump(stability, 'stability.joblib')
  97. # joblib.dump(all_weights_specific, 'all_weights_specific.joblib')
  98. # %%
  99. from collections import defaultdict
  100. shared_counter = defaultdict(int)
  101. categories = defaultdict(list)
  102. mzs_per_category = defaultdict(list)
  103. weight = {}
  104. for category_index in range(stability.shape[0]):
  105. for i in range(stability.shape[1]):
  106. if stability[category_index, i] > 0.95:
  107. shared_counter[extraction_mzs[i]] += 1
  108. categories[extraction_mzs[i]].append(category_index)
  109. weight[extraction_mzs[i]] = float(np.array(all_weights_specific)[:, category_index, i].mean())
  110. mzs_per_category[category_index].append((extraction_mzs[i], weight[extraction_mzs[i]]))
  111. category_index = 102
  112. shared_mzs = []
  113. specific_mzs = []
  114. specific_indices = []
  115. shared_indices = []
  116. for i in range(stability.shape[1] -50):
  117. weight[extraction_mzs[i]] = float(np.array(all_weights_specific)[:, category_index, i].mean())
  118. if stability[category_index, i] > 0.95:
  119. specific_indices.append(i)
  120. specific_mzs.append(extraction_mzs[i])
  121. weight[extraction_mzs[i]] = float(np.array(all_weights_specific)[:, category_index, i].mean())
  122. elif stability[category_index, i] > 0.5:
  123. shared_indices.append(i)
  124. shared_mzs.append(extraction_mzs[i])
  125. print(specific_mzs)
  126. mzs_per_category_with_model = {}
  127. for category in mzs_per_category:
  128. mzs_per_category_with_model[category] = [x[0] for x in sorted(mzs_per_category[category], key=lambda x: x[1], reverse=True)]
  129. # %%
  130. import pandas as pd
  131. from collections import defaultdict
  132. model_mzs_and_stabilities = {"category": []}
  133. for i in range(200):
  134. model_mzs_and_stabilities[f"mz_{i}"] = []
  135. model_mzs_and_stabilities[f"stability_{i}"] = []
  136. frequencies = stability[:, :-50]
  137. mzs_per_category_with_model_80 = defaultdict(list)
  138. mzs_per_category_with_model_100 = defaultdict(list)
  139. for category_index in range(frequencies.shape[0]):
  140. sorted_indices = np.argsort(frequencies[category_index])[::-1]
  141. mzs = [extraction_mzs[i] for i in sorted_indices]
  142. category_stabilities = frequencies[category_index, sorted_indices]
  143. model_mzs_and_stabilities["category"].append(label_encoder.classes_[category_index])
  144. for i, (mz, f) in enumerate(zip(mzs[:200], category_stabilities[:200])):
  145. model_mzs_and_stabilities[f"mz_{i}"].append(mz)
  146. model_mzs_and_stabilities[f"stability_{i}"].append(f)
  147. if f > 0.8:
  148. mzs_per_category_with_model_80[label_encoder.classes_[category_index]].append(mz)
  149. if f == 1.0:
  150. mzs_per_category_with_model_100[label_encoder.classes_[category_index]].append(mz)
  151. model_mzs_and_stabilities = pd.DataFrame(model_mzs_and_stabilities)
  152. model_mzs_and_stabilities.to_csv("model_mzs_and_stabilities.csv", index=False)
  153. # %%
  154. print([str(a) for a in list(label_encoder.inverse_transform(categories[1179.73081311]))])
  155. print([str(a) for a in list(label_encoder.inverse_transform(categories[1207.75938587]))])
  156. print([str(a) for a in list(label_encoder.inverse_transform(categories[808.12340284]))])
  157. label_encoder.transform(["BG_ARTEFACT_ARTEFACT"])
  158. sorted_counter = sorted(shared_counter.items(), key=lambda x: x[1], reverse=True)
  159. print(sorted_counter)
  160. # %%
  161. mzs = joblib.load("/home/jacob/Desktop/auc_mzs.joblib")
  162. auc_mzs_per_category = {}
  163. for category, category_name in enumerate(label_encoder.classes_):
  164. auc_mzs_per_category[category] = mzs["unique"][category_name] + mzs["common"][category_name]
  165. auc_mzs_per_category[category] = [x[0] for x in sorted(auc_mzs_per_category[category], key=lambda x: x[1], reverse=True)]
  166. #auc_mzs_per_category[102]
  167. # %%
  168. import pandas as pd
  169. # Create empty DataFrame with 50 columns
  170. df = pd.DataFrame(index=label_encoder.classes_, columns=range(250))
  171. # Fill DataFrame with m/z values for each category
  172. for category in auc_mzs_per_category:
  173. category_name = label_encoder.classes_[category]
  174. mzs = [x for x in auc_mzs_per_category[category][:250]] # Get top 50 m/z values
  175. # Pad with NaN if less than 50 m/z values
  176. mzs.extend([float('nan')] * (250 - len(mzs)))
  177. df.loc[category_name] = mzs
  178. # Save to CSV
  179. df.to_csv('MZ_AUC.csv')
  180. # Create empty DataFrame with 50 columns
  181. df = pd.DataFrame(index=label_encoder.classes_, columns=range(250))
  182. # Fill DataFrame with m/z values for each category
  183. for category in mzs_per_category:
  184. category_name = label_encoder.classes_[category]
  185. mzs = mzs_per_category_with_model[category][:250] # Get top 50 m/z values
  186. # Pad with NaN if less than 50 m/z values
  187. mzs.extend([float('nan')] * (250 - len(mzs)))
  188. df.loc[category_name] = mzs
  189. # Save to CSV
  190. df.to_csv('MZ_MODEL_TIC.csv')
  191. # %%
  192. from venn import venn
  193. from matplotlib import pyplot as plt
  194. %matplotlib inline
  195. parent_categories = list(set([name.split("_")[0].replace("#", '') for name in label_encoder.classes_]))
  196. mzs_per_parent = defaultdict(list)
  197. for category in mzs_per_category_with_model_80:
  198. parent = category.split("_")[0].replace("#", '')
  199. mzs = mzs_per_category_with_model_80[category]
  200. mzs = [mz for mz in mzs if mz not in auc_mzs_per_category[0]]
  201. mzs_per_parent[parent].extend(mzs)
  202. # Convert lists to sets for each parent category
  203. mz_sets = {parent: set(mzs) for parent, mzs in mzs_per_parent.items() if parent in ['BST', 'CNU', 'CTX', 'WM']}
  204. venn(mz_sets)
  205. plt.rcParams.update({'font.size': 10}) # Increase font size for all text elements
  206. plt.title("Venn diagram for m/z values identified by the model", y=-0.1)
  207. # %%
  208. from venn import venn
  209. from matplotlib import pyplot as plt
  210. %matplotlib inline
  211. parent_categories = list(set([name.split("_")[0].replace("#", '') for name in label_encoder.classes_]))
  212. mzs_per_parent = defaultdict(list)
  213. for category in auc_mzs_per_category:
  214. parent = label_encoder.classes_[category].split("_")[0].replace("#", '')
  215. mzs = auc_mzs_per_category[category]
  216. mzs = [mz for mz in mzs if mz not in auc_mzs_per_category[0]]
  217. mzs_per_parent[parent].extend(mzs)
  218. # Convert lists to sets for each parent category
  219. mz_sets = {parent: set(mzs) for parent, mzs in mzs_per_parent.items() if parent in ['BST', 'CNU', 'CTX', 'WM']}
  220. venn(mz_sets)
  221. plt.title("Venn diagram for m/z values identified by mPAUC")
  222. # %%
  223. # Count occurrences of each m/z value across all parent categories
  224. mz_counts = defaultdict(list)
  225. for parent, mzs in mzs_per_parent.items():
  226. if parent == 'BG':
  227. continue
  228. for mz in set(mzs):
  229. mz_counts[mz].append(parent)
  230. # Sort by number of occurrences (most frequent first)
  231. sorted_mzs = sorted(mz_counts.items(), key=lambda x: len(x[1]), reverse=True)
  232. print("Most common m/z values and their parent categories:")
  233. for mz, parents in sorted_mzs[:150]: # Show top 20
  234. print(f"m/z {mz:.5f} {', '.join(parents)}")
  235. # %%
  236. joblib.dump({"stability": stability, "all_weights_specific": all_weights_specific, "label_encoder": label_encoder}, "spatial_tic_model.joblib")
  237. # %%
  238. # Count category frequencies for m/z values in category 102
  239. category_counts = defaultdict(int)
  240. for mz in auc_mzs_per_category[102]:
  241. found = False
  242. # Find all categories this m/z appears in
  243. for cat_idx, mzs in auc_mzs_per_category.items():
  244. if mz in mzs and cat_idx != 102: # Exclude self-comparison
  245. cat_name = label_encoder.classes_[cat_idx].replace("_ARTEFACT", "")
  246. category_counts[cat_name] += 1
  247. found = True
  248. if not found:
  249. category_counts["unique"] += 1
  250. # Get top 10 categories by frequency
  251. top_10_categories = dict(sorted(category_counts.items(), key=lambda x: x[1], reverse=True)[:10])
  252. # Create pie chart
  253. plt.figure(figsize=(20, 20), dpi=200)
  254. plt.rcParams.update({'font.size': 30}) # Increase font size for all text elements
  255. plt.pie(top_10_categories.values(), labels=top_10_categories.keys(), autopct='%5.1f%%')
  256. plt.title(f"Frequencies of m/z's shared with other categories for {label_encoder.classes_[102].replace('_ARTEFACT', '')}", y=-0.1)
  257. plt.axis('equal')
  258. plt.show()
  259. # %%
  260. # Count category frequencies for m/z values in category 102
  261. category_counts = defaultdict(int)
  262. for mz in mzs_per_category_with_model_80[label_encoder.classes_[102]]:
  263. found = False
  264. # Find all categories this m/z appears in
  265. for cat_name, mzs in mzs_per_category_with_model_80.items():
  266. if mz in mzs and cat_name != label_encoder.classes_[102]: # Exclude self-comparison
  267. cat_name = cat_name.replace("_ARTEFACT", "")
  268. category_counts[cat_name] += 1
  269. found = True
  270. if not found:
  271. category_counts["unique"] += 1
  272. # Get top 10 categories by frequency
  273. top_10_categories = dict(sorted(category_counts.items(), key=lambda x: x[1], reverse=True)[:10])
  274. # Create pie chart
  275. plt.figure(figsize=(10, 10))
  276. plt.pie(top_10_categories.values(), labels=top_10_categories.keys(), autopct='%1.1f%%', textprops={'fontsize': 14})
  277. plt.title(f"Frequencies of m/z's shared with other categories for {label_encoder.classes_[102].replace('_ARTEFACT', '')} (Top 10)\nTIC model mapping")
  278. plt.axis('equal')
  279. plt.show()
  280. # %%
  281. import cv2
  282. from PIL import Image
  283. def get_ihc_image(img, mzs, category, intensity=None):
  284. indices = [extraction_mzs.index(float(mz)) for mz in mzs]
  285. mask = img.max(axis=-1)
  286. img = img / np.max(img, axis=(0, 1))
  287. ion = img[:, :, indices].mean(axis=-1)
  288. if intensity is None:
  289. ion = ion / ion.max()
  290. else:
  291. ion = ion / intensity
  292. ion = ion / np.percentile(ion, 99.7)
  293. ion[ion > 1] = 1
  294. # Apply non-linear stretching to emphasize larger values
  295. # Using power function with exponent < 1 to compress lower values and stretch higher ones
  296. ion = np.power(ion, 2) # Adjust exponent as needed (smaller = more stretching)
  297. ion = np.uint8((ion / ion.max()) * 255) # Rescale back to 0-255 range
  298. #ion = np.uint8(ion* 255) # Rescale back to 0-255 range
  299. #display(Image.fromarray(ion))
  300. # Convert grayscale to RGB IHC-like coloring
  301. # Create RGB image with brown for high values and light pink for low values
  302. rgb = np.zeros((ion.shape[0], ion.shape[1], 3), dtype=np.uint8)
  303. # Brown color (RGB: 139, 69, 19) for high values
  304. # Light pink (RGB: 255, 228, 225) for low values
  305. rgb[:,:,0] = np.uint8(255 - ion * 0.45) # R channel
  306. rgb[:,:,1] = np.uint8(228 - ion * 0.62) # G channel
  307. rgb[:,:,2] = np.uint8(225 - ion * 0.81) # B channel
  308. # Create a colormap from white to brown
  309. white = np.array([255, 255, 255])
  310. brown = np.array([139, 69, 19])
  311. # Create normalized intensity values between 0 and 1
  312. norm_ion = ion.astype(float) / 255
  313. # For each pixel, interpolate between white and brown based on intensity
  314. for i in range(3): # RGB channels
  315. rgb[:,:,i] = np.uint8(white[i] + (brown[i] - white[i]) * norm_ion)
  316. rgb[mask == 0] = 0
  317. # This creates a brownish-purple tone typical of IHC staining
  318. ion = rgb
  319. # Add text overlay with category name
  320. font = cv2.FONT_HERSHEY_SIMPLEX
  321. font_scale = 0.8
  322. font_color = (255, 50, 60) # White text
  323. font_thickness = 2
  324. text_size = cv2.getTextSize(category, font, font_scale, font_thickness)[0] * 2
  325. # Position text at top center
  326. ##text_x = (ion.shape[1]) // 2
  327. text_x = 10
  328. text_y = text_size[1] + 10 # Add some padding from top
  329. # Rotate image 90 degrees counterclockwise
  330. ion = cv2.rotate(ion, cv2.ROTATE_90_COUNTERCLOCKWISE)
  331. ion_no_text = ion.copy()
  332. # Add text to image
  333. category = category.replace("_ARTEFACT", "")
  334. cv2.putText(ion, category, (text_x, text_y), font, font_scale, font_color, font_thickness)
  335. return ion, ion_no_text
  336. # %%
  337. import json
  338. categories_per_mz = defaultdict(list)
  339. for mz in mzs_per_category_with_model[102]:
  340. for cat_idx, mzs in mzs_per_category_with_model.items():
  341. if mz in mzs and cat_idx != 102: # Exclude self-comparison
  342. cat_name = label_encoder.classes_[cat_idx].replace("_ARTEFACT", "")
  343. categories_per_mz[mz].append(cat_name)
  344. # Create folder for this m/z value if it doesn't exist
  345. mz_folder = f"mz_{mz:.4f}"
  346. for path in paths:
  347. img = total_ion_count(np.load(path[0]))
  348. _, ion_no_text = get_ihc_image(img, [mz], '')
  349. Image.fromarray(ion_no_text).save(f"{mz_folder}_{path[1]}.png")
  350. # Save categories per m/z as JSON
  351. with open(f"categories.json", "w") as f:
  352. json.dump(categories_per_mz, f, indent=4)
  353. # %%
  354. import pandas as pd
  355. csv = pd.read_csv("MZ_MODEL_TIC.csv")
  356. # %%
  357. cnu_mzs = mzs_per_parent['CNU']
  358. cnu_mzs = [x for x in cnu_mzs if x not in mzs_per_parent['BST']]
  359. cnu_mzs = [x for x in cnu_mzs if x not in mzs_per_parent['CTX']]
  360. cnu_mzs = [x for x in cnu_mzs if x not in mzs_per_parent['WM']]
  361. cnu_mzs = [x for x in cnu_mzs if x not in mzs_per_parent['BG']]
  362. print(cnu_mzs)
  363. # %%

linear.ipynb at commit de9d453, under MIT · at the source

Overview

  1. Pahnke Lab, www.pahnkelab.eu
  2. Proteomics Core Facility, Department of Immunology, Department of Clinical Medicine (KlinMed), Medical Faculty, University of Oslo (UiO) and Division of Laboratory Medicine (KLM), Oslo University Hospital (OUS), Oslo, Norway
  3. Translational Neurodegeneration Research and Neuropathology Lab, Department of Clinical Medicine, Medical Faculty, University of Oslo, Oslo, Norway
  4. Section of Neuropathology Research, Department of Pathology, Division of Laboratory Medicine, Oslo University Hospital, Oslo, Norway
  5. Institute of Nutritional Medicine, University of Lübeck and University Medical Center Schleswig-Holstein, Lübeck, Germany
  6. Department of Neuromedicine and Neuroscience, The Faculty of Medicine and Life Sciences, University of Latvia, Rīga, Latvia
  7. Department of Neurobiology, School of Neurobiology, Biochemistry and Biophysics, The Georg S. Wise Faculty of Life Sciences, University of Tel Aviv, Ramat Aviv, Israel
Institutions: Oslo University Hospital (Norway); University of Oslo (Norway); University of Latvia (Latvia); University of Lübeck (Germany)
Journal: Free neuropathology, volume 7, article 9
Dates: received 3 March 2026; accepted 17 April 2026; published online 28 April 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.17879/freeneuropathology-2026-9413 · PMID 42078793 · PMCID PMC13129809 · OpenAlex W7157246238
Open access: green, a free copy (OpenAlex)
Status: code verified
Categories: genetics / omics (modality), other (modality), mouse (organism), Alzheimer's / dementia (population), methods / tools (subfield)
Methods: Connectivity, Smoothing, state filtering, decompositions, Machine learning, Statistics
Keywords: Mass spectrometry imaging, Brain atlas, Dimensionality reduction, Machine learning, Data visualization, Explainable machine learning, Lipidomics, Explainability, Alzheimer's disease, MSI-VISUAL, MSI-ATLAS, ABCA7, Index lipids, GM3
Topic: Metabolomics and Mass Spectrometry Studies (Molecular Biology, Biochemistry, Genetics and Molecular Biology), according to OpenAlex
Citations: not cited yet (Europe PMC); 45 references in the paper

Abstract

Recent computational advances in mass spectrometry imaging (MSI) now enable unprecedented insight into organ-wide molecular composition and functional architecture. Here, we present the first high-resolution molecular-computational atlas of specific mouse brain lipids and metabolites, acquired using a NEDC matrix and negative-mode MSI, covering 123 anatomically defined regions and 191 polygonal annotations derived solely from MSI data, without auxiliary imaging. To overcome annotation ambiguity and MSI complexity, we introduced the Computational Brain Lipid Atlas (CBLA), a graph-based visual-explainability framework that generates Virtual Landscape Visualizations (VLVs) of specific lipid distributions across brain substructures. The CBLA integrates dimensionality reduction and ensembles of supervised models to (i) refine annotations, (ii) elucidate interregional relationships, (iii) interpret model behavior, and (iv) formulate biologically testable hypotheses. The CBLA revealed novel lipid distribution patterns, functional integrations, anatomical connections – the brain's telephone cables, and region-specific disease signatures – index lipids, including disease networks in the basal ganglia. It further identified index lipids that trace extrapyramidal nuclei and their cortical-brainstem connections, highlighting network-level molecular organization. A new algorithm decomposes annotated regions into precise mass-to-charge (m/z) features and resolves high-resolution m/z values from MSI data, m/z producing a comprehensive high-resolution brain map. It can be applied to any MS measurements, including metabolites, lipids, and peptides. This resource underpins downstream studies, as exemplified here by characterizing the molecular lipid composition of Aβ plaques in APP and ABCA7 transgenic mice, their spatial arrangement, and their connections with surrounding tissue. For the first time, our data suggest that GM3 ganglioside accumulation in cortical amyloid plaques may originate from hippocampal structures, consistent with longstanding evidence of disrupted hippocampo-cortical connectivity; a similar origin may also apply to plaque-associated Aβ signals in the cortex. More broadly, several selected m/z signals showed putative anatomical origins in specific brain subregions. Together, these findings establish MSI-ATLAS as a general framework for mapping brain organization and disease-related molecular networks directly from MSI data.

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 4 matches between paragraphs and lines of code.

jacobgil/msi-atlas

License: MIT
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: de9d453840f91e8383052ada42322e99547bd95e, 23 May 2025
Languages: Python (4), Jupyter (1)
Size: 9 files, 5 scripts
Software Heritage: not archived
Found in: “Availability of code”
Holds: README, license file, 1 notebook
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (5 files), PyTorch (4 files), Matplotlib (3 files), OpenCV (3 files), scikit-learn (3 files), NetworkX (2 files), Pillow (2 files), Pyro (2 files), pandas (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
7 files

Availability of code

Code is available as an open-source package on GitHub. https://github.com/jacobgil/msi-atlas

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;
  • 5 scripts, each with its path and the digest of its content;
  • 4 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.

Availability of data and material

The datasets generated and analyzed during the current study are available in the ProteomeXchange PRIDE repository under accession number PXD056609.

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 1, 30 September 2026: the first record

Recorded: type, language, journal, volume, pages, dates, 3 authors, 14 keywords, 43 references.

Cite

This paper

Gildenblat, J., Stamnas, J., & Pahnke, J. (2026). Mass spectrometry imaging-based explainable machine learning reveals the biochemical landscapes of the mouse brain. Free neuropathology, 7, 9. https://doi.org/10.17879/freeneuropathology-2026-9413

BibTeX

@article{gildenblat2026mass,
author = {Gildenblat, Jacob and Stamnas, Jorunn and Pahnke, Jens},
title = {{Mass spectrometry imaging-based explainable machine learning reveals the biochemical landscapes of the mouse brain}},
journal = {Free neuropathology},
year = {2026},
month = apr,
volume = {7},
pages = {9},
publisher = {Free Neuropathology General Assembly},
issn = {2699-4445},
doi = {10.17879/freeneuropathology-2026-9413},
url = {https://doi.org/10.17879/freeneuropathology-2026-9413},
pmid = {42078793},
pmcid = {PMC13129809}
}

RIS

TY - JOUR
AU - Gildenblat, Jacob
AU - Stamnas, Jorunn
AU - Pahnke, Jens
TI - Mass spectrometry imaging-based explainable machine learning reveals the biochemical landscapes of the mouse brain
T2 - Free neuropathology
J2 - Free Neuropathol
PY - 2026
DA - 2026/04/28
VL - 7
SP - 9
SN - 2699-4445
PB - Free Neuropathology General Assembly
DO - 10.17879/freeneuropathology-2026-9413
UR - https://doi.org/10.17879/freeneuropathology-2026-9413
LA - en
ER -

CSL-JSON

{
"id": "10.17879/freeneuropathology-2026-9413",
"type": "article-journal",
"title": "Mass spectrometry imaging-based explainable machine learning reveals the biochemical landscapes of the mouse brain",
"container-title": "Free neuropathology",
"author": [
{
"family": "Gildenblat",
"given": "Jacob"
},
{
"family": "Stamnas",
"given": "Jorunn"
},
{
"family": "Pahnke",
"given": "Jens"
}
],
"container-title-short": "Free Neuropathol",
"volume": "7",
"page": "9",
"DOI": "10.17879/freeneuropathology-2026-9413",
"PMID": "42078793",
"PMCID": "PMC13129809",
"ISSN": "2699-4445",
"publisher": "Free Neuropathology General Assembly",
"URL": "https://doi.org/10.17879/freeneuropathology-2026-9413",
"language": "en",
"issued": {
"date-parts": [
[
2026,
4,
28
]
]
}
}

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.1126/sciadv.aed3650 [code]
Truthful visualizations for mass spectrometry imaging enable high-spatial-resolution interactive &lt;i&gt;m/z&lt;/i&gt; mapping and exploration.
Journal: Science advances
In common: NetworkX, OpenCV, Pillow, 5 other tools, other, methods / tools, genetics / omics, 1 other category, 13 references, 2 authors
[2] doi:10.1371/journal.pcbi.1013499 [code]
VesiclePy: A machine learning vesicle analysis toolbox for volume electron microscopy.
Journal: PLoS computational biology
In common: Pyro, OpenCV, Pillow, 5 other tools, methods / tools
[3] doi:10.1038/s41598-026-57519-w [code]
Automated segmentation of neurons and spinal cord structures in immunofluorescence images using SpineDL.
Journal: Scientific reports
In common: NetworkX, OpenCV, Pillow, 5 other tools, methods / tools, mouse
[4] doi:10.1038/s41598-026-61605-4 [code]
Learning precise segmentation of neurofibrillary tangles from rapid manual point annotations.
Journal: Scientific reports
In common: OpenCV, Pillow, PyTorch, 4 other tools, methods / tools, Alzheimer's / dementia, 1 reference
[5] doi:10.1038/s42003-026-10259-z [code]
Spatial transcriptomic profiling of developing mouse hearts reveals a spatially patterned signaling environment.
Journal: Communications biology
In common: Pyro, OpenCV, PyTorch, 4 other tools, genetics / omics, mouse
[6] 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: NetworkX, OpenCV, Pillow, 5 other tools, genetics / omics, mouse
[7] doi:10.1093/bioinformatics/btag652 [code]
mmVelo: a deep generative model for estimating cell state-dependent dynamics across multiple modalities.
Journal: Bioinformatics (Oxford, England)
In common: Pyro, Pillow, PyTorch, 4 other tools, genetics / omics, mouse
[8] doi:10.1002/hbm.70469 [code]
VarCoNet: A Variability-Aware Self-Supervised Framework for Functional Connectome Extraction From Resting-State fMRI.
Journal: Human brain mapping
In common: NetworkX, OpenCV, Pillow, 5 other tools, methods / tools
[9] doi:10.1038/s41398-026-03965-z [code]
Disentangling individual heterogeneity reveals robust network and molecular signatures of major depressive disorder with suicidal ideation.
Journal: Translational psychiatry
In common: NetworkX, OpenCV, Pillow, 5 other tools, genetics / omics
[10] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: NetworkX, OpenCV, Pillow, 5 other tools, mouse

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.