OSCR

Brain Clearance of Contrast Agent in Intravenous DCE-MRI Is Measurable and Cannot Be Explained by Clearance Across the BBB Alone.

Code ↔ Paper

2 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 2 matches
  1. [1] § Methods › DCE‐MRI › Data Extraction and Model Fitting ↔ functions.py, lines 797–922 · score 0.61 · venous impulse response, impulse response functions, deconvolved, arterial, plug, uptake
  2. [2] § Results ↔ rebuttal_functions.py, lines 563–619 · score 0.53 · fast exchange limit, water exchange, BBB, Patlak, 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

Python · 1,562 lines · 58 KB · no license · 1 match

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Created on Tue Nov 2 13:55:59 2021
  5. @author: Martin Kozár
  6. [email hidden]
  7. """
  8. """
  9. Code created using:
  10. Python 3.8.10
  11. Matplotlib 3.3.4
  12. Numpy 1.20.1
  13. Pandas 1.2.4
  14. Scipy 1.6.2
  15. """
  16. import matplotlib.pyplot as plt
  17. from scipy.integrate import cumulative_trapezoid
  18. from scipy.optimize import least_squares
  19. from scipy.signal import convolve, deconvolve
  20. import numpy as np
  21. import os
  22. from multiprocessing import Pool
  23. #from psutil import cpu_count
  24. from multiprocess_function import pool_fun_wrap
  25. from tqdm import tqdm
  26. import copy
  27. import warnings
  28. warnings.simplefilter(action='ignore', category=UserWarning)
  29. import pandas as pd
  30. # %% Helpful wrappers
  31. from functools import wraps
  32. from time import time
  33. def measure(func):
  34. """
  35. Measures function execution time.
  36. """
  37. @wraps(func)
  38. def _time_it(*args, **kwargs):
  39. start = int(round(time() * 1000))
  40. try:
  41. return func(*args, **kwargs)
  42. finally:
  43. end_ = int(round(time() * 1000)) - start
  44. print(f"Exe-time of {func.__name__}: {end_ if end_ > 0 else 0} ms")
  45. return _time_it
  46. def filecheck_csv(func):
  47. """
  48. Checks if a csv file exists and reads it in if it does.
  49. Adds a first positional argument -> filepath : str
  50. """
  51. @wraps(func)
  52. def _file_exists(filepaths, *args,
  53. index_col = None, header = 'infer', **kwargs):
  54. if all(os.path.isfile(filepath) for filepath in filepaths):
  55. t = tuple(
  56. pd.read_csv(
  57. filepath,
  58. index_col = index_col,
  59. header = header
  60. )
  61. for filepath in filepaths
  62. )
  63. return t[0] if len(t) == 1 else t
  64. else:
  65. return func(filepaths, *args, **kwargs)
  66. return _file_exists
  67. def deconv(data, irf):
  68. N = int(2**(np.ceil(np.log2(len(irf) + len(data)))))
  69. return np.abs(np.fft.ifft(np.fft.fft(data, N) / np.fft.fft(irf, N))), None
  70. # %% Helpful functions
  71. @filecheck_csv
  72. def get_data(filepath_s, brain_loc, slice_data, undersampled):
  73. """
  74. Checks if a file with all the data exists, if not, create it.
  75. Parameters
  76. ----------
  77. brain_loc : list
  78. A list of brain locations from which the data was taken.
  79. Returns
  80. -------
  81. multiDF : pandas.core.frame.DataFrame
  82. Multiindex dataframe where index 0 are brain locations and index 1 are
  83. time points.
  84. """
  85. dfs = []
  86. udsstr = "_undersampled" if undersampled else ""
  87. # read in the data from 3 .csv files (different brain_loc, same VIF)
  88. for i in range(len(brain_loc)):
  89. filepath_r = f"PvU_remaster/CSVData/ROI_fits_{brain_loc[i]}_June18_signal_clean{udsstr}.csv"
  90. dfs.append(pd.read_csv(filepath_r, decimal = ','))
  91. # join the data into a single dataframe
  92. multiDF = dfs_into_multiDF(dfs, brain_loc, slice_data)
  93. multiDF.to_csv(make_dir(*filepath_s))
  94. return multiDF
  95. def make_dir(filepath):
  96. """
  97. Checks if a directory exists and creates it if not. Useful when trying to
  98. save a file to a new directory.
  99. Parameters
  100. ----------
  101. filepath : str
  102. File path to save the file.
  103. Returns
  104. -------
  105. filepath : str
  106. """
  107. head, _ = os.path.split(filepath)
  108. if not os.path.isdir(head):
  109. os.makedirs(head, mode = 0o766)
  110. return filepath
  111. @measure
  112. def wrapper_process(inputs, cores):
  113. """
  114. A function for running concurrent processing. The tricky part of using
  115. Pool().map() is that you have to supply exactly 2 arguments: the function
  116. and a list of all inputs you want to run. I use pool_model_wrap() to then
  117. call model_optimise() with all the arguments from inputs unpacked.
  118. Parameters
  119. ----------
  120. inputs : list
  121. A list of all inputs model_optimise requires.
  122. cores : integer
  123. Number of concurrent processes.
  124. Returns
  125. -------
  126. list
  127. A list containing the outputs of running pool_model_wrap with each input.
  128. """
  129. with Pool(cores) as p:
  130. r = list(tqdm(p.imap(pool_fun_wrap, inputs), total = len(inputs)))
  131. print()
  132. return r
  133. def remove_files(filepaths, delete = False):
  134. """
  135. Checks if a file exists and removes it if it does.
  136. Parameters
  137. ----------
  138. filepaths : list-like
  139. List of filepaths.
  140. delete : bool, optional
  141. The default is False.
  142. """
  143. if delete:
  144. for filepath in filepaths:
  145. if os.path.isfile(filepath):
  146. print(f"Removing {filepath}.")
  147. os.remove(filepath)
  148. def check_input(user_input: str, method = "isalnum", boolean = True,
  149. ignore_space = True) -> str:
  150. """
  151. Checks if the user_input is or isn't a chosen type based on the method.
  152. Parameters
  153. ----------
  154. user_input : str
  155. method : str, optional
  156. String method that needs to return 'boolean' to pass the test. Accepts:
  157. isalnum, isalpha, isdecimal, isdigit, isidentifier, islower, isnumeric,
  158. isprintable, isspace, istitle, isupper. The default is "isalpha".
  159. boolean : bool, optional
  160. ignore_space : bool, optional
  161. Returns
  162. -------
  163. user_input : str
  164. """
  165. # string methods that return True/False
  166. str_methods = ["isalnum", "isalpha", "isdecimal", "isdigit", "isidentifier",
  167. "islower", "isnumeric", "isprintable", "isspace", "istitle",
  168. "isupper"]
  169. # check if function parameters are valid
  170. if not isinstance(method, str):
  171. raise TypeError("The 'method' parameter only accepts strings.")
  172. if method not in str_methods:
  173. raise ValueError("Please select a valid built-in Python method for a "
  174. "string that returns True/False, Options are: "
  175. f"{' '.join(str_methods)}. "
  176. f"You chose: {method}")
  177. temp_input = user_input
  178. if ignore_space:
  179. temp_input = temp_input.replace(" ", "")
  180. # apply method to user_input
  181. if getattr(temp_input, method)() != boolean:
  182. raise ValueError(f"Invalid input! Input must cause str.{method}() to"
  183. f" return {boolean}. Your input was: {user_input}")
  184. return user_input
  185. # %% Loading data and simple data statistics
  186. def get_filters(patient_dict, index_data):
  187. """
  188. Creates a dictionary where keys are patient descriptions and values are all
  189. column names that correspond to that description.
  190. """
  191. catg_filters = {}
  192. for description, category in patient_dict.items():
  193. catg_filters[description] = ([idx for idx in index_data
  194. if idx.startswith(category)])
  195. return catg_filters
  196. def dfs_into_multiDF(data, brain_loc, slice_data):
  197. """
  198. Turns a list of dataframes into a single multiindex dataframe.
  199. data: List[Dataframe], brain_loc: List[string],
  200. return multiDF: Dataframe
  201. """
  202. loaded_data = []
  203. for i, csv_data in enumerate(data):
  204. temp = (csv_data.dropna(how='all')
  205. .dropna(axis='columns')
  206. .set_index("time")
  207. )
  208. tissue = temp.iloc[:(temp.shape[0]//2)]
  209. VIF = temp.iloc[(temp.shape[0]//2):]
  210. if slice_data:
  211. tissue = tissue.iloc[:int(tissue.shape[0]*slice_data)]
  212. VIF = VIF.iloc[:int(VIF.shape[0]*slice_data)]
  213. temp = pd.concat((tissue, VIF),
  214. keys = ["tissue", "VIF"],
  215. names = ["tissue", "time"])
  216. temp = temp.T
  217. loaded_data.append(temp)
  218. multiDF = pd.concat(loaded_data,
  219. keys = brain_loc,
  220. names = ["brain_loc", "ID"])
  221. return multiDF
  222. @filecheck_csv
  223. def statistics(filepath, data, catg_filters):
  224. """
  225. Summarises data for each patient_desc as columns of stats.
  226. """
  227. stat_labels = ['mean','std','sem']
  228. methods = [pd.DataFrame.mean, pd.DataFrame.std, pd.DataFrame.sem]
  229. # for groupby using a dictionary, assign each person a category
  230. # c - category ("control", "stroke", "Parkinson's")
  231. # P - list of IDs for category
  232. # p - individual IDs
  233. groupby_filters = {p: c for c, P in catg_filters.items() for p in P}
  234. # applies each method in methods and joins up the results in a single df
  235. stat_df = pd.concat([(data.groupby(["brain_loc", groupby_filters],
  236. level = "ID")
  237. .apply(method)
  238. .round(6)
  239. ) for method in methods], keys = stat_labels)
  240. stat_df.to_csv(make_dir(*filepath))
  241. stat_df.T.to_csv(make_dir(f"{os.path.split(*filepath)[0]}/raw_data_statistics_forViewing.csv"))
  242. return stat_df
  243. @filecheck_csv
  244. def assign_group_VIF(filepath, data_indiv, stat_df, catg_filters, brain_loc):
  245. """
  246. Assigns group-averaged VIF data to a copy of the dataframe with raw data.
  247. """
  248. groupby_filters = {p: c for c, P in catg_filters.items() for p in P}
  249. data_group = data_indiv.copy()
  250. data_group["VIF"] = (data_indiv["VIF"].groupby(["brain_loc", groupby_filters],
  251. level = "ID")
  252. .transform(np.mean)
  253. .round(6))
  254. data_group.to_csv(make_dir(*filepath))
  255. return data_group
  256. @filecheck_csv
  257. def join_noVIF_data(filepath, data_i):
  258. """
  259. Remove VIF data from the dataset and save (used when plotting individual curves).
  260. """
  261. df = data_i.loc[:,"tissue"]
  262. df.to_csv(make_dir(*filepath))
  263. return df
  264. @filecheck_csv
  265. def shift_raw_data(filepath, data_raw, cwd):
  266. from scipy import interpolate
  267. tissue = data_raw.loc[:,"tissue"]
  268. VIF = data_raw.loc[:,"VIF"]
  269. #the index value at which the bolus arrives
  270. BATtissue = pd.read_csv(os.path.join(cwd, 'PvU_remaster', 'CSVData', 'BATtissue.csv'), header = None)
  271. BATvif = pd.read_csv(os.path.join(cwd, 'PvU_remaster', 'CSVData', 'BATvif.csv'), header = None)
  272. #create an array of time points for each person
  273. tissueTime = np.tile(tissue.columns.values.astype(float), (tissue.shape[0],1))
  274. #difference between vif and tissue BAT (in units of array index)
  275. timeDiff = (BATvif - BATtissue).to_numpy()
  276. #calculate necessary timeshift
  277. tissueTimeSftd = tissueTime - timeDiff * 7.6
  278. dfTisTimeSftd = pd.DataFrame(tissueTimeSftd, index = tissue.index, columns = tissue.columns)
  279. #df where left is tissue signal, right shifted time
  280. df = pd.concat([tissue, dfTisTimeSftd], axis = 1, keys = ["tissue", "shift"])
  281. def apply_interp(row, x):
  282. """
  283. Interpolate the signal values to a shift. Expects a DataFrame where each
  284. row is separated by a multiindex into tissue signal and shifted timepoints.
  285. Uses scipy.interpolate
  286. Parameters
  287. ----------
  288. row : Series
  289. Row of the dataframe supplied by apply function
  290. x : array-like
  291. Original time points when the signal was taken
  292. """
  293. tck = interpolate.splrep(x, row["tissue"], s = 0)
  294. return interpolate.splev(row["shift"], tck, der = 0)
  295. tissueSftd = df.apply(apply_interp, args = (tissue.columns.values.astype(float), ), axis = 1, result_type='expand')
  296. tissueSftd.columns = tissue.columns
  297. output = pd.concat([tissueSftd, VIF], axis = 1, keys = ["tissue", "VIF"], names = ['tissue', 'time'])
  298. output.to_csv(make_dir(*filepath))
  299. return output
  300. # %% Optimisation (data fitting)
  301. @filecheck_csv
  302. def model_setup(fps_predict, data_indiv_VIF, model_type, time_data,
  303. time_windows, brain_loc, weighted = True):
  304. """
  305. Sets up the combinations of settings for how to fit model to measurements.
  306. Each setting then gets passed to the wrapper_process for multiprocessing.
  307. The output then gets concatenated into 3 dataframes and returned.
  308. Parameters
  309. ----------
  310. fps_predict : str
  311. Filepath to save the prediction values.
  312. data : tuple
  313. Contains the individual-VIF and group-VIF data.
  314. model_type : str
  315. time_data : numpy.ndarray
  316. The timestamps at which the data was measured.
  317. time_window : list
  318. List containing the cut-off time stamp for weighting in seconds.
  319. brain_loc : list
  320. Strings describing the brain locations where measurements were taken.
  321. weighted : bool, optional
  322. Toggle weighting for the residual function. The default is True.
  323. Returns
  324. -------
  325. concatd_data : tuple
  326. Contains (values predicted by model, residual function, constant
  327. values fitted to the model) - each element is a DataFrame.
  328. """
  329. print("Setting up model.")
  330. if not weighted:
  331. time_windows = [(0, 0)]
  332. model_settings = []
  333. concat_keys = []
  334. for time_window in time_windows:
  335. model_settings.append([model_optimise, data_indiv_VIF, model_type,
  336. time_data, time_window, brain_loc])
  337. concat_keys.append(str(time_window))
  338. print("Done.", end = "\n\n")
  339. # choose number of cores to use
  340. L = len(model_settings)
  341. if L > 6:
  342. cores = 6
  343. elif 6 > L > 1:
  344. for i in range(L, 1, -1):
  345. if L % i == 0:
  346. cores = i
  347. else:
  348. cores = 1
  349. print(f"Fitting model to data using {cores} cores.")
  350. fitted_data_disjoined = wrapper_process(model_settings, cores)
  351. concatd_data = tuple(pd.concat(df,
  352. keys = concat_keys,
  353. names = ["time_window","brain_loc","ID"])
  354. for df in zip(*fitted_data_disjoined))
  355. print("Done.", end = "\n\n")
  356. # calculate v_p and k_trans from 2CUptake predictions
  357. if model_type == "2CUptake":
  358. pred_const_data = concatd_data[2]
  359. pred_const_data["v_p"] = pred_const_data["F_p"] * pred_const_data["T_p"]
  360. pred_const_data["k_trans"] = pred_const_data["F_p"] * pred_const_data["E"]
  361. if model_type == "2CExchange":
  362. pred_const_data = concatd_data[2]
  363. #sourbron perfusion eq. [9]
  364. pred_const_data["T_B"] = np.reciprocal(pred_const_data["K_plus"] - pred_const_data["E_minus"] * (pred_const_data["K_plus"] - pred_const_data["K_minus"]))
  365. #[10]
  366. pred_const_data["T_E"] = np.reciprocal(pred_const_data["T_B"] * pred_const_data["K_plus"] * pred_const_data["K_minus"])
  367. #[11]
  368. pred_const_data["T_P"] = np.reciprocal(pred_const_data["K_plus"] + pred_const_data["K_minus"] - np.reciprocal(pred_const_data["T_E"]))
  369. #next 3 are [12]
  370. pred_const_data["v_p"] = pred_const_data["F_P"] * pred_const_data["T_B"]
  371. pred_const_data["PS"] = pred_const_data["F_P"] * (pred_const_data["T_B"] / pred_const_data["T_P"] - 1)
  372. pred_const_data["v_e"] = pred_const_data["PS"] * pred_const_data["T_E"]
  373. print("Saving model data.")
  374. for data, filepath in zip(concatd_data, fps_predict):
  375. data.to_csv(filepath)
  376. #predictions, resid_df, const_pred, x0_df
  377. return concatd_data
  378. def model_optimise(data, model_type, time_data, time_window, brain_loc):
  379. """
  380. Sets up empty dataframes and fills them up with the model values.
  381. Parameters
  382. ----------
  383. data : pandas.core.frame.DataFrame
  384. model_type : str
  385. time_data : numpy.ndarray
  386. The timestamps at which the data was measured.
  387. time_window : list
  388. List containing the cut-off time stamp for weighting in seconds.
  389. brain_loc : list
  390. Strings describing the brain locations where measurements were taken.
  391. Returns
  392. -------
  393. predictions : pandas.core.frame.DataFrame
  394. Values predicted by model.
  395. resid_df : pandas.core.frame.DataFrame
  396. Residual function.
  397. const_pred : pandas.core.frame.DataFrame
  398. Constant values fitted to the model.
  399. """
  400. #x0 - array of starting points
  401. const_list, x0, bounds = _set_model_inputs(model_type)
  402. person_list = data.index.get_level_values("ID").unique()
  403. midx = pd.MultiIndex.from_product([brain_loc, person_list],
  404. names = ["brain_loc","ID"])
  405. #prediction curves
  406. predictions = pd.DataFrame(index=midx, columns=time_data, dtype = np.float64)
  407. #fitted parameters
  408. const_pred = pd.DataFrame(index=pd.MultiIndex.from_product([brain_loc, person_list],
  409. names = ["brain_loc", "ID"]),
  410. columns=const_list)
  411. resid_df = pd.DataFrame(index=midx, columns=time_data)
  412. #starting parameters
  413. x0_df = pd.DataFrame(index=midx, columns = const_list)
  414. (predictions[predictions.columns],
  415. resid_df[resid_df.columns],
  416. const_pred[const_pred.columns],
  417. x0_df[x0_df.columns]) = zip(*data.apply(_scipy_fun_wrap,
  418. axis = 1,
  419. args = (residual,
  420. x0,
  421. time_data/60,
  422. time_window,
  423. model_type
  424. ),
  425. method = "trf",
  426. bounds = bounds
  427. )
  428. )
  429. return predictions, resid_df, const_pred, x0_df
  430. def residual(x, VIF, measured_data, time_data, time_window,
  431. model_type, integral):
  432. """
  433. Calculates the difference between model and measured values.
  434. Parameters
  435. ----------
  436. x : array_like
  437. Contains the variables to be determined.
  438. VIF : array_like
  439. Contains input data used for prediction.
  440. measured_data : array_like
  441. Contains measured data to compare the prediction against.
  442. time_data : array_like
  443. time_data points at which the data was measured.
  444. time_window : tuple
  445. 'weighted = True' sets data in the period time_window to zero.
  446. model_type : string
  447. Determines which model to use.
  448. Returns
  449. -------
  450. array_like
  451. Array-like with differences between predicted and measured values.
  452. """
  453. if model_type == "Intravascular":
  454. #x -> v_p
  455. temp = measured_data - x[0] * VIF
  456. temp[temp.index.astype(float) < time_window] = 0
  457. return temp
  458. elif model_type == "Patlak":
  459. #x -> v_p, k_trans
  460. temp = measured_data - (x[0]*VIF + x[1]*integral)
  461. #temp[60 < temp.index < 280] = 0
  462. temp[temp.index.astype(float) < time_window] = 0
  463. return temp
  464. elif model_type == "2CUptake":
  465. #x -> F_p, T_p, E
  466. # the search function in scipy searches values outside of float64
  467. #so just ignore the warnings if necessary
  468. # with np.errstate(over='ignore', invalid='ignore'):
  469. R = np.exp(-time_data/x[1]) + (x[2] * (1 - np.exp(-time_data/x[1])))
  470. convolution = convolve(x[0] * R, VIF)*(time_data[2]-time_data[1])
  471. hl = round(convolution.size/2)
  472. temp = measured_data - (convolution[:hl+1])
  473. temp[temp.index.astype(float) < time_window] = 0
  474. return temp
  475. elif model_type == "2CUptakeDisp":
  476. VIF = VIF.to_numpy()
  477. time_data = time_data.to_numpy()
  478. #x -> F_p, T_p, E, T_v
  479. # the search function in scipy searches values outside of float64
  480. #so just ignore the warnings if necessary
  481. # with np.errstate(over='ignore', invalid='ignore'):
  482. # this is a time array for the impulse response function. It needs to be longer than time_data
  483. time_irf = np.arange(0, time_data[1]*(2*len(VIF)-2), time_data[1])
  484. #this is the venous impulse response function
  485. irf_v = np.exp(-time_irf/x[3])/(x[3])
  486. #this is the length we need to pad the VIF with zeroes
  487. lengthtopad = len(time_irf)-len(VIF)
  488. #this is the padded VIF. You will probably need to change how this is done in python.
  489. VIF_pad = np.concatenate((VIF, np.tile(0, lengthtopad)))
  490. #this should provide the full deconvolution
  491. C_p, _ = deconv(VIF_pad, irf_v)
  492. C_p = C_p / time_data[1]
  493. #this crops the full deconvolution to the bit we are interested in
  494. C_p2 = C_p[0:len(VIF)]
  495. #this defines the irf of the cappilary bed
  496. irf_c = np.exp(-time_irf/x[1])/(x[1])
  497. #this bit pads the cappilary concentration with zeros
  498. C_p2_padded = np.concatenate((C_p2, np.tile(0, lengthtopad)))
  499. #this deconvolves the cappilary concentration to the arterial concentration
  500. C_a, _ = deconv(C_p2_padded, irf_c)
  501. C_a = C_a / time_data[1]
  502. #this crops the arterial concentration
  503. AIF = C_a[0:len(VIF)]
  504. R = irf_c[0:len(VIF)] + (x[2] * (1 - np.exp(-time_data/x[1])))
  505. convolution = convolve(x[0] * R, AIF)*(time_data[2]-time_data[1])
  506. hl = round(convolution.size/2)
  507. temp = measured_data - (convolution[:hl+1])
  508. temp[temp.index.astype(float) < time_window] = 0
  509. return temp
  510. elif model_type == "2CUptakePlug":
  511. VIF = VIF.to_numpy()
  512. time_data = time_data.to_numpy()
  513. #x -> F_p, T_p, E, T_v, T_l
  514. # the search function in scipy searches values outside of float64
  515. #so just ignore the warnings if necessary
  516. # with np.errstate(over='ignore', invalid='ignore'):
  517. # this is a time array for the impulse response function. It needs to be longer than time_data
  518. time_irf = np.arange(0, time_data[1]*(2*len(VIF)-2), time_data[1])
  519. #this is the venous impulse response function
  520. irf_v = np.exp(-time_irf/x[3])/(x[3])
  521. #this is the length we need to pad the VIF with zeroes
  522. lengthtopad = len(time_irf)-len(VIF)
  523. #this is the padded VIF. You will probably need to change how this is done in python.
  524. VIF_pad = np.concatenate((VIF, np.tile(0, lengthtopad)))
  525. #this should provide the full deconvolution
  526. C_p, _ = deconv(VIF_pad, irf_v)
  527. C_p = C_p / time_data[1]
  528. #this crops the full deconvolution to the bit we are interested in
  529. C_p2 = C_p[0:len(VIF)]
  530. #this defines the irf of the cappilary bed
  531. irf_c = np.heaviside(x[1] - time_irf, 0)*np.exp(-time_irf/x[4])/(x[1])
  532. #this bit pads the cappilary concentration with zeros
  533. C_p2_padded = np.concatenate((C_p2, np.tile(0, lengthtopad)))
  534. #this deconvolves the cappilary concentration to the arterial concentration
  535. C_a, _ = deconv(C_p2_padded, irf_c)
  536. C_a = C_a / time_data[1]
  537. #this crops the arterial concentration
  538. AIF = C_a[0:len(VIF)]
  539. k_trans = x[2] * x[0]
  540. irf_e = (k_trans / x[2]) * (1 - np.exp(-time_data / x[4])) * np.heaviside(x[1] - time_data, 0) + k_trans * np.heaviside(time_data - x[1], 0)
  541. h_p = irf_c[0:len(VIF)]
  542. v_p = x[0] * x[1]
  543. H = h_p * v_p + irf_e
  544. convolution = convolve(H, AIF)*(time_data[2]-time_data[1])
  545. hl = round(convolution.size/2)
  546. temp = measured_data - (convolution[:hl+1])
  547. temp[temp.index.astype(float) < time_window] = 0
  548. return temp
  549. elif model_type == "ExTofts":
  550. #x -> v_p, k_trans, v_e
  551. #Extended Tofts is the Tofts model (TM) plus intravascular component
  552. convolution = convolve(np.exp(-time_data*x[1]/x[2]), VIF)*(time_data[2]-time_data[1])
  553. #TM = x[1] * convolution[:round(convolution.size/2)+1]
  554. TM = x[1] * convolution[:len(VIF)]
  555. #concentration curve
  556. C = x[0]*VIF + TM
  557. temp = measured_data - C
  558. temp[temp.index.astype(float) < time_window] = 0
  559. return temp
  560. elif model_type == "Clearance":
  561. #x -> v_p, k_trans, k
  562. convolution = convolve(np.exp(-time_data*x[2]), VIF)*(time_data[2]-time_data[1])
  563. TM = x[1] * convolution[:round(convolution.size/2)+1]
  564. #concentration curve
  565. C = x[0]*VIF + TM
  566. temp = measured_data - C
  567. temp[temp.index.astype(float) < time_window] = 0
  568. return temp
  569. elif model_type == "2CExchange":
  570. #x -> F_P, E_minus, K_plus, K_minus
  571. # R = e^(-t*K+) + (E- * (e^(-t*K-) - e^(-t*K+)))
  572. R = np.exp(-time_data*x[2]) + (x[1] * (np.exp(-time_data * x[3]) - np.exp(-time_data * x[2])))
  573. # (F_P * R) (X) VIF
  574. convolution = convolve(x[0] * R, VIF)*(time_data[2]-time_data[1])
  575. hl = round(convolution.size/2)
  576. temp = measured_data - (convolution[:hl+1])
  577. temp[temp.index.astype(float) < time_window] = 0
  578. return temp
  579. def _set_model_inputs(model_type):
  580. """
  581. Sets initial guess and bounds based on model_type (different number of
  582. parameters) and zero_ktrans (sets k_trans to zero).
  583. Parameters
  584. ----------
  585. model_type : str
  586. Returns
  587. -------
  588. const_list : list
  589. List of names of the fitted parameters.
  590. x0 : numpy.ndarray
  591. Initial guess for optimisation function.
  592. bounds : 2-tuple containing lists
  593. Sets the boundaries on the fitted parameters.
  594. """
  595. #x0 is a list of np arrays
  596. if model_type == "Intravascular":
  597. const_list = ["v_p"]
  598. x0 = [np.array([0.03])]
  599. bounds = ([-np.inf], [np.inf])
  600. elif model_type == "Patlak":
  601. const_list = ["v_p", "k_trans"]
  602. x0 = list(np.array([0.03, x]) for x in np.linspace(0.0001, 0.005, 20))
  603. bounds = ([-np.inf,-np.inf], [np.inf,np.inf])
  604. elif model_type == "2CUptake":
  605. const_list = ["F_p", "T_p", "E"]
  606. x0 = list(np.array([x, 0.1, 0.00125]) for x in np.linspace(0.05, 1, 20))
  607. bounds = ([0,0,0],[1,0.3,1])
  608. elif model_type == "2CUptakeDisp":
  609. const_list = ["F_p", "T_p", "E", "T_v"]
  610. x0 = list(np.array([x, 0.1, 0.00125, 0.1]) for x in np.linspace(0.05, 1, 20))
  611. bounds = ([0,0,0,0],[1,0.3,1,0.3])
  612. elif model_type == "2CUptakePlug":
  613. const_list = ["F_p", "T_p", "E", "T_v", "T_l"]
  614. x0 = list(np.array([x, 0.1, 0.00125, 0.1, 0.1]) for x in np.linspace(0.05, 1, 20))
  615. bounds = ([0,0,0,0,0],[1,0.3,1,0.3,0.3])
  616. #v_e limits based on DOI 10.1002/mrm.25793
  617. elif model_type == "ExTofts":
  618. const_list = ["v_p", "k_trans", "v_e"]
  619. x0 = list(np.array([0.03, x, 0.1]) for x in np.linspace(0.0001, 0.005, 20))
  620. bounds = ([0,-np.inf,0.0], [1,np.inf,1])
  621. elif model_type == "2CExchange":
  622. const_list = ["F_P", "E_minus", "K_plus", "K_minus"]
  623. x0 = list(np.array([x,1,1,0.5]) for x in np.linspace(0.05, 1, 20))
  624. bounds = ([0,0,0,0],[5,100,100,100])
  625. else:
  626. print(f"model_type = {model_type}")
  627. #raise ValueError('model_type invalid input, valid inputs are: "Patlak", "2CUptake"')
  628. return const_list, x0, bounds
  629. def _scipy_fun_wrap(row, fun, x0, time_data, time_window, model_type, **kwargs):
  630. """
  631. Calls the function scipy.optimize.least_squares to do the model fitting and
  632. uses the results to calculate measurement prediction based on VIF.
  633. Parameters
  634. ----------
  635. row : pandas Series
  636. Individual rows given by .apply() function from pandas.
  637. fun : function
  638. The function that calculates the residual error between model and measurement.
  639. x0 : array-like
  640. Initial guess of the fitted constants.
  641. time_data : array-like
  642. Time stamps at which the data was taken.
  643. time_window : integer
  644. If weighted == True, residuals up to this point get weighted to zero.
  645. model_type : string
  646. Which model to use for fitting and predicting.
  647. **kwargs :
  648. Extra params for least_squares().
  649. Returns
  650. -------
  651. predictions: array-like
  652. Array containing predictions after fitting.
  653. residual: array-like
  654. Residuals after fitting is complete.
  655. constants: array-like
  656. Fitted values for constants.
  657. xnot: array-like
  658. Starting values with lowest sum of residuals.
  659. """
  660. # add a loop over several x0 starting points and choose lowest residual sum
  661. tissue, VIF = row["tissue"], row["VIF"]
  662. integral = None
  663. if model_type == "Patlak":
  664. VIF_copy = VIF.copy()
  665. VIF_copy[VIF_copy.index.astype(float) < time_window] = 0
  666. integral = cumulative_trapezoid(
  667. VIF_copy,
  668. time_data,
  669. initial=0
  670. )
  671. residuals_list = []
  672. #loop over all starting points to find global minimum
  673. for xnot in x0:
  674. res_lsq = least_squares(fun, xnot,
  675. ftol=1e-12, xtol=1e-12, gtol=1e-12,
  676. args = (VIF,
  677. tissue,
  678. time_data,
  679. time_window,
  680. model_type,
  681. integral),
  682. **kwargs
  683. )
  684. residuals_list.append((res_lsq.fun.sum(), res_lsq, xnot))
  685. #get the result with minimum residual sum
  686. res_lsq, xnot = min(residuals_list, key = lambda x:x[0])[1:]
  687. prediction = model_predict(res_lsq.x, VIF, time_data, integral, model_type)
  688. return prediction, res_lsq.fun, res_lsq.x, xnot
  689. def model_predict(x, VIF, time_data, integral, model_type):
  690. """
  691. Predicts measured value based on VIF and fitted constants.
  692. Parameters
  693. ----------
  694. x : array_like
  695. List of constants in the same order as in residual().
  696. VIF : array_like
  697. Contains input data used for prediction.
  698. time_data : array_like
  699. time_data points at which the data was measured.
  700. integral : array_like or None
  701. In Patlak model we do a single integral instead of many convolutions.
  702. model_type : string
  703. Determines which model to use.
  704. Returns
  705. -------
  706. array_like
  707. Array-like with the predictions based on VIF.
  708. """
  709. if model_type == "Intravascular":
  710. #x -> v_p
  711. return x[0]*VIF
  712. elif model_type == "Patlak":
  713. #x -> v_p, k_trans
  714. return x[0]*VIF + x[1]*integral
  715. elif model_type == "2CUptake":
  716. #x -> F_p, T_p, E
  717. R = np.exp(-time_data/x[1]) + (x[2] * (1 - np.exp(-time_data/x[1])))
  718. convolution = convolve(x[0] * R, VIF)*(time_data[2]-time_data[1])
  719. hl = round(convolution.size/2)
  720. return convolution[:hl+1]
  721. elif model_type == "2CUptakeDisp":
  722. #x -> F_p, T_p, E, T_v
  723. VIF = VIF.to_numpy()
  724. time_data = time_data.to_numpy()
  725. #x -> F_p, T_p, E, T_v
  726. # the search function in scipy searches values outside of float64
  727. #so just ignore the warnings if necessary
  728. # with np.errstate(over='ignore', invalid='ignore'):
  729. # this is a time array for the impulse response function. It needs to be longer than time_data
  730. time_irf = np.arange(0, time_data[1]*(2*len(VIF)-2), time_data[1])
  731. #this is the venous impulse response function
  732. irf_v = np.exp(-time_irf/x[3])/(x[3])
  733. #this is the length we need to pad the VIF with zeroes
  734. lengthtopad = len(time_irf)-len(VIF)
  735. #this is the padded VIF. You will probably need to change how this is done in python.
  736. VIF_pad = np.concatenate((VIF, np.tile(0, lengthtopad)))
  737. #this should provide the full deconvolution
  738. C_p, _ = deconv(VIF_pad, irf_v)
  739. C_p = C_p / time_data[1]
  740. #this crops the full deconvolution to the bit we are interested in
  741. C_p2 = C_p[0:len(VIF)]
  742. #this defines the irf of the cappilary bed
  743. irf_c = np.exp(-time_irf/x[1])/(x[1])
  744. #this bit pads the cappilary concentration with zeros
  745. C_p2_padded = np.concatenate((C_p2, np.tile(0, lengthtopad)))
  746. #this deconvolves the cappilary concentration to the arterial concentration
  747. C_a, _ = deconv(C_p2_padded, irf_c)
  748. C_a = C_a / time_data[1]
  749. #this crops the arterial concentration
  750. AIF = C_a[0:len(VIF)]
  751. R = irf_c[0:len(VIF)] + (x[2] * (1 - np.exp(-time_data/x[1])))
  752. convolution = convolve(x[0] * R, AIF)*(time_data[2]-time_data[1])
  753. hl = round(convolution.size/2)
  754. return convolution[:hl+1]
  755. elif model_type == "2CUptakePlug":
  756. VIF = VIF.to_numpy()
  757. time_data = time_data.to_numpy()
  758. #x -> F_p, T_p, E, T_v, T_l
  759. # the search function in scipy searches values outside of float64
  760. #so just ignore the warnings if necessary
  761. # with np.errstate(over='ignore', invalid='ignore'):
  762. # this is a time array for the impulse response function. It needs to be longer than time_data
  763. time_irf = np.arange(0, time_data[1]*(2*len(VIF)-2), time_data[1])
  764. #this is the venous impulse response function
  765. irf_v = np.exp(-time_irf/x[3])/(x[3])
  766. #this is the length we need to pad the VIF with zeroes
  767. lengthtopad = len(time_irf)-len(VIF)
  768. #this is the padded VIF. You will probably need to change how this is done in python.
  769. VIF_pad = np.concatenate((VIF, np.tile(0, lengthtopad)))
  770. #this should provide the full deconvolution
  771. C_p, _ = deconv(VIF_pad, irf_v)
  772. C_p = C_p / time_data[1]
  773. #this crops the full deconvolution to the bit we are interested in
  774. C_p2 = C_p[0:len(VIF)]
  775. #this defines the irf of the cappilary bed
  776. irf_c = np.heaviside(x[1] - time_irf, 0)*np.exp(-time_irf/x[4])/(x[1])
  777. #this bit pads the cappilary concentration with zeros
  778. C_p2_padded = np.concatenate((C_p2, np.tile(0, lengthtopad)))
  779. #this deconvolves the cappilary concentration to the arterial concentration
  780. C_a, _ = deconv(C_p2_padded, irf_c)
  781. C_a = C_a / time_data[1]
  782. #this crops the arterial concentration
  783. AIF = C_a[0:len(VIF)]
  784. k_trans = x[2] * x[0]
  785. irf_e = (k_trans / x[2]) * (1 - np.exp(-time_data / x[4])) * np.heaviside(x[1] - time_data, 0) + k_trans * np.heaviside(time_data - x[1], 0)
  786. h_p = irf_c[0:len(VIF)]
  787. v_p = x[0] * x[1]
  788. H = h_p * v_p + irf_e
  789. convolution = convolve(H, AIF)*(time_data[2]-time_data[1])
  790. hl = round(convolution.size/2)
  791. return convolution[:hl+1]
  792. elif model_type == "ExTofts":
  793. #x -> v_p, k_trans, v_e
  794. convolution = convolve(np.exp(-time_data*x[1]/x[2]), VIF)*(time_data[2]-time_data[1])
  795. #TM = x[1] * convolution[:round(convolution.size/2)+1]
  796. TM = x[1] * convolution[:len(VIF)]
  797. #concentration curve
  798. return x[0]*VIF + TM
  799. elif model_type == "2CExchange":
  800. #x -> F_P, E_minus, K_plus, K_minus
  801. R = np.exp(-time_data * x[2]) + (x[1] * (np.exp(-time_data * x[3]) - np.exp(-time_data * x[2])))
  802. convolution = convolve(x[0] * R, VIF)*(time_data[2]-time_data[1])
  803. hl = round(convolution.size/2)
  804. return convolution[:hl+1]
  805. # %% Plotting
  806. def pred_plt_wrap(model_type, df_pred, df_meas, dir_plt):
  807. """
  808. Sets up a dataframe with each row having predicted and the appropriate measured
  809. values next to each other, then feeds each row for plotting.
  810. Parameters
  811. ----------
  812. model_type : str
  813. df_pred : pandas.core.frame.DataFrame
  814. Dataframe containing prediction values.
  815. df_meas : pandas.core.frame.DataFrame
  816. Dataframe containing measured values. Expected to have layers
  817. compatible with df_pred for joining.
  818. Returns
  819. -------
  820. None.
  821. """
  822. df_pred.columns = pd.MultiIndex.from_product([['predictions'],
  823. df_pred.columns],
  824. names = ["data", "time"])
  825. df_meas.columns = pd.MultiIndex.from_product([['measurements'],
  826. df_meas.columns],
  827. names = ["data", "time"])
  828. df_P_M = (df_pred.reset_index(("time_window"), drop = False)
  829. .join(df_meas)
  830. .set_index(["time_window"], append = True)
  831. )
  832. df_P_M = df_P_M.reorder_levels(["time_window", "brain_loc", "ID"])
  833. # it might make sense to use threading instead since this is a lot of saving? haven't tested the speed
  834. with Pool(6) as p:
  835. #remove .head(5) to plot all rows
  836. list(tqdm(p.imap(prediction_plot,
  837. ((model_type, dir_plt, x) for x in df_P_M.iterrows())),
  838. total = df_P_M.shape[0]))
  839. print()
  840. def prediction_plot(row):
  841. """
  842. Creates a lineplot and labels it using data from row, saves and clears figure.
  843. Parameters
  844. ----------
  845. row : tuple
  846. Tuple of (model_type, cwd, (index labels and pandas Series)).
  847. Returns
  848. -------
  849. None.
  850. """
  851. model_type = row[0]
  852. dir_plt = row[1]
  853. labels = row[2][0]
  854. data = row[2][1]
  855. data.rename(index=lambda val: int(float(val)), level = "time", inplace = True)
  856. data["predictions"].plot()
  857. data["measurements"].plot()
  858. dirdesc = '/'.join(str(x) for x in labels[0:-1])
  859. plt.title(f"{model_type} Time window: {labels[0]}, Location: {labels[1]}, ID: {labels[2]}")
  860. plt.xlabel('Time (s)')
  861. plt.ylabel('Contrast agent concentration (mM)')
  862. plt.legend(["prediction", "measurement"])
  863. dirpath = (f"{dir_plt}/graphs/{model_type}/" + dirdesc)
  864. if not os.path.isdir(dirpath):
  865. os.makedirs(dirpath, mode = 0o766)
  866. plt.savefig(f"{dirpath}/pred_vs_meas_{model_type}_{labels}.jpg")
  867. plt.clf()
  868. def boxplot_const_wrap(pred_const_data, model_type, time_windows, dir_plt):
  869. """
  870. Plots a boxplot for each constant and brain location.
  871. Parameters
  872. ----------
  873. pred_const_data : pandas.core.frame.DataFrame
  874. model_type : str
  875. Returns
  876. -------
  877. None.
  878. """
  879. dpi = 96
  880. res = (1920,1080)
  881. unstack_lvls = ['time_window', 'patient_catg', 'ID']
  882. # with ktrans -> Patlak
  883. plot_data = (pred_const_data.unstack(level = unstack_lvls)
  884. .stack(level = 0)
  885. )
  886. # unique values describing the plotted data
  887. uniq_cols = plot_data.columns.get_level_values("time_window").unique()
  888. if uniq_cols.dtype == int:
  889. uniq_cols = uniq_cols.astype(int)
  890. for row in plot_data.iterrows():
  891. data = row[1]
  892. const_label = row[0][1]
  893. brain_loc = row[0][0]
  894. fig, axs = plt.subplots(1, uniq_cols.size,
  895. num = f'{const_label} {brain_loc}',
  896. sharey = "row",
  897. figsize = (res[0]/dpi, res[1]/dpi),
  898. dpi = dpi)
  899. data = data.unstack(level = "time_window")
  900. data = data.reindex(["control","stroke","Parkinson's"], level = "patient_catg")
  901. categories = data.index.get_level_values("patient_catg").unique()
  902. axs = data.boxplot(by = "patient_catg",
  903. ax = axs,
  904. grid = False)
  905. for i, window in enumerate(uniq_cols):
  906. for w, description in enumerate(categories):
  907. scat_data = data.loc[description, window]
  908. #add a random jiggle to the scatter points
  909. x = np.random.normal(w+1, 0.05, len(scat_data))
  910. axs[i].scatter(x = x,
  911. y = scat_data,
  912. alpha = 0.4)
  913. axs[i].set_title(f"Cutoff: {window}s")
  914. # pandas adds the input of "by=" as xlabel, this removes it
  915. axs[i].set(xlabel = "")
  916. axs[i].grid(axis = "y")
  917. if const_label == "v_e":
  918. axs[i].set_ylim([0, 0.06])
  919. fig.suptitle(f"Boxplots of {model_type} predictions at various time windows for "
  920. f"constant: {const_label} in: {brain_loc}")
  921. if const_label == "k_trans":
  922. axs[0].set_ylabel("$\mathregular{K_{trans}}$ [min⁻¹]")
  923. elif const_label == "v_p":
  924. axs[0].set_ylabel("$\mathregular{v_p}$ [ml⁻¹ tissue]")
  925. elif const_label == "E":
  926. axs[0].set_ylabel("E")
  927. elif const_label == "F_p":
  928. axs[0].set_ylabel("$\mathregular{F_p}$ [ml min⁻¹]")
  929. elif const_label == "T_p":
  930. axs[0].set_ylabel("$\mathregular{T_p}$ [min⁻¹]")
  931. elif const_label == "v_e":
  932. axs[0].set_ylabel("$\mathregular{v_e}$ [ml cc⁻¹ tissue]")
  933. elif const_label == "E_minus":
  934. axs[0].set_ylabel("$\mathregular{E-}$")
  935. elif const_label == "K_minus":
  936. axs[0].set_ylabel("$\mathregular{K-}$ [min⁻¹]")
  937. elif const_label == "K_plus":
  938. axs[0].set_ylabel("$\mathregular{K+}$ [min⁻¹]")
  939. dirpath = f"{dir_plt}/boxplots_constants/{model_type}"
  940. if not os.path.isdir(dirpath):
  941. os.makedirs(dirpath, mode = 0o766)
  942. filepath = f"{dirpath}/box_{model_type}_{const_label}_{brain_loc}.png"
  943. if not os.path.isfile(filepath):
  944. fig.savefig(filepath)
  945. plt.close(fig)
  946. def boxplot_AIC_wrap(AICs, pred_const_data, descriptions, brain_locs, models,
  947. undersampled):
  948. AICs_lst = copy.deepcopy(AICs)
  949. dpi = 96
  950. res = (1920,1080)
  951. uniq_cols = pred_const_data.index.get_level_values("time_window").unique()
  952. #reorder so I get index with patient category labels with the same order as AICs
  953. midx = (pred_const_data.copy()
  954. .unstack("patient_catg")
  955. .reindex(AICs[0].index)
  956. .stack("patient_catg")
  957. ).index
  958. for i in range(len(AICs_lst)):
  959. AICs_lst[i].index = midx
  960. data = pd.concat(AICs_lst, keys = tuple(models)).unstack("time_window")
  961. print(data)
  962. temp = list(data.index.names)
  963. temp[0] = "model"
  964. data.index.names = temp
  965. data = data.reorder_levels(["patient_catg","brain_loc","model","ID"])
  966. s = "_undersampled" if undersampled else ""
  967. dirpath = f"{os.getcwd()}/PvUgraphs&tables{s}/boxplots_AIC/"
  968. models_in_order = pd.unique(data.index.get_level_values('model'))
  969. # Convert only that level to a categorical with a frozen order
  970. data.index = data.index.set_levels(
  971. pd.CategoricalIndex(models_in_order, categories=models_in_order, ordered=True),
  972. level='model'
  973. )
  974. for desc in descriptions:
  975. for brain_loc in brain_locs:
  976. fig, axs = plt.subplots(1, uniq_cols.size,
  977. num = f'{desc} {brain_loc}',
  978. sharey = "row",
  979. figsize = (res[0]/dpi, res[1]/dpi),
  980. dpi = dpi)
  981. #the groupby inside boxplot sorts the data which is annoying
  982. axs = data.loc[(desc, brain_loc)].boxplot(by = "model",
  983. ax = axs,
  984. rot = 45,
  985. grid = False)
  986. for j, ax in enumerate(axs):
  987. plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")
  988. ax.set_title(f"Window: {uniq_cols[j]}s")
  989. ax.set(xlabel = "")
  990. ax.grid(axis = "y")
  991. for w, model in enumerate(models):
  992. scat_data = data.loc[(desc, brain_loc, model), uniq_cols[j]]
  993. x = np.random.normal(w+1, 0.05, len(scat_data))
  994. ax.scatter(x = x,
  995. y = scat_data,
  996. alpha = 0.4)
  997. axs[0].set_ylabel("AIC")
  998. fig.suptitle(f"Akaike information criterion for fits at different time windows in {brain_loc} for {desc}")
  999. dirpath_d = f"{dirpath}{desc}/"
  1000. filepath = make_dir(dirpath_d) + f"AIC_model_comparison_{brain_loc}.png"
  1001. #only closes the figure when saving
  1002. #if not os.path.isfile(filepath):
  1003. print(f"Saving {filepath}")
  1004. plt.savefig(filepath)
  1005. plt.close(fig)
  1006. # %% Statistics on prediction values
  1007. def add_catg_label(pred_const_data, catg_filters, model_type):
  1008. """
  1009. Adds a multiindex level for the 3 patient categories to separate IDs by
  1010. category easier, useful when doing groupby and statistics per category.
  1011. Parameters
  1012. ----------
  1013. pred_const_data : pandas.core.frame.DataFrame
  1014. catg_filters : dict
  1015. model_type : str
  1016. Returns
  1017. -------
  1018. pred_const_data : pandas.core.frame.DataFrame
  1019. Dataframe with the new midx level added.
  1020. """
  1021. # get tuples with patient category for each patient ID
  1022. midx_arr = [(p, c) for c, P in catg_filters.items() for p in P]
  1023. # create multiindex
  1024. midx = pd.MultiIndex.from_tuples(midx_arr, names = ["ID", "patient_catg"])
  1025. # remove index levels except for patient ID
  1026. unstack_labels = pred_const_data.index.names[:-1]
  1027. pred_const_data = pred_const_data.unstack(unstack_labels)
  1028. # replace index values, essentially adding the new layer
  1029. pred_const_data.index = midx
  1030. # return previous index levels
  1031. pred_const_data = pred_const_data.stack(unstack_labels)
  1032. # reorder index levels back to original order
  1033. idcs = pred_const_data.index.names
  1034. pred_const_data = pred_const_data.reorder_levels(idcs[2:] + idcs[0:2][::-1])
  1035. return pred_const_data
  1036. @filecheck_csv
  1037. def stats_on_constants(filepath, pred_const_data):
  1038. """
  1039. Groups patient data and applies a list of aggregate functions. Then adds
  1040. extra columns for standard deviation of a agg result divided by mean of
  1041. results in the group.
  1042. Parameters
  1043. ----------
  1044. filepath : list
  1045. pred_const_data : pandas.core.frame.DataFrame
  1046. Dataframe containing fitted constant values.
  1047. Returns
  1048. -------
  1049. pred_const_stats : pandas.core.frame.DataFrame
  1050. Dataframe containing a summary of fitted constant values.
  1051. """
  1052. methods = ["mean", "std", "sem"]
  1053. pred_const_stats = (pred_const_data.groupby(pred_const_data.index.names[:-1])
  1054. .agg(methods))
  1055. pred_const_stats.rename_axis(columns = ["constant","aggregate"], inplace = True)
  1056. for val in pred_const_stats.columns.unique(level = 0):
  1057. pred_const_stats[(val, "std/mean")] = (pred_const_stats[(val, "std")].divide(pred_const_stats[(val,"mean")])
  1058. .fillna(0))
  1059. # reorder columns
  1060. pretty_order_0 = pred_const_stats.columns.unique(level = 'constant')
  1061. pretty_order_1 = ["mean", "std", "sem", "std/mean"]
  1062. pred_const_stats = (pred_const_stats.reindex(columns = pretty_order_0,
  1063. level = "constant")
  1064. .reindex(columns = pretty_order_1,
  1065. level = "aggregate"))
  1066. pred_const_stats.to_csv(make_dir(*filepath))
  1067. return pred_const_stats
  1068. @filecheck_csv
  1069. def get_AIC(filepath, resids, model_type):
  1070. """
  1071. Calculate the Akaike information criterion using ΔAIC = 2*k + n*ln(RSS)
  1072. where RSS = residual sum of squares, n is the number of samples and k
  1073. number of parameters of the model.
  1074. Parameters
  1075. ----------
  1076. filepath : str
  1077. resids : pd.DataFrame
  1078. Array of residuals.
  1079. model_type : str
  1080. Returns
  1081. -------
  1082. resids_AIC : pd.DataFrame
  1083. """
  1084. #model_type : k
  1085. num_model_params = {"Intravascular": 1,
  1086. "Patlak": 2,
  1087. "2CUptake": 3,
  1088. "ExTofts": 3,
  1089. "2CExchange": 4,
  1090. "2CUptakeDisp": 4,
  1091. "2CUptakePlug": 5}
  1092. sum_list = []
  1093. for cutoff in resids.index.get_level_values("time_window").unique():
  1094. #sum up the residuals corresponding to a window -> Series
  1095. window_data_slice = resids.loc[cutoff].pow(2).sum(axis = 1)
  1096. df_slice = pd.DataFrame(window_data_slice, columns = ["sum"])
  1097. #add the number of elements with index value greater than cutoff
  1098. df_slice["num_elements"] = (resids.columns.values.astype(float) >= int(cutoff)).sum()
  1099. sum_list.append(df_slice)
  1100. #this WILL store AIC information, doesn't have it yet
  1101. resids_AIC = pd.concat(sum_list)
  1102. #return time_window index information
  1103. resids_AIC.index = resids.index
  1104. k = num_model_params[model_type]
  1105. #2 * number of parameters + number of data points
  1106. # https://www.sciencedirect.com/science/article/pii/S2468042719300508
  1107. resids_AIC["AIC"] = 2*k + resids_AIC["num_elements"]*np.log(resids_AIC["sum"]/resids_AIC["num_elements"])
  1108. resids_AIC.to_csv(make_dir(*filepath))
  1109. return resids_AIC
  1110. @filecheck_csv
  1111. def get_AICc(filepath, resids, model_type):
  1112. """
  1113. https://pmc.ncbi.nlm.nih.gov/articles/PMC3291742/
  1114. Calculate the Akaike information criterion with small sample correction using ΔAIC = 2*k + n*ln(RSS) + 2*k*(k+1)/(n-k-1)
  1115. where RSS = residual sum of squares, n is the number of samples and k
  1116. number of parameters of the model.
  1117. Parameters
  1118. ----------
  1119. filepath : str
  1120. resids : pd.DataFrame
  1121. Array of residuals.
  1122. model_type : str
  1123. Returns
  1124. -------
  1125. resids_AIC : pd.DataFrame
  1126. """
  1127. #model_type : k
  1128. num_model_params = {"Intravascular": 1,
  1129. "Patlak": 2,
  1130. "2CUptake": 3,
  1131. "ExTofts": 3,
  1132. "2CExchange": 4,
  1133. "2CUptakeDisp": 4,
  1134. "2CUptakePlug": 5}
  1135. sum_list = []
  1136. for cutoff in resids.index.get_level_values("time_window").unique():
  1137. #sum up the residuals corresponding to a window -> Series
  1138. window_data_slice = resids.loc[cutoff].pow(2).sum(axis = 1)
  1139. df_slice = pd.DataFrame(window_data_slice, columns = ["sum"])
  1140. #add the number of elements with index value greater than cutoff
  1141. df_slice["num_elements"] = (resids.columns.values.astype(float) >= int(cutoff)).sum()
  1142. sum_list.append(df_slice)
  1143. #this WILL store AIC information, doesn't have it yet
  1144. resids_AICc = pd.concat(sum_list)
  1145. #return time_window index information
  1146. resids_AICc.index = resids.index
  1147. k = num_model_params[model_type]
  1148. #2 * number of parameters + number of data points
  1149. # https://www.sciencedirect.com/science/article/pii/S2468042719300508
  1150. resids_AICc["AICc"] = 2*k + resids_AICc["num_elements"]*np.log(resids_AICc["sum"]/resids_AICc["num_elements"]) + 2*k*(k+1)/(resids_AICc["num_elements"]-k-1)
  1151. resids_AICc.to_csv(make_dir(*filepath))
  1152. return resids_AICc
  1153. def boxplot_AICc_wrap(AICcs, pred_const_data, descriptions, brain_locs, models,
  1154. undersampled):
  1155. AICcs_lst = copy.deepcopy(AICcs)
  1156. dpi = 96
  1157. res = (1920,1080)
  1158. uniq_cols = pred_const_data.index.get_level_values("time_window").unique()
  1159. #reorder so I get index with patient category labels with the same order as AICs
  1160. midx = (pred_const_data.copy()
  1161. .unstack("patient_catg")
  1162. .reindex(AICcs[0].index)
  1163. .stack("patient_catg")
  1164. ).index
  1165. for i in range(len(AICcs_lst)):
  1166. AICcs_lst[i].index = midx
  1167. data = pd.concat(AICcs_lst, keys = tuple(models)).unstack("time_window")
  1168. print(data)
  1169. temp = list(data.index.names)
  1170. temp[0] = "model"
  1171. data.index.names = temp
  1172. data = data.reorder_levels(["patient_catg","brain_loc","model","ID"])
  1173. s = "_undersampled" if undersampled else ""
  1174. dirpath = f"{os.getcwd()}/PvUgraphs&tables{s}/boxplots_AICc/"
  1175. models_in_order = pd.unique(data.index.get_level_values('model'))
  1176. # Convert only that level to a categorical with a frozen order
  1177. data.index = data.index.set_levels(
  1178. pd.CategoricalIndex(models_in_order, categories=models_in_order, ordered=True),
  1179. level='model'
  1180. )
  1181. for desc in descriptions:
  1182. for brain_loc in brain_locs:
  1183. fig, axs = plt.subplots(1, uniq_cols.size,
  1184. num = f'{desc} {brain_loc}',
  1185. sharey = "row",
  1186. figsize = (res[0]/dpi, res[1]/dpi),
  1187. dpi = dpi)
  1188. #the groupby inside boxplot sorts the data which is annoying
  1189. axs = data.loc[(desc, brain_loc)].boxplot(by = "model",
  1190. ax = axs,
  1191. rot = 45,
  1192. grid = False)
  1193. for j, ax in enumerate(axs):
  1194. plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")
  1195. ax.set_title(f"Window: {uniq_cols[j]}s")
  1196. ax.set(xlabel = "")
  1197. ax.grid(axis = "y")
  1198. for w, model in enumerate(models):
  1199. scat_data = data.loc[(desc, brain_loc, model), uniq_cols[j]]
  1200. x = np.random.normal(w+1, 0.05, len(scat_data))
  1201. ax.scatter(x = x,
  1202. y = scat_data,
  1203. alpha = 0.4)
  1204. axs[0].set_ylabel("AICc")
  1205. fig.suptitle(f"Akaike information criterion for small datasets for fits at different time windows in {brain_loc} for {desc}")
  1206. dirpath_d = f"{dirpath}{desc}/"
  1207. filepath = make_dir(dirpath_d) + f"AICc_model_comparison_{brain_loc}.png"
  1208. #only closes the figure when saving
  1209. #if not os.path.isfile(filepath):
  1210. print(f"Saving {filepath}")
  1211. plt.savefig(filepath)
  1212. plt.close(fig)
  1213. @filecheck_csv
  1214. def get_BIC(filepath, resids, model_type):
  1215. """
  1216. Calculate the Akaike information criterion using ΔAIC = 2*k + n*ln(RSS)
  1217. where RSS = residual sum of squares, n is the number of samples and k
  1218. number of parameters of the model.
  1219. Parameters
  1220. ----------
  1221. filepath : str
  1222. resids : pd.DataFrame
  1223. Array of residuals.
  1224. model_type : str
  1225. Returns
  1226. -------
  1227. resids_BIC : pd.DataFrame
  1228. """
  1229. #model_type : k
  1230. num_model_params = {"Intravascular": 1,
  1231. "Patlak": 2,
  1232. "2CUptake": 3,
  1233. "ExTofts": 3,
  1234. "2CExchange": 4,
  1235. "2CUptakeDisp": 4,
  1236. "2CUptakePlug": 5}
  1237. sum_list = []
  1238. for cutoff in resids.index.get_level_values("time_window").unique():
  1239. #sum up the residuals corresponding to a window -> Series
  1240. window_data_slice = resids.loc[cutoff].pow(2).sum(axis = 1)
  1241. df_slice = pd.DataFrame(window_data_slice, columns = ["sum"])
  1242. #add the number of elements with index value greater than cutoff
  1243. df_slice["num_elements"] = (resids.columns.values.astype(float) >= int(cutoff)).sum()
  1244. sum_list.append(df_slice)
  1245. #this WILL store AIC information, doesn't have it yet
  1246. resids_BIC = pd.concat(sum_list)
  1247. #return time_window index information
  1248. resids_BIC.index = resids.index
  1249. k = num_model_params[model_type]
  1250. #2 * number of parameters + number of data points
  1251. # https://www.sciencedirect.com/science/article/pii/S2468042719300508
  1252. resids_BIC["BIC"] = resids_BIC["num_elements"]*np.log(resids_BIC["sum"]/resids_BIC["num_elements"]) + k*np.log(resids_BIC["num_elements"])
  1253. resids_BIC.to_csv(make_dir(*filepath))
  1254. return resids_BIC
  1255. def boxplot_BIC_wrap(BICs, pred_const_data, descriptions, brain_locs, models,
  1256. undersampled):
  1257. BICs_lst = copy.deepcopy(BICs)
  1258. dpi = 96
  1259. res = (1920,1080)
  1260. uniq_cols = pred_const_data.index.get_level_values("time_window").unique()
  1261. #reorder so I get index with patient category labels with the same order as AICs
  1262. midx = (pred_const_data.copy()
  1263. .unstack("patient_catg")
  1264. .reindex(BICs[0].index)
  1265. .stack("patient_catg")
  1266. ).index
  1267. for i in range(len(BICs_lst)):
  1268. BICs_lst[i].index = midx
  1269. data = pd.concat(BICs_lst, keys = tuple(models)).unstack("time_window")
  1270. print(data)
  1271. temp = list(data.index.names)
  1272. temp[0] = "model"
  1273. data.index.names = temp
  1274. data = data.reorder_levels(["patient_catg","brain_loc","model","ID"])
  1275. s = "_undersampled" if undersampled else ""
  1276. dirpath = f"{os.getcwd()}/PvUgraphs&tables{s}/boxplots_BIC/"
  1277. models_in_order = pd.unique(data.index.get_level_values('model'))
  1278. # Convert only that level to a categorical with a frozen order
  1279. data.index = data.index.set_levels(
  1280. pd.CategoricalIndex(models_in_order, categories=models_in_order, ordered=True),
  1281. level='model'
  1282. )
  1283. for desc in descriptions:
  1284. for brain_loc in brain_locs:
  1285. fig, axs = plt.subplots(1, uniq_cols.size,
  1286. num = f'{desc} {brain_loc}',
  1287. sharey = "row",
  1288. figsize = (res[0]/dpi, res[1]/dpi),
  1289. dpi = dpi)
  1290. #the groupby inside boxplot sorts the data which is annoying
  1291. axs = data.loc[(desc, brain_loc)].boxplot(by = "model",
  1292. ax = axs,
  1293. rot = 45,
  1294. grid = False)
  1295. for j, ax in enumerate(axs):
  1296. plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")
  1297. ax.set_title(f"Window: {uniq_cols[j]}s")
  1298. ax.set(xlabel = "")
  1299. ax.grid(axis = "y")
  1300. for w, model in enumerate(models):
  1301. scat_data = data.loc[(desc, brain_loc, model), uniq_cols[j]]
  1302. x = np.random.normal(w+1, 0.05, len(scat_data))
  1303. ax.scatter(x = x,
  1304. y = scat_data,
  1305. alpha = 0.4)
  1306. axs[0].set_ylabel("BIC")
  1307. fig.suptitle(f"Bayesian information criterion for fits at different time windows in {brain_loc} for {desc}")
  1308. dirpath_d = f"{dirpath}{desc}/"
  1309. filepath = make_dir(dirpath_d) + f"BIC_model_comparison_{brain_loc}.png"
  1310. #only closes the figure when saving
  1311. #if not os.path.isfile(filepath):
  1312. print(f"Saving {filepath}")
  1313. plt.savefig(filepath)
  1314. plt.close(fig)

functions.py at commit d7989f3, no license · at the source

Overview

Authors: Martin Kozár1,2, Ioana‐Emilia Mosneag1,2, Sarah Al‐Bachari3, Igor Chernyavsky4,5, Geoff J M Parker6,7, Hervé Boutin2,8,9, Laura M Parkes2,10, Ingo Schiessl2,9, Ben R Dickie1,2,11
  1. Division of Imaging, Informatics and Data Sciences, School of Health Sciences, Faculty of Biology, Medicine and Health, University of Manchester, Manchester, UK
  2. Geoffrey Jefferson Brain Research Centre, Faculty of Biology, Medicine and Health, University of Manchester, Manchester Academic Health Science Centre, Manchester, UK
  3. Department of Clinical and Movement Neurosciences, University College London, London, UK
  4. Department of Mathematics, School of Natural Sciences, Faculty of Science and Engineering, University of Manchester, Manchester, UK
  5. Maternal and Fetal Health Research Centre, School of Medical Sciences, University of Manchester, Manchester, UK
  6. Quantitative Imaging Group, UCL Hawkes Institute, Department of Medical Physics & Biomedical Engineering, University College London, London, UK
  7. Bioxydyn Limited, Manchester, UK
  8. Imaging Brain and Neuropsychiatry iBraiN 1253, Université de Tours, Inserm, Bat Planiol, UFR de Médecine, Tours, France
  9. Division of Neuroscience, School of Biological Sciences, Faculty of Biology, Medicine and Health, Manchester Academic Health Science Centre, The University of Manchester, Manchester, UK
  10. Division of Psychology, Communication and Human Neuroscience, School of Health Sciences, Faculty of Biology, Medicine and Health, University of Manchester, Manchester, UK
  11. Division of Pharmacy and Optometry, School of Health Sciences, Faculty of Biology, Medicine and Health, University of Manchester, Manchester, UK
Institutions: Manchester Academic Health Science Centre (United Kingdom); University of Manchester (United Kingdom); University College London (United Kingdom); Université de Tours (France); Inserm (France); Imaging, Brain, and Neuropsychiatry (France)
Journal: Magnetic resonance in medicine, volume 96, issue 5, pages 2309-2320
Dates: received 23 July 2025; accepted 2 July 2026; published online 18 July 2026; in print November 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1002/mrm.70514 · PMID 42470230 · PMCID PMC13527286 · OpenAlex W7169658861
Open access: hybrid, a free copy (OpenAlex)
Status: code verified
Categories: structural MRI / diffusion (modality), human (organism), mouse (organism), stroke (population), Parkinson's (population)
Methods: Connectivity, Statistics, fMRI & imaging
MeSH: Blood-Brain Barrier*, Brain*, Contrast Media*, Dynamic Contrast Enhanced Magnetic Resonance Imaging*, Magnetic Resonance Imaging*, Parkinson Disease*, Animals, Feasibility Studies, Female, Humans, Male, Metabolic Clearance Rate, Mice, Reproducibility of Results, Stroke (* major topic)
Topic: MRI in cancer diagnosis (Radiology, Nuclear Medicine and Imaging, Medicine), according to OpenAlex
Funding: Medical Research Council DTP Studentship: MR/N013751/1; Sydney Driscoll Neuroscience Foundation and the Engineering and Physical Sciences Research Council (EP/M005909/1); Medical Research Council
Citations: cited by 1 paper (Europe PMC); 61 references in the paper

Abstract

Purpose: To determine the feasibility of measuring brain clearance of gadolinium contrast agent using standard intravenous DCE‐MRI acquisitions and evaluate the contribution of blood–brain barrier (BBB) and non‐BBB clearance routes.

Methods: Uptake and extended Tofts models were fit to DCE‐MRI data from people with Parkinson's disease, post‐stroke and controls. Models were compared using the Akaike information criterion. Key parameters (K trans, v p, v e, and k) were extracted from gray and white matter ROIs. In mice, two‐photon microscopy of intravenously injected Sulforhodamine 101 was used to confirm small tracer clearance kinetics without the confounds of partial volume effects or water exchange.

Results: The extended Tofts model provided a superior fit compared to all uptake‐only models. Extended Tofts estimates of the extravascular extracellular volume fraction v e were underestimated by a factor of 10–20 compared to known literature values (1.5% vs. 20%–30%). We hypothesized this bias may be due to an additional competing non‐BBB clearance mechanism not accounted for by the extended Tofts model. In both MRI and two‐photon data, we found that v e estimates trended toward literature values as BBB permeability (and thus BBB clearance) increased, supporting our hypothesis.

Conclusion: Modeling clearance of extravasated contrast agent from the brain improves fit quality compared to models that neglect clearance. Estimates of interstitial volume fraction were underestimated at low K trans but tended toward literature values as BBB permeability (and clearance) increased. This study indicates that estimating brain clearance of contrast agent using DCE‐MRI is feasible, and that estimates reflect a combination of BBB and non‐BBB clearance pathways.

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

nimo-group/DCE-MRI-brain-clearance-2025

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: d7989f3228dd041cd6f599d1fbeba779535e513c, 15 December 2025
Languages: Python (5)
Size: 293 files, 5 scripts
Software Heritage: not archived
Found in: “Data Availability Statement”
Holds: README, environment (pyproject.toml, uv.lock)
Not found: license file, CITATION.cff, tests, continuous integration, documentation
Tools: Matplotlib (4 files), NumPy (4 files), pandas (4 files), SciPy (2 files), seaborn (2 files)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
6 files

The paper's code and data availability statement is in the Data section.

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;
  • 2 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 Statement

Raw data used within this study are available upon reasonable request from the corresponding author. Derived data and analysis scripts can be found on https://github.com/nimo‐group/DCE‐MRI‐brain‐clearance‐2025 (https://github.com/nimo-group/DCE-MRI-brain-clearance-2025).

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 → Wiley

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 9 authors, 15 MeSH terms, 3 funders, 61 references.

Cite

This paper

Kozár, M., Mosneag, I., Al‐Bachari, S., Chernyavsky, I., Parker, G. J. M., Boutin, H., Parkes, L. M., Schiessl, I., & Dickie, B. R. (2026). Brain Clearance of Contrast Agent in Intravenous DCE-MRI Is Measurable and Cannot Be Explained by Clearance Across the BBB Alone. Magnetic resonance in medicine, 96(5), 2309-2320. https://doi.org/10.1002/mrm.70514

BibTeX

@article{kozar2026brain,
author = {Kozár, Martin and Mosneag, Ioana‐Emilia and Al‐Bachari, Sarah and Chernyavsky, Igor and Parker, Geoff J M and Boutin, Hervé and Parkes, Laura M and Schiessl, Ingo and Dickie, Ben R},
title = {{Brain Clearance of Contrast Agent in Intravenous DCE-MRI Is Measurable and Cannot Be Explained by Clearance Across the BBB Alone}},
journal = {Magnetic resonance in medicine},
year = {2026},
month = jul,
volume = {96},
number = {5},
pages = {2309--2320},
publisher = {Wiley},
issn = {0740-3194},
doi = {10.1002/mrm.70514},
url = {https://doi.org/10.1002/mrm.70514},
pmid = {42470230},
pmcid = {PMC13527286}
}

RIS

TY - JOUR
AU - Kozár, Martin
AU - Mosneag, Ioana‐Emilia
AU - Al‐Bachari, Sarah
AU - Chernyavsky, Igor
AU - Parker, Geoff J M
AU - Boutin, Hervé
AU - Parkes, Laura M
AU - Schiessl, Ingo
AU - Dickie, Ben R
TI - Brain Clearance of Contrast Agent in Intravenous DCE-MRI Is Measurable and Cannot Be Explained by Clearance Across the BBB Alone
T2 - Magnetic resonance in medicine
J2 - Magn Reson Med
PY - 2026
DA - 2026/07/18
VL - 96
IS - 5
SP - 2309
EP - 2320
SN - 0740-3194
PB - Wiley
DO - 10.1002/mrm.70514
UR - https://doi.org/10.1002/mrm.70514
LA - en
ER -

CSL-JSON

{
"id": "10.1002/mrm.70514",
"type": "article-journal",
"title": "Brain Clearance of Contrast Agent in Intravenous DCE-MRI Is Measurable and Cannot Be Explained by Clearance Across the BBB Alone",
"container-title": "Magnetic resonance in medicine",
"author": [
{
"family": "Kozár",
"given": "Martin"
},
{
"family": "Mosneag",
"given": "Ioana‐Emilia"
},
{
"family": "Al‐Bachari",
"given": "Sarah"
},
{
"family": "Chernyavsky",
"given": "Igor"
},
{
"family": "Parker",
"given": "Geoff J M"
},
{
"family": "Boutin",
"given": "Hervé"
},
{
"family": "Parkes",
"given": "Laura M"
},
{
"family": "Schiessl",
"given": "Ingo"
},
{
"family": "Dickie",
"given": "Ben R"
}
],
"container-title-short": "Magn Reson Med",
"volume": "96",
"issue": "5",
"page": "2309-2320",
"DOI": "10.1002/mrm.70514",
"PMID": "42470230",
"PMCID": "PMC13527286",
"ISSN": "0740-3194",
"publisher": "Wiley",
"URL": "https://doi.org/10.1002/mrm.70514",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
18
]
]
}
}

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.1002/nbm.70277 [code]
Hierarchical Bayesian Modelling Improves Microstructural Parameter Mapping in Diffusion and Exchange MRI Data.
Journal: NMR in biomedicine
In common: seaborn, SciPy, Matplotlib, 1 other tool, structural MRI / diffusion, 2 references, 2 authors
[2] doi:10.1073/pnas.2516601123 [code]
Unveiling the glymphatic system's role in brain aging: A comprehensive biomarker and modifiable intervention target.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: seaborn, pandas, SciPy, 2 other tools, structural MRI / diffusion, 5 references
[3] doi:10.1126/sciadv.aeb0404 [code]
MR-AIV reveals in vivo brain-wide fluid flow with physics-informed AI.
Journal: Science advances
In common: pandas, SciPy, Matplotlib, 1 other tool, structural MRI / diffusion, 4 references
[4] doi:10.1073/pnas.2526239123 [code]
Quantitative assessment of flow between cerebrospinal and interstitial fluid compartments in humans.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: structural MRI / diffusion, 5 references
[5] doi:10.1002/alz.71745 [code]
Regional astrocyte dysregulation and altered glymphatic-related markers in Alzheimer's disease frontal cortex.
Journal: Alzheimer's & dementia : the journal of the Alzheimer's Association
In common: seaborn, pandas, SciPy, 2 other tools, 3 references
[6] doi:10.3389/fpsyt.2026.1848053
Neuroanatomical substrates of perivascular space index: a morphometric and tractography study.
Journal: Frontiers in psychiatry
In common: structural MRI / diffusion, 4 references
[7] doi:10.1038/s41467-026-76306-9 [code]
Non-invasive characterization of perivascular subarachnoid spaces.
Journal: Nature communications
In common: SciPy, Matplotlib, NumPy, structural MRI / diffusion, 3 references
[8] doi:10.1177/0271678x261455452 [code]
Meningeal CSF transport varies across parasagittal dura subregions with age in humans.
Journal: Journal of cerebral blood flow and metabolism : official journal of the International Society of Cerebral Blood Flow and Metabolism
In common: structural MRI / diffusion, 4 references
[9] doi:10.1038/s41593-026-02358-1 [code]
Cerebral venous blood flow regulates intracerebral pressure and brain clearance via meningeal lymphatic vessels.
Journal: Nature neuroscience
In common: SciPy, Matplotlib, NumPy, stroke, structural MRI / diffusion, mouse, 2 references
[10] doi:10.1093/brain/awaf432
Global network and local vulnerabilities underlie brain atrophy across Parkinson's disease stages.
Journal: Brain : a journal of neurology
In common: Parkinson's, structural MRI / diffusion, author Laura M Parkes

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.