OSCR

Long-term editing of brain circuits using an engineered electrical synapse.

Code ↔ Paper

13 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 13 matches · 4 of them tie a paragraph to a whole file, not to given lines: weak matches, whose lines are not tinted
  1. [1] § Methods › Automated FETCH output processing pipeline ↔ FETCH.py, lines 270–279 · score 0.75 · optimal bandwidth, kernel density, cross validation, fit, FETCH
  2. [2] § Methods › Quantifying MD single-unit responses to direct IL activation ↔ LinCx_Opto_Stim_mkcg.m, the whole file · a weak match · score 0.69 · Laser Power, light pulses, intervals, optical, width, stimulation
  3. [3] § Methods › Neural data acquisition and analysis in IL→MD circuit-edited mice ↔ OptoLinCx_Analysis_v2.ipynb, lines 232–298 · score 0.67 · pre stimulus dominant, outliers, voltage, window, peaks, mice
  4. [4] § Methods › Quantifying MD single-unit responses to direct IL activation ↔ LinCx_Opto_Stim.m, the whole file · a weak match · score 0.66 · Laser Power, light pulses, intervals, optical, stimulation, analog
  5. [5] § Methods › Automated FETCH output processing pipeline ↔ FETCH.py, lines 446–497 · score 0.64 · FETCH score, Q2, Q4, Q1, Q3, cells
  6. [6] § Cx34.7(M1)–Cx35(M1) potentiates a long-range circuit ↔ OptoLinCx_Analysis_v2.ipynb, lines 1499–1573 · score 0.62 · medial dorsal thalamus, infralimbic cortex, Cx34.7, MD, Cx35, IL
  7. [7] § Methods › Automated FETCH output processing pipeline ↔ FETCH.py, lines 500–554 · score 0.62 · mApple, FITC, PE, SSC, fcs, FSC
  8. [8] § Methods › Neural data acquisition and analysis in IL→MD circuit-edited mice ↔ OptoLinCx_Analysis.ipynb, lines 194–216 · score 0.60 · pre stimulus dominant, window, peaks, filtered, channel
  9. [9] § Methods › Neural data acquisition and analysis in IL→MD circuit-edited mice ↔ LinCx_Opto_Stim_mkcg.m, the whole file · a weak match · score 0.57 · baseline recording, intensities, pulse, intervals, laser, stimulated
  10. [10] § Cx34.7(M1)–Cx35(M1) potentiates a long-range circuit ↔ OptoLinCx_Analysis_v2.ipynb, lines 1354–1497 · score 0.54 · MD channel, IL channel, Cx34.7, box, width, GFP
  11. [11] § Methods › Automated FETCH output processing pipeline ↔ FETCH.py, lines 282–326 · score 0.53 · kernel density, Gaussian, bandwidth, fitted, gate, FETCH
  12. [12] § Methods › Characterizing gap junction biophysical properties using Xenopus oocytes ↔ OptoLinCx_Analysis_v2.ipynb, lines 1057–1067 · score 0.51 · Cx34.7M1, Cx35M1
  13. [13] § Methods › Neural data acquisition and analysis in IL→MD circuit-edited mice ↔ LinCx_Opto_Stim.m, the whole file · a weak match · score 0.51 · baseline recording, pulse, intervals, laser, stimulated, analog

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 · 841 lines · 40 KB · no license · 4 matches

  1. #e.g. $python FETCH.py -f example -p my_cool_project -s FC114_A2_A02_002.fcs FC114_C1_C01_025.fcs
  2. import argparse
  3. import matplotlib
  4. import matplotlib.pyplot as plt
  5. from matplotlib.pyplot import figure
  6. import scipy
  7. import warnings
  8. import random
  9. def _centered(arr, newsize):
  10. # Return the center newsize portion of the array.
  11. newsize = np.asarray(newsize)
  12. currsize = np.array(arr.shape)
  13. startind = (currsize - newsize) // 2
  14. endind = startind + newsize
  15. myslice = [slice(startind[k], endind[k]) for k in range(len(endind))]
  16. return arr[tuple(myslice)]
  17. scipy.signal.signaltools._centered = _centered
  18. import flowkit as fk
  19. import numpy as np
  20. from numpy.linalg import norm
  21. import scipy.stats as st
  22. from os.path import join
  23. import os
  24. #these are still libraries
  25. from sklearn.neighbors import KernelDensity
  26. from sklearn.model_selection import GridSearchCV
  27. from numpy.linalg import eig, inv
  28. from matplotlib.patches import Ellipse
  29. from matplotlib.path import Path
  30. from sklearn.model_selection import GridSearchCV
  31. from matplotlib.offsetbox import AnchoredText
  32. from matplotlib import path
  33. import pandas as pd
  34. import seaborn as sns
  35. plt.switch_backend('agg')
  36. plt.ioff()
  37. #plot formatting
  38. #def = define function; in this case... allows argument input into terminal..
  39. def parse_arguments():
  40. ap = argparse.ArgumentParser(description="Parse arguments")
  41. ap.add_argument(
  42. "-f", "--folder",
  43. default="",
  44. help="The path to a directory containing your .fcs files",
  45. type=str )
  46. ap.add_argument(
  47. "-p", "--project",
  48. default="Untitled_project",
  49. help="The project name",
  50. type=str )
  51. ap.add_argument(
  52. "-s", "--skip_renaming",
  53. default='',
  54. help="This script expects fcs files to be formatted as 'FC114_A1_A01_001.fcs'; if just a few files are not formatted like that, pass them as arguments in this function to avoid errors; otherwise edit summarize function too parse your filenames correctly", nargs='*')
  55. ap.add_argument(
  56. "-l", "--legacy_analysis",
  57. default="False",
  58. help ="An optional argument that sets a more narrow first gate and quantile cutoff in the last gate for replicability of past analysis",
  59. type=str )
  60. ap.add_argument(
  61. "-n", "--negative_control",
  62. default="None",
  63. help ="An optional argument: negative control file name w/o fcs; draws the third gate against a known negative control instead of automatic gating",
  64. type=str )
  65. ap.add_argument(
  66. "-pn", "--predefined_negative_control",
  67. default="None",
  68. help ="An optional argument: can specify x axis and y axis precise values to draw the third gate in the format e.g.: -pn 100,240",
  69. type=str )
  70. ap.add_argument(
  71. "-g", "--log_gate",
  72. default="None",
  73. help ="An optional argument: one uses log transform on the first gate for the unorthodox FETCH template",
  74. type=str )
  75. return ap.parse_args()
  76. #this function writes gatefiles that can be opened in flowjo specifically
  77. def gate_writer(vertices1, vertices2, boundaries, filename, channame1=None, channame2=None, fluorophore1=None, fluorophore2=None):
  78. gate_text = ['<?xml version="1.0" encoding="UTF-8"?>',
  79. '<gating:Gating-ML',
  80. ' xmlns:gating="http://www.isac-net.org/std/Gating-ML/v2.0/gating"',
  81. ' xmlns:data-type="http://www.isac-net.org/std/Gating-ML/v2.0/datatypes">']
  82. polygon1 = [' <gating:PolygonGate gating:id="Polygon1">',
  83. ' <gating:dimension gating:compensation-ref="uncompensated">',
  84. ' <data-type:fcs-dimension data-type:name="SSC-A" />',
  85. ' </gating:dimension>',
  86. ' <gating:dimension gating:compensation-ref="uncompensated">',
  87. ' <data-type:fcs-dimension data-type:name="FSC-A" />',
  88. ' </gating:dimension>']
  89. for vertex in vertices1:
  90. vertex = [' <gating:vertex>',
  91. ' <gating:coordinate data-type:value="' + str(vertex[0]) + '" />',
  92. ' <gating:coordinate data-type:value="' + str(vertex[1]) + '" />',
  93. ' </gating:vertex>']
  94. polygon1 = polygon1 + vertex
  95. polygon1 = polygon1 + [' </gating:PolygonGate>']
  96. if vertices2 is not None:
  97. polygon2 = [' <gating:PolygonGate gating:id="Polygon2">',
  98. ' <gating:dimension gating:compensation-ref="uncompensated">',
  99. ' <data-type:fcs-dimension data-type:name="FSC-A" />',
  100. ' </gating:dimension>',
  101. ' <gating:dimension gating:compensation-ref="uncompensated">',
  102. ' <data-type:fcs-dimension data-type:name="FSC-H" />',
  103. ' </gating:dimension>']
  104. for vertex in vertices2:
  105. vertex = [' <gating:vertex>',
  106. ' <gating:coordinate data-type:value="' + str(vertex[0]) + '" />',
  107. ' <gating:coordinate data-type:value="' + str(vertex[1]) + '" />',
  108. ' </gating:vertex>']
  109. polygon2 = polygon2 + vertex
  110. polygon2 = polygon2 + [' </gating:PolygonGate>']
  111. and_gate = [' <gating:BooleanGate gating:id="And1">',
  112. ' <data-type:custom_info>',
  113. ' Only keep results satisfying both Polygon gates',
  114. ' </data-type:custom_info>',
  115. ' <gating:and>',
  116. ' <gating:gateReference gating:ref="Polygon1" />',
  117. ' <gating:gateReference gating:ref="Polygon2" />',
  118. ' </gating:and>',
  119. ' </gating:BooleanGate>']
  120. if boundaries is not None:
  121. quadrants = [' <gating:QuadrantGate gating:id="Quadrant1" gating:parent_id="And1">',
  122. ' <gating:divider gating:id="A" gating:compensation-ref="uncompensated">',
  123. ' <data-type:fcs-dimension data-type:name="' + channame1 + '" />',
  124. ' <gating:value>' + str(boundaries[1]) + '</gating:value>',
  125. ' </gating:divider>',
  126. ' <gating:divider gating:id="B" gating:compensation-ref="uncompensated">',
  127. ' <data-type:fcs-dimension data-type:name="' + channame2 + '" />',
  128. ' <gating:value>' + str(boundaries[0]) + '</gating:value>',
  129. ' </gating:divider>',
  130. ' <gating:Quadrant gating:id="Untransfected">',
  131. ' <gating:position gating:divider_ref="A" gating:location="' + str(boundaries[1] - 1) + '" />',
  132. ' <gating:position gating:divider_ref="B" gating:location="' + str(boundaries[0] - 1) + '" />',
  133. ' </gating:Quadrant>',
  134. ' <gating:Quadrant gating:id="Double-Positive">',
  135. ' <gating:position gating:divider_ref="A" gating:location="' + str(boundaries[1] + 1) + '" />',
  136. ' <gating:position gating:divider_ref="B" gating:location="' + str(boundaries[0] + 1) + '" />',
  137. ' </gating:Quadrant>',
  138. ' <gating:Quadrant gating:id="fluorophorea">',
  139. ' <gating:position gating:divider_ref="A" gating:location="' + str(boundaries[1] + 1) + '" />',
  140. ' <gating:position gating:divider_ref="B" gating:location="' + str(boundaries[0] - 1) + '" />',
  141. ' </gating:Quadrant>',
  142. ' <gating:Quadrant gating:id="fluorophoreb">',
  143. ' <gating:position gating:divider_ref="A" gating:location="' + str(boundaries[1] - 1) + '" />',
  144. ' <gating:position gating:divider_ref="B" gating:location="' + str(boundaries[0] + 1) + '" />',
  145. ' </gating:Quadrant>',
  146. ' </gating:QuadrantGate>']
  147. second_and_gate = [' <gating:BooleanGate gating:id="And2Untransfected">',
  148. ' <data-type:custom_info>',
  149. ' Only keep results satisfying both Polygon gates and Untransfected',
  150. ' </data-type:custom_info>',
  151. ' <gating:and>',
  152. ' <gating:gateReference gating:ref="And1" />',
  153. ' <gating:gateReference gating:ref="Untransfected" />',
  154. ' </gating:and>',
  155. ' </gating:BooleanGate>']
  156. third_and_gate = [' <gating:BooleanGate gating:id="And3DoublePositive">',
  157. ' <data-type:custom_info>',
  158. ' Only keep results satisfying both Polygon gates and Double-Positive',
  159. ' </data-type:custom_info>',
  160. ' <gating:and>',
  161. ' <gating:gateReference gating:ref="And1" />',
  162. ' <gating:gateReference gating:ref="Double-Positive" />',
  163. ' </gating:and>',
  164. ' </gating:BooleanGate>']
  165. fourth_and_gate = [' <gating:BooleanGate gating:id="And4fluorophorea">',
  166. ' <data-type:custom_info>',
  167. ' Only keep results satisfying both Polygon gates and fluorophorea',
  168. ' </data-type:custom_info>',
  169. ' <gating:and>',
  170. ' <gating:gateReference gating:ref="And1" />',
  171. ' <gating:gateReference gating:ref="fluorophorea" />',
  172. ' </gating:and>',
  173. ' </gating:BooleanGate>']
  174. fifth_and_gate = [' <gating:BooleanGate gating:id="And5fluorophoreb">',
  175. ' <data-type:custom_info>',
  176. ' Only keep results satisfying both Polygon gates and fluorophoreb',
  177. ' </data-type:custom_info>',
  178. ' <gating:and>',
  179. ' <gating:gateReference gating:ref="And1" />',
  180. ' <gating:gateReference gating:ref="fluorophoreb" />',
  181. ' </gating:and>',
  182. ' </gating:BooleanGate>']
  183. gate_text = gate_text + polygon1 + polygon2 + and_gate + quadrants + \
  184. second_and_gate + third_and_gate + fourth_and_gate + fifth_and_gate + ['</gating:Gating-ML>']
  185. else:
  186. gate_text = gate_text + polygon1 + polygon2 + and_gate + ['</gating:Gating-ML>']
  187. else:
  188. gate_text = gate_text + polygon1 + ['</gating:Gating-ML>']
  189. with open(filename, "w") as f:
  190. for line in gate_text:
  191. if line not in ['\n', '\r\n']:
  192. f.write("%s\n" % line)
  193. #selection for the first gate
  194. def fitEllipse(x,y):
  195. x = x[:,np.newaxis]
  196. y = y[:,np.newaxis]
  197. D = np.hstack((x*x, x*y, y*y, x, y, np.ones_like(x)))
  198. S = np.dot(D.T,D)
  199. C = np.zeros([6,6])
  200. C[0,2] = C[2,0] = 2; C[1,1] = -1
  201. E, V = eig(np.dot(inv(S), C))
  202. n = np.argmax(np.abs(E))
  203. a = V[:,n]
  204. b,c,d,f,g,a=a[1]/2., a[2], a[3]/2., a[4]/2., a[5], a[0]
  205. num=b*b-a*c
  206. cx=(c*d-b*f)/num
  207. cy=(a*f-b*d)/num
  208. angle=0.5*np.arctan(2*b/(a-c))*180/np.pi
  209. up = 2*(a*f*f+c*d*d+g*b*b-2*b*d*f-a*c*g)
  210. down1=(b*b-a*c)*( (c-a)*np.sqrt(1+4*b*b/((a-c)*(a-c)))-(c+a))
  211. down2=(b*b-a*c)*( (a-c)*np.sqrt(1+4*b*b/((a-c)*(a-c)))-(c+a))
  212. a=np.sqrt(abs(up/down1))
  213. b=np.sqrt(abs(up/down2))
  214. ell=Ellipse((cx,cy),a*2.,b*2.,angle=angle)
  215. ell_coord=ell.get_verts()
  216. return [ell_coord, cx, cy, a*2, b*2, angle]
  217. #kde = kernel density estimation; matching density to color
  218. def make_kde(points):
  219. x = points[:, 0]
  220. y = points[:, 1]
  221. # Define the borders
  222. deltaX = (max(x) - min(x))/1000
  223. deltaY = (max(y) - min(y))/1000
  224. xmin = min(x) - deltaX
  225. xmax = max(x) + deltaX
  226. ymin = min(y) - deltaY
  227. ymax = max(y) + deltaY
  228. # Create meshgrid
  229. xx, yy = np.mgrid[xmin:xmax:50j, ymin:ymax:50j]
  230. positions = np.vstack([xx.ravel(), yy.ravel()])
  231. values = np.vstack([x, y])
  232. kernel = st.gaussian_kde(values)
  233. f = np.reshape(kernel(positions).T, xx.shape)
  234. fig = plt.figure(figsize=(16,16))
  235. ax = fig.gca()
  236. cset1 = ax.contour(xx, yy, f, levels=50, colors='k')
  237. figure_centre = [(xmin + xmax)/2, (ymin + ymax)/2]
  238. plt.cla()
  239. plt.clf()
  240. return [cset1.allsegs, figure_centre, x, y]
  241. #similar to above
  242. def getKernelDensityEstimation(values, x, bandwidth = 0.2, kernel = 'gaussian'):
  243. model = KernelDensity(kernel = kernel, bandwidth=bandwidth)
  244. model.fit(values[:, np.newaxis])
  245. log_density = model.score_samples(x[:, np.newaxis])
  246. return np.exp(log_density)
  247. #a helper function for kde color matching
  248. def bestBandwidth(data, minBandwidth = 0.1, maxBandwidth = 2, nb_bandwidths = 30, cv = 30):
  249. """
  250. Run a cross validation grid search to identify the optimal bandwidth for the kernel density
  251. estimation.
  252. """
  253. model = GridSearchCV(KernelDensity(),
  254. {'bandwidth': np.linspace(minBandwidth, maxBandwidth, nb_bandwidths)}, cv=cv, n_jobs=-1)
  255. model.fit(data)
  256. return model.best_params_['bandwidth']
  257. #possibly for defining the last gate
  258. def z(samplename, Z, a, vertices1, vertices2, dest, sample, fluorophore1, fluorophore2, channame1, channame2, neg_cntrl, leg_g1):
  259. fluorophores = fluorophore1 + '_' + fluorophore2
  260. filename = join(dest, 'gates.xml')
  261. d = Z[a]
  262. #Take a pseudorandom subsample of d if it is > 10000 points:
  263. # if d.shape[0] > 10000:
  264. # random.seed(42)
  265. # print(d.shape)
  266. # rand_idx = random.sample(range(d.shape[0]), k = 10000)
  267. # d = d[rand_idx, :]
  268. if len(d) == 0:
  269. warnings.warn("No sample for last gate")
  270. return [samplename, 0, None, 0, [0, 0], [0, 0, 0, 0]]
  271. new_Z = np.array([d[:, 0] + abs(min(d[:, 0])) + 1, d[:, 1] + abs(min(d[:, 1])) + 1])
  272. new_Z= new_Z.T
  273. Z_log = np.log(new_Z)
  274. try:
  275. [alls, figure_centre, x, y] = make_kde(Z_log)
  276. except ValueError:
  277. warnings.warn("Can't make kde")
  278. return [samplename, 0, None, 0, [0, 0], [0, 0, 0, 0]]
  279. max_area = 0
  280. best_top_point = None
  281. best_right_point = None
  282. xy = np.vstack([x,y])
  283. cv_bandwidth = bestBandwidth(xy.T)
  284. kde_model = KernelDensity(kernel='gaussian', bandwidth=cv_bandwidth).fit(xy.T)
  285. kde = np.exp(kde_model.score_samples(xy.T))
  286. idx = kde.argsort()
  287. candidates = []
  288. #These are variables for the negative control gating
  289. nc_top_point = 0
  290. nc_right_point = 0
  291. if neg_cntrl[0] != "None" and samplename.rsplit('.fcs')[0] != neg_cntrl[0]:
  292. best_top_point, best_right_point = neg_cntrl[1]
  293. else:
  294. for j in range(len(alls)):
  295. for ii, seg in enumerate(alls[j]):
  296. #To find the best points for negative control, identify the rightmost and topmost points on a contour
  297. if neg_cntrl[0] != "None" and samplename.rsplit('.fcs')[0] == neg_cntrl[0]:
  298. if seg[0][0] == seg[-1][0] and seg[0][1] == seg[-1][1]:
  299. top_point_log = max(seg[:,1])
  300. right_point_log = max(seg[:,0])
  301. top_point = np.exp(top_point_log) - abs(min(d[:, 1])) - 1
  302. right_point = np.exp(right_point_log) - abs(min(d[:, 0])) - 1
  303. if top_point > nc_top_point:
  304. nc_top_point = top_point
  305. if right_point > nc_right_point:
  306. nc_right_point = right_point
  307. else:
  308. #The following applies to FETCH, isn't relevant for negative control
  309. p = Path(seg) # make a polygon
  310. grid = p.contains_points(xy.T)
  311. mean_kde = np.mean(kde[grid])
  312. top_point_log = max(seg[:,1])
  313. right_point_log = max(seg[:,0])
  314. top_point = np.exp(top_point_log) - abs(min(d[:, 1])) - 1
  315. right_point = np.exp(right_point_log) - abs(min(d[:, 0])) - 1
  316. if leg_g1:
  317. transfected_cells_x = right_point >= 500
  318. transfected_cells_y = top_point >= 500
  319. else:
  320. transfected_cells_x = right_point >= 1100
  321. transfected_cells_y = top_point >= 1100
  322. plt.plot(seg[:,0], seg[:,1], '.-')
  323. if transfected_cells_x or transfected_cells_y:
  324. continue
  325. area = 0.5*np.abs(np.dot(seg[:,0],np.roll(seg[:,1],1))-np.dot(seg[:,1],np.roll(seg[:,0],1)))
  326. if area > max_area:
  327. candidates.append([area, kde[grid], top_point, right_point])
  328. if neg_cntrl[0] == "None":
  329. largest_cand = [0]
  330. if len(candidates) == 0:
  331. warnings.warn("Pipeline error on this file")
  332. return [samplename, 0, None, 0, [0, 0], [0, 0, 0, 0]]
  333. for cand in candidates:
  334. if cand[0] > largest_cand[0]:
  335. largest_cand = cand
  336. h_plt = plt.hist(largest_cand[1], bins=100)
  337. bin_count = h_plt[0]
  338. cutoff = h_plt[1]
  339. #this if/else is not currently used, but could be used to make the last gate more stringent
  340. if leg_g1:
  341. quantile_cutoff_val = 0.60
  342. else:
  343. quantile_cutoff_val = 0.30
  344. quantile_cutoff = np.quantile(cutoff, quantile_cutoff_val)
  345. for cand in candidates:
  346. if np.mean(cand[1]) < quantile_cutoff:
  347. continue
  348. if cand[0] > max_area:
  349. max_area = cand[0]
  350. best_top_point = cand[2]
  351. best_right_point = cand[3]
  352. #Plot kde lines on the log-transformed data for debugging:
  353. plt.figure(num=None, figsize=(16, 16), dpi=80, facecolor='w', edgecolor='k')
  354. for j in range(len(alls)):
  355. for ii, seg in enumerate(alls[j]):
  356. plt.plot(seg[:,0], seg[:,1], '.-')
  357. plt.scatter(Z_log[:, 0], Z_log[:, 1], s=12.5)
  358. if best_top_point != None:
  359. best_log_top = np.log(best_top_point + abs(min(d[:, 1])) + 1)
  360. best_log_right = np.log(best_right_point + abs(min(d[:, 0])) + 1)
  361. plt.plot([min(Z_log[:, 0]), max(Z_log[:, 0])], [best_log_top, best_log_top], c='black')
  362. plt.plot([best_log_right, best_log_right], [min(Z_log[:, 1]), max(Z_log[:, 1])], c='black')
  363. print(channame1)
  364. print(channame2)
  365. plt.savefig(join(dest, channame1 + '_' + channame2 + '_debug_third_gate.pdf'), format='pdf', bbox_inches='tight')
  366. plt.cla()
  367. plt.clf()
  368. boundaries = [best_right_point, best_top_point]
  369. if len(sample.channels) == 7:
  370. gname = join(dest, fluorophores + '_gates.xml')
  371. else:
  372. gname = join(dest, 'gates.xml')
  373. if neg_cntrl[0] != "None":
  374. if neg_cntrl[0] == samplename.rsplit('.fcs')[0]:
  375. best_top_point = nc_top_point
  376. best_right_point = nc_right_point
  377. boundaries = [best_right_point, best_top_point]
  378. else:
  379. best_right_point, best_top_point = neg_cntrl[1]
  380. boundaries = [best_right_point, best_top_point]
  381. samplename = samplename + "_" + channame1 + "_" + channame2
  382. gate_writer(vertices1, vertices2, boundaries, gname, channame1, channame2, fluorophore1, fluorophore2)
  383. g_strat = fk.parse_gating_xml(gname)
  384. gs_results = g_strat.gate_sample(sample)
  385. e = gs_results.get_gate_membership('And3DoublePositive')
  386. fig = figure(num=None, figsize=(16, 16), dpi=80, facecolor='w', edgecolor='k')
  387. xy = d
  388. x = d[:, 0]
  389. y = d[:, 1]
  390. x_sorted, y_sorted, kde_sorted = x[idx], y[idx], kde[idx]
  391. # parameters of the main output plot
  392. plt.scatter(x, y, c=kde, cmap = 'turbo', s=15)
  393. plt.yscale('symlog', linthresh=1000)
  394. plt.xscale('symlog', linthresh=1000)
  395. # plt.scatter(new_Z[:, 0], new_Z[:, 1], c=kde, cmap = 'turbo', s=15)
  396. # plt.yscale('log')
  397. # plt.xscale('log')
  398. #these are gate lines; min, max are the range of point values; best points define position of the gate
  399. plt.plot([min(x), max(x)], [best_top_point, best_top_point], c='black')
  400. plt.plot([best_right_point, best_right_point], [min(y), max(y)], c='black')
  401. #df is a table format for... parsed.. data..
  402. df = gs_results.report
  403. df = df.reset_index()
  404. #numbers of each individual quadrant
  405. double_positives = list(df.loc[df['gate_name'] == 'And3DoublePositive']['count'])[0]
  406. green = list(df.loc[df['gate_name'] == 'And5fluorophoreb']['count'])[0]
  407. red = list(df.loc[df['gate_name'] == 'And4fluorophorea']['count'])[0]
  408. untransfected = list(df.loc[df['gate_name'] == 'And2Untransfected']['count'])[0]
  409. #for each sample, for each pair of colors in it, export a dataframe with fluorescence values for each cell in it
  410. color_df = pd.DataFrame(columns = [channame2, channame1])
  411. color_df[channame2] = x
  412. color_df[channame1] = y
  413. color_df.to_csv(join(dest, channame1 + "_" + channame2 + ".csv"))
  414. #this is a contingency for blank samples
  415. if neg_cntrl[0] == "None" and double_positives + green + red == 0:
  416. print("Only untransfected cells found")
  417. return [samplename, 0, None, 0, boundaries, [untransfected, red, green, double_positives]]
  418. #defining the FETCH score
  419. try:
  420. FETCH_score = double_positives/(double_positives + green + red)
  421. except ZeroDivisionError:
  422. FETCH_score = 0
  423. #another contingency
  424. if neg_cntrl[0] == "None" and (FETCH_score > 0.90 or untransfected/(double_positives + green + red + untransfected) > 0.90):
  425. print("FETCH score unreasonably high -- something went wrong")
  426. return [samplename, 0, None, double_positives + green + red + untransfected, boundaries, [untransfected, red, green, double_positives]]
  427. try:
  428. r_g = red/green
  429. except ZeroDivisionError:
  430. print("Can't calculate the proportion of red to green: there is no green cells")
  431. r_g = 0
  432. ax = plt.gca()
  433. minor = matplotlib.ticker.LogLocator(base = 10.0, subs = np.arange(1.0, 10.0) * 0.1, numticks = 10)
  434. ax.yaxis.set_minor_locator(minor)
  435. ax.yaxis.set_minor_formatter(matplotlib.ticker.NullFormatter())
  436. ax.xaxis.set_minor_locator(minor)
  437. ax.xaxis.set_minor_formatter(matplotlib.ticker.NullFormatter())
  438. ax.tick_params(which='minor', length=10, width=2)
  439. ax.tick_params(which='major', length=20, width=3)
  440. plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor", fontsize=14)
  441. plt.setp(ax.get_yticklabels(), fontsize=14)
  442. #text boxes in the output plot
  443. txt1 = AnchoredText('Q1\n' + str(round(100*red/(double_positives + green + red + untransfected), 1)), loc="upper left", pad=0.4, borderpad=0, prop={"fontsize":14})
  444. txt2 = AnchoredText('Q2\n' + str(round(100*double_positives/(double_positives + green + red + untransfected), 1)), loc="upper right", pad=0.4, borderpad=0, prop={"fontsize":14})
  445. txt3 = AnchoredText('Q3\n' + str(round(100*green/(double_positives + green + red + untransfected), 1)), loc="lower right", pad=0.4, borderpad=0, prop={"fontsize":14})
  446. txt4 = AnchoredText('Q4\n' + str(round(100*untransfected/(double_positives + green + red + untransfected), 1)), loc="lower left", pad=0.4, borderpad=0, prop={"fontsize":14})
  447. #this puts texts boxes onto the plot
  448. ax.add_artist(txt1)
  449. ax.add_artist(txt2)
  450. ax.add_artist(txt3)
  451. ax.add_artist(txt4)
  452. ax.set_title("FETCH Score: " + str(FETCH_score))
  453. #look into the bbox, bounding box
  454. plt.savefig(join(dest, channame1 + '_' + channame2 + '_double_positive_final.pdf'), format='pdf', bbox_inches='tight')
  455. #clear axes and figure to plot next
  456. plt.cla()
  457. plt.clf()
  458. return [samplename, FETCH_score, r_g, double_positives + green + red + untransfected, boundaries, [untransfected, red, green, double_positives]]
  459. #Above, the helper files were added, Below here, the actual processing, central functions are listed
  460. def FETCH_analysis(inputlist):
  461. plt.close('all')
  462. fcs_path, samplename, dest, leg_g1, neg_cntrl, log_gate = inputlist
  463. #make a directly for an FCS file and use flowkit to parse that, to get variable called sample
  464. os.mkdir(dest)
  465. plt.grid(visible=None)
  466. sample = fk.Sample(fcs_path)
  467. fsc_a_loc = sample.channels.loc[sample.channels['pnn'].str.contains('FSC-A')].index[0]
  468. ssc_a_loc = sample.channels.loc[sample.channels['pnn'].str.contains('SSC-A')].index[0]
  469. fsc_h_loc = sample.channels.loc[sample.channels['pnn'].str.contains('FSC-H')].index[0]
  470. remaining_rows = [r for r in range(sample.channels.shape[0]) if r not in [fsc_a_loc, ssc_a_loc, fsc_h_loc]]
  471. other_chans = list(sample.channels.iloc[remaining_rows]['pnn'])
  472. other_chans = [chn for chn in other_chans if 'Time' not in chn]
  473. fluor_chan_n = len(other_chans)
  474. arr1 = sample.get_channel_events(fsc_a_loc, source='raw', subsample=False) #FSC-A
  475. arr2 = sample.get_channel_events(ssc_a_loc, source='raw', subsample=False) #SSC-A
  476. arr3 = sample.get_channel_events(fsc_h_loc, source='raw', subsample=False) #FSC-H
  477. if 'FITC' in other_chans or '1-A' in other_chans: #always have green along x axis
  478. if 'FITC' in other_chans:
  479. grn_ch_name = 'FITC'
  480. else:
  481. grn_ch_name = '1-A'
  482. x_ax_index = [chn for chn in other_chans if grn_ch_name in chn][0]
  483. x_chan_name = list(sample.channels.loc[sample.channels['pnn'].str.contains(grn_ch_name)]['pns'])[0]
  484. green_loc = sample.channels.loc[sample.channels['pnn'].str.contains(grn_ch_name)].index[0]
  485. other_chans = [chn for chn in other_chans if grn_ch_name not in chn]
  486. arr4 = sample.get_channel_events(green_loc, source='raw', subsample=False) #1-A or FITC-A(Emerald)
  487. else:
  488. x_ax_index = other_chans[0]
  489. x_chan_name = list(sample.channels.loc[sample.channels['pnn'].str.contains(other_chans[0])]['pns'])[0]
  490. first_loc = sample.channels.loc[sample.channels['pnn'].str.contains(other_chans[0])].index[0]
  491. other_chans = [chn for chn in other_chans if other_chans[0] not in chn]
  492. arr4 = sample.get_channel_events(first_loc, source='raw', subsample=False) #1-A or FITC-A(Emerald)
  493. if x_chan_name == '':
  494. x_chan_name = x_ax_index
  495. if len(other_chans) == 0: #If only got one fluorescent channel, use it as x and FSC-A as y
  496. y_chan_name = 'FSC-A'
  497. y_ax_index = 'FSC-A'
  498. Z = np.stack((arr4, arr1), axis=1)
  499. else:
  500. second_loc = sample.channels.loc[sample.channels['pnn'].str.contains(other_chans[0])].index[0]
  501. y_ax_index = other_chans[0]
  502. y_chan_name = list(sample.channels.loc[sample.channels['pnn'].str.contains(other_chans[0])]['pns'])[0]
  503. if y_chan_name == '':
  504. y_chan_name = y_ax_index
  505. arr5 = sample.get_channel_events(second_loc, source='raw', subsample=False) #5-A(RFP670), PE-Texas Red-A(mCherry), PE-A (mApple), or any other color
  506. Z = np.stack((arr4, arr5), axis=1)
  507. if fluor_chan_n == 3: # have 3 fluorescent channels
  508. third_loc = sample.channels.loc[sample.channels['pnn'].str.contains(other_chans[1])].index[0]
  509. z_chan_name = list(sample.channels.loc[sample.channels['pnn'].str.contains(other_chans[1])]['pns'])[0]
  510. z_ax_index = other_chans[1]
  511. arr6 = sample.get_channel_events(third_loc, source='raw', subsample=False)
  512. Z_ea = np.stack((arr4, arr5), axis=1)
  513. Z_er = np.stack((arr4, arr6), axis=1)
  514. Z_ar = np.stack((arr5, arr6), axis=1)
  515. elif fluor_chan_n > 3:
  516. raise Exception("Something is wrong with your channel number")
  517. #arr = array, plot.. x = 1st gate and y = 2nd gate
  518. if log_gate:
  519. alter_X = np.array([arr2 + abs(min(arr2)) + 1, arr1 + abs(min(arr1)) + 1])
  520. alter_X = alter_X.T
  521. X = np.log(alter_X)
  522. else:
  523. X = np.stack((arr2, arr1), axis=1)
  524. Y = np.stack((arr1, arr3), axis=1)
  525. #this loop goes through the contours of the first gate
  526. [alls, figure_centre, x, y] = make_kde(X)
  527. max_area = 0
  528. best_seg = None
  529. point_num = None
  530. plt.figure(num=None, figsize=(16, 16), dpi=80, facecolor='w', edgecolor='k')
  531. seg_list = []
  532. min_points = 4000
  533. for j in range(len(alls)):
  534. for ii, seg in enumerate(alls[j]):
  535. non_single_cells_x = min(seg[:,0]) <= 25000
  536. non_single_cells_y = min(seg[:,1]) <= 25000
  537. out_of_bounds_x = max(seg[:,0]) > (max(x) - 10000)
  538. out_of_bounds_y = max(seg[:,1]) > (max(y) - 10000)
  539. plt.plot(seg[:,0], seg[:,1], '.-')
  540. area = 0.5*np.abs(np.dot(seg[:,0],np.roll(seg[:,1],1))-np.dot(seg[:,1],np.roll(seg[:,0],1)))
  541. p = path.Path(seg)
  542. mask = p.contains_points(X)
  543. num_points = X[mask].shape[0]
  544. if num_points >= min_points:
  545. seg_list.append([j, area, ii])
  546. if non_single_cells_x or non_single_cells_y or out_of_bounds_x or out_of_bounds_y or len(seg[:,0])<10:
  547. continue
  548. if area > max_area:
  549. max_area = area
  550. best_seg = seg
  551. point_num = num_points
  552. if best_seg is None or point_num < min_points:
  553. if len(seg_list) == 0:
  554. print('No cells in the first gate')
  555. return [samplename, 0, None, 0]
  556. newbest = None
  557. smallest_area = np.inf
  558. for item in seg_list:
  559. if item[1] < smallest_area:
  560. smallest_area = item[1]
  561. best_seg = alls[item[0]][item[2]]
  562. #fitting an elipse to our identified best fit contour
  563. if log_gate:
  564. return_X = np.array([np.exp(X[:, 0]), np.exp(X[:, 1])])
  565. return_X = return_X.T
  566. X = np.array([return_X[:, 0] - abs(min(arr2)) - 1, return_X[:, 1] - abs(min(arr1)) - 1])
  567. X = X.T
  568. return_best_seg = np.array([np.exp(best_seg[:, 0]), np.exp(best_seg[:, 1])])
  569. return_best_seg = return_best_seg.T
  570. best_seg = np.array([return_best_seg[:, 0] - abs(min(arr2)) - 1, return_best_seg[:, 1] - abs(min(arr1)) - 1])
  571. best_seg = best_seg.T
  572. ell_coord, el_cx, el_cy, el_w, el_h, el_angle = fitEllipse(best_seg[:,0],best_seg[:,1])
  573. vertices1 = np.round(ell_coord, 0)
  574. if not leg_g1: #Keep the definition of vertices1 only if replicating old data
  575. # Step 1: Filter points whose y values are within 1000 of the target value
  576. filtered_points = X[np.abs(X[:, 1] - min(vertices1[:, 1])) <= 1000]
  577. thresh = 1000
  578. while np.shape(filtered_points)[0] == 0:
  579. thresh += 500
  580. filtered_points = X[np.abs(X[:, 1] - min(vertices1[:, 1])) <= thresh]
  581. # # Step 2: Sort the filtered points based on x values
  582. sorted_points = filtered_points[np.argsort(filtered_points[:, 0])]
  583. # # Step 3: Select the point with the smallest x value from the sorted array
  584. left_low = sorted_points[0]
  585. # # # Step 4: Filter points whose y values are at least 10 more than the left_low's
  586. # filtered_points2 = X[(X[:, 1] > (left_low[1] + 10)) & (X[:, 0] > left_low[0])]
  587. # # # Step5: get the left_mid point to get the slope of the left bound
  588. # left_mid = filtered_points2[np.argsort(filtered_points2[:, 0])][0]
  589. # # left_mid = [left_mid[1], left_mid[0]]
  590. # slope = (left_mid[1] - left_low[1]) / (left_mid[0] - left_low[0])
  591. # y_intercept = left_low[1] - slope * left_low[0]
  592. # top_left = [(max(X[:, 1]) - 10 - y_intercept) / slope, max(X[:, 1]) -10]
  593. top_left = [left_low[0], max(X[:, 1]) -10]
  594. right_top = [max(X[:, 0]) -10, max(X[:, 1]) -10]
  595. # #Step 6: get the line parallel to the ellipse's angle and perpendicular to its second principal component to define left bound
  596. rad90 = np.radians(90)
  597. if el_angle < 0:
  598. el_angle = el_angle + 90
  599. ell_side_point = [el_cx + el_h/2*np.sin(np.radians(el_angle))/np.sin(rad90), el_cy - el_h/2*np.sin(np.radians(90 - el_angle))/np.sin(rad90)]
  600. slope_right = np.tan(np.radians(el_angle))
  601. y_intercept_right = ell_side_point[1] - slope_right * ell_side_point[0]
  602. mid_right = [max(X[:, 0]) -10, slope_right*(max(X[:, 0]) -10) + y_intercept_right]
  603. low_right = [(left_low[1] - y_intercept_right)/slope_right, left_low[1]]
  604. vertices1 = np.round([left_low, top_left, right_top, mid_right, low_right], 0)
  605. #generates the .xml file
  606. filename = join(dest, 'gates.xml')
  607. gate_writer(vertices1, None, None, filename)
  608. g_strat = fk.parse_gating_xml(filename)
  609. gs_results = g_strat.gate_sample(sample)
  610. #gets the indices of selected cells to move into gate 2
  611. a = gs_results.get_gate_membership('Polygon1')
  612. b = Y[a]
  613. #len= length; a contingency
  614. if len(b) == 0:
  615. print('No cells in the second gate')
  616. return [samplename, 0, None, 0]
  617. plt.scatter(X[:, 0], X[:, 1], c=a, s=12.5)
  618. ax = plt.gca()
  619. ax.set_xlabel('SSC-A', fontsize=36)
  620. ax.set_ylabel('FSC-A', fontsize=36)
  621. ax.set_title("First Gate", fontsize=30)
  622. plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor", fontsize=36)
  623. plt.setp(ax.get_yticklabels(), fontsize=36)
  624. plt.savefig(join(dest, 'first_gate_KDE.pdf'), format='pdf', bbox_inches='tight')
  625. plt.cla()
  626. plt.clf()
  627. coefficients = np.polyfit(b[:, 0], b[:, 1], 1)
  628. poly = np.poly1d(coefficients)
  629. new_x = np.linspace(min(b[:, 0]), max(b[:, 0]), 2)
  630. new_y = poly(new_x)
  631. norms = []
  632. p1 = np.array([new_x[0], new_y[0]])
  633. p2 = np.array([new_x[1], new_y[1]])
  634. for point in b:
  635. d = norm(np.cross(p2-p1, p1-point))/norm(p2-p1)
  636. norms.append(d)
  637. std = np.array(norms).std()
  638. mask2 = []
  639. for i in range(len(b)):
  640. if norms[i] > 4*std:
  641. mask2.append(False)
  642. else:
  643. mask2.append(True)
  644. coefficients2 = np.polyfit(b[:, 0], b[:, 1], 1)
  645. poly2 = np.poly1d(coefficients)
  646. new_x2 = np.linspace(min(b[:, 0]), max(b[:, 0]), 2)
  647. new_y2 = poly(new_x)
  648. factor = 4*std
  649. plt.figure(num=None, figsize=(16, 16), dpi=80, facecolor='w', edgecolor='k')
  650. plt.scatter(b[:, 0], b[:, 1], c=mask2, s=12.5)
  651. plt.plot(new_x.tolist(), new_y.tolist(), marker = "o", c='red')
  652. plt.plot(new_x, [new_y[0] - factor, new_y[1] - factor], marker = "o", c='black')
  653. plt.plot(new_x, [new_y[0] + factor, new_y[1] + factor], marker = "o", c='black')
  654. ax = plt.gca()
  655. ax.set_xlabel('FSC-A', fontsize=36)
  656. ax.set_ylabel('FSC-H', fontsize=36)
  657. ax.set_title("Second Gate", fontsize=36)
  658. plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor", fontsize=36)
  659. plt.setp(ax.get_yticklabels(), fontsize=36)
  660. plt.savefig(join(dest, 'secondgate.pdf'), format='pdf', bbox_inches='tight')
  661. plt.cla()
  662. plt.clf()
  663. vertices2 = np.array([[new_x[0], new_y[0] - factor],
  664. [new_x[1], new_y[1] - factor],
  665. [new_x[1], new_y[1] + factor],
  666. [new_x[0], new_y[0] + factor]])
  667. if np.isnan(vertices1).any() or np.isnan(vertices2).any():
  668. return [samplename, 0, None, 0]
  669. gate_writer(vertices1, vertices2, None, filename)
  670. g_strat = fk.parse_gating_xml(filename)
  671. gs_results = g_strat.gate_sample(sample)
  672. a = gs_results.get_gate_membership('And1')
  673. c = Y[a]
  674. fluorescent_chan_names = list(sample.channels['pns'].unique())
  675. if fluor_chan_n == 1 or fluor_chan_n == 2:
  676. return z(samplename, Z, a, vertices1, vertices2, dest, sample, y_chan_name, x_chan_name, y_ax_index, x_ax_index, [neg_cntrl[0], neg_cntrl[1][0]], leg_g1)
  677. elif len(sample.channels) == 7:
  678. first = z(samplename, Z_ea, a, vertices1, vertices2, dest, sample, y_chan_name, x_chan_name, y_ax_index, x_ax_index, [neg_cntrl[0], neg_cntrl[1][0]], leg_g1)
  679. second = z(samplename, Z_er, a, vertices1, vertices2, dest, sample, z_chan_name, x_chan_name, z_ax_index, x_ax_index, [neg_cntrl[0], neg_cntrl[1][1]], leg_g1)
  680. third = z(samplename, Z_ar, a, vertices1, vertices2, dest, sample, z_chan_name, y_chan_name, z_ax_index, y_ax_index, [neg_cntrl[0], neg_cntrl[1][2]], leg_g1)
  681. return [first, second, third]
  682. def summarize(outputs, fcs_folder, project_name, skip_renaming):
  683. dataf = pd.DataFrame(outputs)
  684. print(dataf)
  685. dataf = dataf.rename({0: "File", 1: "FETCH score", 2 : "r_g", 3:"n_tot", 4: "3rd_gate_coord", 5:"raw_counts"}, axis='columns')
  686. dataf['Dubious?'] = [False for i in range(dataf.shape[0])]
  687. dataf['FETCH score'] = dataf['FETCH score'].astype(float)
  688. dataf['r_g'] = dataf['r_g'].astype(float)
  689. dataf['n_tot'] = dataf['n_tot'].astype(int)
  690. dataf['Dubious?'] = dataf.apply(lambda row : 'yes' if ((row['r_g'] >= 2) or (row['r_g'] <= 0.5) or (row['n_tot'] < 500) or np.isnan(row['r_g'])) else 'no',
  691. axis=1)
  692. dataf = dataf.drop(['r_g'], axis=1)
  693. try:
  694. dataf["File"] = dataf["File"].apply(lambda x: x.rsplit('_')[2] + '_' + x.rsplit('_')[0] + '_' + x.rsplit('_')[1] + '_' + x.rsplit('_')[3] if x not in skip_renaming else x)
  695. except IndexError:
  696. pass
  697. dataf = dataf.sort_values(by='File')
  698. dataf['Numbername'] = [i for i in range(dataf.shape[0])]
  699. sns.set(font_scale=2)
  700. figure(num=None, figsize=(32, 16), dpi=80, facecolor='w', edgecolor='k')
  701. colors = [(0, 0, 0) if dataf["Dubious?"].iloc[i] == 'no' else (1, 0, 0) for i in range(dataf["File"].unique().shape[0])]
  702. sns.set_style('ticks')
  703. sns.catplot(x='File', y='FETCH score', palette=colors, capsize=.2, kind="point", ci="sd", data=dataf, height=10, aspect=2)
  704. g = sns.swarmplot(x='File', y='FETCH score', data=dataf, color="purple", size=5)
  705. ax = plt.gca()
  706. ax.set_xlabel('FETCH_id')
  707. ax.set_ylabel('FETCH Score')
  708. plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor", fontsize=8)
  709. plt.setp(ax.get_yticklabels(), fontsize=26)
  710. ax.set(facecolor = "white")
  711. ax.set_title(project_name, fontsize=26)
  712. dataf['FETCH score'] = dataf['FETCH score'].fillna(0)
  713. ax.set(ylim=(0, max(dataf['FETCH score'])+0.1))
  714. plt.savefig(join(fcs_folder, project_name + ".pdf"), format='pdf', bbox_inches='tight')
  715. plt.cla()
  716. plt.clf()
  717. plt.close()
  718. dataf = dataf.set_index("File")
  719. dataf.to_csv(join(fcs_folder, project_name + ".csv"))
  720. #identify which folder contains our fcs files, etc.
  721. def main(args):
  722. fcs_folder = args.folder
  723. project_name = args.project
  724. leg_g1 = not(args.legacy_analysis == 'False') #for replicability of past analysis, add an optional argument that sets a more narrow first gate
  725. skip_renaming = args.skip_renaming
  726. if skip_renaming == '':
  727. skip_renaming = []
  728. negative_control = args.negative_control
  729. predefined_negative_control = args.predefined_negative_control
  730. log_gate = args.log_gate
  731. if log_gate != 'None':
  732. log_gate = True
  733. else:
  734. log_gate = False
  735. plt.cla()
  736. plt.clf()
  737. plt.close()
  738. plt.style.use('default')
  739. inpts = [[join(fcs_folder, samplename), samplename,
  740. join(fcs_folder, samplename.rsplit('.')[0]), leg_g1,
  741. [negative_control, [[None, None], [None, None], [None, None]]], log_gate] if
  742. (samplename != '.DS_Store' and not os.path.isdir(join(fcs_folder, samplename.rsplit('.')[0])))
  743. else None for samplename in os.listdir(fcs_folder)]
  744. inpts = list(filter(None, inpts))
  745. outstuff = []
  746. if negative_control != "None":
  747. for i_pos, el in enumerate(inpts):
  748. if el[1].rsplit('.fcs')[0] == negative_control:
  749. neg_outpt = FETCH_analysis(inpts.pop(i_pos))
  750. if len(neg_outpt) == 3: #the three fluorescent channels condition
  751. first_res = neg_outpt[0][4]
  752. second_res = neg_outpt[1][4]
  753. third_res = neg_outpt[2][4]
  754. for subel in neg_outpt:
  755. outstuff.append(subel)
  756. inpts = [[el[0], el[1], el[2], el[3], [el[4], [first_res, second_res, third_res]], log_gate] for el in inpts]
  757. else:
  758. outstuff.append(neg_outpt)
  759. inpts = [[el[0], el[1], el[2], el[3], [el[4], [neg_outpt[4], [None, None], [None, None]]], log_gate] for el in inpts]
  760. break
  761. if predefined_negative_control != "None":
  762. x_ax_thresh, y_ax_thresh = [int(val) for val in predefined_negative_control.split(',')]
  763. inpts = [[el[0], el[1], el[2], el[3], [el[4], [[x_ax_thresh, y_ax_thresh], [None, None], [None, None]]], log_gate] for el in inpts]
  764. for inp in inpts:
  765. res = FETCH_analysis(inp)
  766. if len(res) == 3:
  767. for subel in res:
  768. outstuff.append(subel)
  769. else:
  770. outstuff.append(res)
  771. #draws the aggregate plot figure and table comparing FETCH scores
  772. summarize(outstuff, fcs_folder, project_name, skip_renaming)
  773. #this is where the code actually starts; runs 'main', above
  774. if __name__ == '__main__':
  775. args = parse_arguments()
  776. main(args)

FETCH.py at commit 89e9901, no license · at the source

Overview

Authors: Elizabeth Ransey1,2, Gwenaëlle E. Thomas1,3, Elias M. Wisdom4, Agustin Almoril-Porras4, Ryan Bowman2, Elise Adamson1,5, Kathryn K. Walder-Christensen1,2, Jesse A. White1,6, Dalton N. Hughes1,3, Hannah Schwennesen2, Caly Ferguson1,2, Kay M. Tye1,6, Stephen D. Mague1,2, Longgang Niu7, Zhao-Wen Wang7, Daniel Colón-Ramos4,8, Rainbo Hultman9, Nenad Bursac5, Kafui Dzirasa1,2,3,5,10
  1. Howard Hughes Medical Institute,Chevy Chase, MD USA
  2. Deparment of Psychiatry and Behavioral Sciences, Duke University Medical Center,Durham, NC USA
  3. Department of Neurobiology, Duke University Medical Center,Durham, NC USA
  4. Department of Neuroscience and Department of Cell Biology, Program in Cellular Neuroscience, Neurodegeneration and Repair, Yale University School of Medicine,New Haven, CT USA
  5. Department of Biomedical Engineering, Duke University,Durham, NC USA
  6. Salk Institute for Biological Studies,La Jolla, CA USA
  7. Department of Neuroscience, University of Connecticut School of Medicine,Farmington, CT USA
  8. Instituto de Neurobiología, Recinto de Ciencias Médicas, Universidad de Puerto Rico,San Juan, Puerto Rico
  9. Department of Molecular Physiology and Biophysics, Department of Psychiatry, University of Iowa,Iowa City, IA USA
  10. Department of Neurosurgery, Duke University Medical Center,Durham, NC USA
Institutions: Howard Hughes Medical Institute (United States); Duke Medical Center (United States); Yale University (United States); Duke University (United States); Salk Institute for Biological Studies (United States); University of Connecticut (United States); University of Puerto Rico System (Puerto Rico); University of Iowa (United States)
Journal: Nature, volume 655, issue 8123, pages 703-715
Dates: received 9 August 2023; accepted 7 April 2026; published online 13 May 2026; in print 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1038/s41586-026-10501-y · PMID 42129559 · PMCID PMC13372691 · OpenAlex W7160996005
Open access: hybrid, a free copy (OpenAlex)
Status: code verified
Categories: mouse (organism), C. elegans (organism), cellular / molecular (subfield)
Methods: Spectral & time-frequency, Preprocessing, Connectivity, Statistics, Smoothing, state filtering, decompositions, Machine learning, Evoked potentials, fMRI & imaging, Single-unit activity, calcium imaging
Keywords: Emotion, Molecular neuroscience
MeSH: Brain*, Connexins*, Electrical Synapses*, Amino Acid Motifs, Animals, Caenorhabditis elegans, Mice, Models, Molecular, Time Factors (* major topic)
Topic: Connexins and lens biology (Molecular Biology, Biochemistry, Genetics and Molecular Biology), according to OpenAlex
Funding: NHLBI NIH HHS (R01 HL126524, R01 HL132389, R01 HL164013, U01 HL134764); NIMH NIH HHS (R01 MH125430, R01 MH120158, DP1 MH132709, R01 MH085927); NIBIB NIH HHS (R01 EB032726); NINDS NIH HHS (R01 NS076558, DP1 NS111778); NEI NIH HHS (R21 EY029451)
Citations: cited by 3 papers (Europe PMC); 98 references in the paper

Abstract

Electrical signalling across distinct populations of brain cells underpins cognitive and emotional function. However, approaches that selectively regulate electrical signalling between two cellular components of a mammalian neural circuit remain sparse. Here we engineered an electrical synapse composed of two connexin proteins1 found in Morone americana (white perch fish)—connexin 34.7 and connexin 35—to accomplish mammalian circuit modulation. By exploiting protein mutagenesis, devising a new in vitro system for assaying connexin hemichannel docking, and performing computational modelling of hemichannel interactions, we uncovered a structural motif that contributes to electrical synapse formation. Targeting this motif, we designed connexin 34.7 and connexin 35 hemichannels that dock with each other to form an electrical synapse but not with other major connexins expressed in the mammalian central nervous system. We validated this electrical synapse in vivo using worms (Caenorhabditis elegans) and mice (Mus musculus). We demonstrate that it can strengthen communication across neural circuits composed of pairs of distinct cell types and modify behaviour accordingly. Thus, we establish ‘long-term integration of circuits using connexins’ (LinCx) for precision circuit editing in mammals.

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

Repositories

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

carlson-lab/FETCH

License: none: the authors keep all their rights
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 89e99011dd920cb70f1ea87cf26c894bf9b4e3c3, 21 July 2024
Languages: Python (1)
Size: 5 files, 1 script
Software Heritage: not archived
Found in: “Code availability”
Holds: README
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Matplotlib (1 file), NumPy (1 file), pandas (1 file), scikit-learn (1 file), SciPy (1 file), seaborn (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
2 files

carlson-lab/VMD-and-NAMD-Connexin-Protein-Simulation-Protocol

License: CC-BY-4.0
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 0c48394e07514b514bf671a383587a89017c46e8, 15 August 2025
Size: 10 files
Software Heritage: not archived
Found in: “Code availability”
Holds: README, license file
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
2 files

carlson-lab/OptoLinCx

License: none: the authors keep all their rights
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: fa62a83d2fe30cfd0e3b744174fb6cf5fcae5be4, 4 November 2024
Languages: MATLAB (65), Jupyter (2), C (1), C/C++ (1)
Size: 115 files, 69 scripts
Software Heritage: not archived
Found in: “Code availability”
Holds: README, 2 notebooks
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Signal Processing Toolbox (2 files), Matplotlib (2 files), Neo (2 files), NumPy (2 files), pandas (2 files), Pingouin (2 files), SciPy (2 files), seaborn (2 files), Statistics and Machine Learning Toolbox (1 file), scikit-learn (1 file), statsmodels (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
70 files

Code availability

The following codes for the methods implemented in this study are available from GitHub: (1) https://github.com/carlson-lab/FETCH; (2) https://github.com/carlson-lab/VMD-and-NAMD-Connexin-Protein-Simulation-Protocol; (3) https://github.com/carlson-lab/OptoLinCx. All are available stored in an online repository (10.7924/r4r486).

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:

  • 3 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 70 scripts, each with its path and the digest of its content;
  • 13 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

All data generated in support of the findings of this study are available from the corresponding author for academic purposes upon reasonable request. Such data will be made available under a material transfer agreement. Connexin gene information was procured from the National Center for Biotechnology Information (https://www.ncbi.nlm.nih.gov) and the Ensembl genome browser (http://ensembl.org), and are readily accessible.

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

Versions

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

Version 2, 28 September 2026

  • Publisher: n/a → Nature Portfolio

Version 1, 28 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 19 authors, 2 keywords, 9 MeSH terms, 5 funders, 95 references.

Cite

This paper

Ransey, E., Thomas, G. E., Wisdom, E. M., Almoril-Porras, A., Bowman, R., Adamson, E., Walder-Christensen, K. K., White, J. A., Hughes, D. N., Schwennesen, H., Ferguson, C., Tye, K. M., Mague, S. D., Niu, L., Wang, Z.-W., Colón-Ramos, D., Hultman, R., Bursac, N., & Dzirasa, K. (2026). Long-term editing of brain circuits using an engineered electrical synapse. Nature, 655(8123), 703-715. https://doi.org/10.1038/s41586-026-10501-y

BibTeX

@article{ransey2026long,
author = {Ransey, Elizabeth and Thomas, Gwenaëlle E. and Wisdom, Elias M. and Almoril-Porras, Agustin and Bowman, Ryan and Adamson, Elise and Walder-Christensen, Kathryn K. and White, Jesse A. and Hughes, Dalton N. and Schwennesen, Hannah and Ferguson, Caly and Tye, Kay M. and Mague, Stephen D. and Niu, Longgang and Wang, Zhao-Wen and Colón-Ramos, Daniel and Hultman, Rainbo and Bursac, Nenad and Dzirasa, Kafui},
title = {{Long-term editing of brain circuits using an engineered electrical synapse}},
journal = {Nature},
year = {2026},
month = may,
volume = {655},
number = {8123},
pages = {703--715},
publisher = {Nature Portfolio},
issn = {0028-0836},
doi = {10.1038/s41586-026-10501-y},
url = {https://doi.org/10.1038/s41586-026-10501-y},
pmid = {42129559},
pmcid = {PMC13372691}
}

RIS

TY - JOUR
AU - Ransey, Elizabeth
AU - Thomas, Gwenaëlle E.
AU - Wisdom, Elias M.
AU - Almoril-Porras, Agustin
AU - Bowman, Ryan
AU - Adamson, Elise
AU - Walder-Christensen, Kathryn K.
AU - White, Jesse A.
AU - Hughes, Dalton N.
AU - Schwennesen, Hannah
AU - Ferguson, Caly
AU - Tye, Kay M.
AU - Mague, Stephen D.
AU - Niu, Longgang
AU - Wang, Zhao-Wen
AU - Colón-Ramos, Daniel
AU - Hultman, Rainbo
AU - Bursac, Nenad
AU - Dzirasa, Kafui
TI - Long-term editing of brain circuits using an engineered electrical synapse
T2 - Nature
J2 - Nature
PY - 2026
DA - 2026/05/13
VL - 655
IS - 8123
SP - 703
EP - 715
SN - 0028-0836
PB - Nature Portfolio
DO - 10.1038/s41586-026-10501-y
UR - https://doi.org/10.1038/s41586-026-10501-y
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41586-026-10501-y",
"type": "article-journal",
"title": "Long-term editing of brain circuits using an engineered electrical synapse",
"container-title": "Nature",
"author": [
{
"family": "Ransey",
"given": "Elizabeth"
},
{
"family": "Thomas",
"given": "Gwenaëlle E."
},
{
"family": "Wisdom",
"given": "Elias M."
},
{
"family": "Almoril-Porras",
"given": "Agustin"
},
{
"family": "Bowman",
"given": "Ryan"
},
{
"family": "Adamson",
"given": "Elise"
},
{
"family": "Walder-Christensen",
"given": "Kathryn K."
},
{
"family": "White",
"given": "Jesse A."
},
{
"family": "Hughes",
"given": "Dalton N."
},
{
"family": "Schwennesen",
"given": "Hannah"
},
{
"family": "Ferguson",
"given": "Caly"
},
{
"family": "Tye",
"given": "Kay M."
},
{
"family": "Mague",
"given": "Stephen D."
},
{
"family": "Niu",
"given": "Longgang"
},
{
"family": "Wang",
"given": "Zhao-Wen"
},
{
"family": "Colón-Ramos",
"given": "Daniel"
},
{
"family": "Hultman",
"given": "Rainbo"
},
{
"family": "Bursac",
"given": "Nenad"
},
{
"family": "Dzirasa",
"given": "Kafui"
}
],
"container-title-short": "Nature",
"volume": "655",
"issue": "8123",
"page": "703-715",
"DOI": "10.1038/s41586-026-10501-y",
"PMID": "42129559",
"PMCID": "PMC13372691",
"ISSN": "0028-0836",
"publisher": "Nature Portfolio",
"URL": "https://doi.org/10.1038/s41586-026-10501-y",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
13
]
]
}
}

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.1038/s42003-026-10464-w [code]
Quantifying electrostatic control of docking and binding energetics in functional Cx36 gap junctions.
Journal: Communications biology
In common: seaborn, scikit-learn, pandas, 3 other tools, cellular / molecular, 10 references
[2] doi:10.7554/elife.106496
AFD thermosensory neurons mediate tactile-dependent locomotion modulation in &lt;i&gt;C. elegans&lt;/i&gt;.
Journal: eLife
In common: C. elegans, cellular / molecular, 7 references
[3] doi:10.1016/j.celrep.2026.117590 [code]
Impaired behavioral inhibition in Fmr1 KO mice is linked to disrupted visual cortex theta oscillations.
Journal: Cell reports
In common: Neo, Pingouin, statsmodels, 7 other tools, mouse, 1 reference
[4] doi:10.1016/j.patter.2026.101590 [code]
Density-based longitudinal neuron tracking in high-density electrophysiological recordings.
Journal: Patterns (New York, N.Y.)
In common: Neo, Signal Processing Toolbox, statsmodels, 7 other tools, 1 reference
[5] doi:10.1126/sciadv.aef0343 [code]
Learning induces activation-mechanism-dependent neural plasticity in an intracortical microstimulation task.
Journal: Science advances
In common: Neo, Signal Processing Toolbox, statsmodels, 7 other tools
[6] doi:10.1016/j.isci.2026.117375 [code]
Motor priming is associated with widespread recruitment into neural ensembles and more rapid ensemble transitions.
Journal: iScience
In common: Pingouin, Signal Processing Toolbox, statsmodels, 7 other tools, 1 reference
[7] doi:10.7554/elife.108675 [code]
SynaptoTagMe, a toolkit for in vivo mapping and modulating neurotransmission at single-cell resolution.
Journal: eLife
In common: C. elegans, cellular / molecular, 3 references, author Daniel A Colón-Ramos
[8] doi:10.1038/s41593-026-02232-0 [code]
Entorhinal cortex represents task-relevant remote locations independently of CA1.
Journal: Nature neuroscience
In common: Pingouin, Signal Processing Toolbox, statsmodels, 7 other tools, mouse
[9] doi:10.1038/s41467-026-73818-2 [code]
Prefrontal parvalbumin neurons mediate working memory in a task demand-dependent manner.
Journal: Nature communications
In common: statsmodels, scikit-learn, pandas, 3 other tools, mouse, 3 references
[10] doi:10.1016/j.nicl.2026.104012 [code]
Structural-functional multilayer brain network properties and outcome of combined repetitive transcranial magnetic stimulation and psychotherapy for obsessive-compulsive disorder.
Journal: NeuroImage. Clinical
In common: Pingouin, Signal Processing Toolbox, statsmodels, 7 other tools

Contribute

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

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

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.