OSCR

Embodied behavioural complexity in a ciliated microorganism.

Code ↔ Paper

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

The 4 matches
  1. [1] § Methods › Wavelet analysis and multidimensional scaling analysis ↔ wavelet_analysis.jl, lines 189–253 · score 0.70 · overlapping points, wavelet vector, MDS coordinates, Euclidean, distances
  2. [2] § Results › Active ciliary bundle oscillations drive distinct behaviours ↔ Example_Codes.py, lines 97–166 · score 0.69 · Ciliary waveforms, tangent angle, arc length, cell body, axis, frame
  3. [3] § Methods › Wavelet analysis and multidimensional scaling analysis ↔ wavelet_analysis.jl, lines 128–187 · score 0.55 · dispersion heatmap, wavelet transform, multidimensional
  4. [4] § Methods › Cilium tracking and reconstruction ↔ Example_Codes.py, lines 48–94 · score 0.53 · Cartesian space, arc length, tangent, tracking, reconstruction, Cilium

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

Julia · 438 lines · 19 KB · CC-BY-4.0 · 2 matches

  1. cd(@__DIR__); import Pkg; Pkg.activate("")
  2. # Load relevant packages
  3. using LinearAlgebra, Statistics
  4. using ClassicalOrthogonalPolynomials, FFTW, RecurrenceRelationships
  5. using Optim
  6. using DelimitedFiles
  7. using ProgressMeter
  8. using CairoMakie, GLMakie; CairoMakie.activate!()
  9. using Roots
  10. using Metaheuristics
  11. using FFTW
  12. using Distances, Clustering, MultivariateStats
  13. import TOML
  14. include("chebyshev.jl")
  15. include("utils.jl")
  16. include("wavelet.jl")
  17. include("plotting.jl")
  18. ## Analysis parameters
  19. cwtfreqs = 2.0 .^LinRange(log2(5), log2(300), 101) # Log spaced frequencies to evaluate the wavelet transform at
  20. f = 1.0 # Morelet scale factor
  21. max_chebind = 20 # Maximum chebyshev index
  22. crplen = 20 # Length to crop wavelet transform at the boundaries
  23. # Experiments to pick out as examples
  24. target_names = ["F26(6)", "F20(4)", "F20(29)", "F20(16)", "F20(20)", "F20(25)", "F15(16)", "F7(4)", "F46(71)", "F27(4)", "F15(30)"]
  25. ##
  26. # Read in the data
  27. all_data = read_coefficients("LocalDataPath.toml")
  28. # Get all the nonstationary datasets
  29. nonstatInds = unique(push!(findall(.!in(["Stationary"]), all_data.datasettypes), findall(all_data.datasetnames .∈ (target_names, ))...))
  30. nonstat_data = all_data[nonstatInds]
  31. target_data = nonstat_data[target_names]
  32. # Get all the stationary datasets
  33. statInds = findall(in(["Stationary"]), all_data.datasettypes)
  34. stat_data = all_data[statInds]
  35. # Evaluate the wavelet transform
  36. nonstat_wavelets = CiliaWaveletTransforms(nonstat_data, cwtfreqs, f; max_cwtind = max_chebind, fps = 1000, zp = true)
  37. target_wavelets = CiliaWaveletTransforms(target_data, cwtfreqs, f; max_cwtind = max_chebind, fps = 1000, zp = true)
  38. ## Calculate the disperion plot
  39. cwtfreqs = nonstat_wavelets[1].cwtfreqs
  40. dispmat = Matrix{Float64}(undef, length(cwtfreqs), max_chebind)
  41. for i = 2:max_chebind + 1
  42. dispmat[:, i - 1] = mean(vcat([cdata.abs_wavelet_transform[crplen + 1:end - crplen, :, i] for cdata in nonstat_wavelets]..., target_wavelets[8].abs_wavelet_transform[crplen + 1:end - crplen, :, i]), dims = 1)[:]
  43. end
  44. # normalized version of the plot
  45. normdispmat = dispmat ./ sqrt.(sum(abs2, dispmat, dims = 1))
  46. # Calculate the Chebyshev wavenumber
  47. mL = Statistics.median(vcat(nonstat_data.Ls...))
  48. chebwavlen = 0.370*mL*[2; [mean(abs.(cos.(((0:n - 2) .+ 0.5) * pi / n) * (cos.(pi/n) - 1) .- sin.(((0:n - 2) .+ 0.5) * pi / n) * sin(pi/n))) for n = 2:max_chebind]]
  49. chebwavnum = 2*pi ./ chebwavlen
  50. # Get the trajectories on the wavelet transform
  51. dispkind = "softmax"
  52. if dispkind === "softmax"
  53. target_disptrajs = dispersion_trajectory_softmax.(target_wavelets, (chebwavnum, ), filtlen = 50)
  54. nonstat_disptrajs = dispersion_trajectory_softmax.(nonstat_wavelets, (chebwavnum, ), filtlen = 50)
  55. elseif dispkind === "mean"
  56. target_disptrajs = dispersion_trajectory.(target_wavelets, (chebwavnum, ))
  57. nonstat_disptrajs = dispersion_trajectory.(nonstat_wavelets, (chebwavnum, ))
  58. else
  59. error("Oops")
  60. end
  61. target_disptrajs_mean = dispersion_trajectory.(target_wavelets, (chebwavnum, ))
  62. nonstat_disptrajs_mean = dispersion_trajectory.(nonstat_wavelets, (chebwavnum, ))
  63. target_disptrajs_softmax = dispersion_trajectory_softmax.(target_wavelets, (chebwavnum, ), filtlen = 50)
  64. nonstat_disptrajs_softmax = dispersion_trajectory_softmax.(nonstat_wavelets, (chebwavnum, ), filtlen = 50)
  65. #target_disptrajs = dispersion_trajectory.(target_wavelets, (chebwavnum, ))
  66. #nonstat_disptrajs = dispersion_trajectory.(nonstat_wavelets, (chebwavnum, ))
  67. #= Uncomment if you want to save the analysis results
  68. # Save the results
  69. savelength = maximum(length.(getindex.(target_disptrajs, 1)))
  70. for i = 1:length(target_dispfs)
  71. push!(target_disptrajs[i][1], NaN*ones(savelength - length(target_disptrajs[i][1]))...)
  72. push!(target_disptrajs[i][2], NaN*ones(savelength - length(target_disptrajs[i][1]))...)
  73. end
  74. writedlm("dispersion_trajectories_v2.csv", [reshape([reshape(target_names, 1, :); reshape(target_names, 1, :)][:], 1, :); repeat(["wavenumber" "freq"], 1, 11); reshape([hcat(getindex.(target_disptrajs, 1)...); hcat(getindex.(target_disptrajs, 2)...)], savelength, :)], ',')
  75. writedlm("dispersion_04_08_25.csv", [["freq (Hz) / chebwavnum (um^-1)"; nonstat_wavelets.cwtfreqs] [chebwavnum'; dispmat]], ',')
  76. =#
  77. ##
  78. # Plot the result for the dispersion subsamples
  79. fig = Figure(size = (1000, 480), fontsize = 24)
  80. # Plot the dispersion heatmap
  81. LOGAX = false; LOGCB = false
  82. if LOGAX
  83. ax = Axis(fig[1, 1], aspect = AxisAspect(1), yscale = log10); ax.xlabel = "k (μm⁻¹)"; ax.ylabel = "f (Hz)" # yscale = log10
  84. else
  85. ax = Axis(fig[1, 1], aspect = AxisAspect(1)); ax.xlabel = "k (μm⁻¹)"; ax.ylabel = "f (Hz)" # yscale = log10
  86. end
  87. if LOGCB
  88. crange = (-2.35, -0.5)
  89. pltmat = log10.(normdispmat')
  90. hmp = heatmap!(ax, chebwavnum, nonstat_wavelets.cwtfreqs, pltmat, colormap = :nuuk, colorrange = crange)
  91. cb = Colorbar(fig[1, 2], hmp, label = "Log10 Normalized power")
  92. else
  93. crange = (0.0, 0.3)
  94. pltmat = normdispmat'
  95. hmp = heatmap!(ax, chebwavnum, nonstat_wavelets.cwtfreqs, pltmat, colormap = :nuuk, colorrange = crange)
  96. cb = Colorbar(fig[1, 2], hmp, label = "Normalized power")
  97. end
  98. # Reconstructions
  99. xs = LinRange(-1, 1, 1001)
  100. V = zeros(length(xs), 126); eval_vandermonde!(V, xs)
  101. for i = 1:9
  102. dispks = target_disptrajs[i][1]
  103. dispfs = target_disptrajs[i][2]
  104. crange = (1, size(target_data[i].allcfsx, 1))
  105. lines!(ax, dispks, dispfs, colormap = cmaps[i][2:end], color = 1:length(dispks), linewidth = 1.5, colorrange = crange)
  106. xrecon = V[:, 2:end] * target_data[i].allcfsx[:, 2:end]'
  107. yrecon = V[:, 2:end] * target_data[i].allcfsy[:, 2:end]'
  108. axtmp = Axis(fig[1, 3][divrem(i - 1, 3) .+ 1...], aspect = DataAspect()); hidedecorations!(axtmp)
  109. ylims!(axtmp, -100, 100); xlims!(axtmp, -100, 100)
  110. nplt = div(size(target_data[i].allcfsx, 1), 25) # Plot every 25 frames
  111. for j = size(target_data[i].allcfsx, 1):-nplt:1
  112. lines!(axtmp, Point2.(xrecon[:, j], yrecon[:, j]), color = j, colormap = cmaps[i][2:end], colorrange = crange, linewidth = 0.5)
  113. end
  114. end
  115. display(fig)
  116. ## Plot the results corresponding to the SI movies
  117. # Plot the result for the dispersion subsamples
  118. fig = Figure(size = (1000, 480), fontsize = 24)
  119. # Plot the dispersion heatmap
  120. ax = Axis(fig[1, 1], aspect = AxisAspect(1), yscale = log10); ax.xlabel = "k (μm⁻¹)"; ax.ylabel = "f (Hz)" # yscale = log10
  121. hmp = heatmap!(ax, chebwavnum, nonstat_wavelets.cwtfreqs, normdispmat', colormap = :nuuk, colorrange = (0, 0.3))
  122. cb = Colorbar(fig[1, 2], hmp, label = "Normalized power")
  123. # Reconstructions
  124. xs = LinRange(-1, 1, 1001)
  125. V = zeros(length(xs), 126); eval_vandermonde!(V, xs)
  126. cmapind = [1, 6]
  127. for i2 = 1:2
  128. i = i2 + 9
  129. dispks = target_disptrajs[i][1]
  130. dispfs = target_disptrajs[i][2]
  131. crange = (1, size(target_data[i].allcfsx, 1))
  132. lines!(ax, dispks, dispfs, colormap = cmaps[cmapind[i2]][2:end], color = 1:length(dispks), linewidth = 1.5, colorrange = crange)
  133. xrecon = V[:, 2:end] * target_data[i].allcfsx[:, 2:end]'
  134. yrecon = V[:, 2:end] * target_data[i].allcfsy[:, 2:end]'
  135. axtmp = Axis(fig[1, 3][i2, 1], aspect = DataAspect()); hidedecorations!(axtmp)
  136. ylims!(axtmp, -100, 100); xlims!(axtmp, -100, 100)
  137. nplt = div(size(target_data[i].allcfsx, 1), 25) # Plot every 25 frames
  138. for j = size(target_data[i].allcfsx, 1):-nplt:1
  139. lines!(axtmp, Point2.(xrecon[:, j], yrecon[:, j]), color = j, colormap = cmaps[cmapind[i2]][2:end], colorrange = crange, linewidth = 0.5)
  140. end
  141. end
  142. display(fig)
  143. ## Multidimensional scaling
  144. reorientation_mats = [hcat([cwav.abs_wavelet_transform[crplen + 1:end - crplen, :, i] for i = 2:max_chebind + 1]...) for cwav in nonstat_wavelets]
  145. for rmat = reorientation_mats
  146. rmat ./= sum(rmat, dims = 2)
  147. end
  148. # Split the data and perform MDS on each split
  149. using Random; Random.seed!(123456)
  150. # Total number of time points
  151. n = sum(size.(reorientation_mats, 1))
  152. # Map each time point to a unique linear time index (index of coli in expi = offset[i] + coli)
  153. offsets = [0; cumsum(size.(reorientation_mats, 1))]
  154. # Overlap
  155. c = 1666
  156. # Number of independent MSDs
  157. nfolds = 3
  158. m, r = divrem(n - c, nfolds)
  159. l = m + c
  160. # Maybe there is a small fold is not a perfect divisor
  161. np = ceil(Int, 1 + (n - l)/(l - c))
  162. # Generate a random permutation of the data
  163. rp = randperm(n)
  164. # Get the columns in each fold
  165. partitionInds = [rp[1:l], [rp[l + (i - 1)*(l - c) + 1:l + (i)*(l - c)] for i = 1:np - 2]..., rp[l + (np - 2)*(l - c) + 1:end]]
  166. # Map each linear time index to an experiment ind
  167. expindvec = vcat(map(i ->i * ones(Int64, size(reorientation_mats[i], 1)), 1:length(reorientation_mats))...)
  168. # Map each linear time index to an experiment colum
  169. colindvec = vcat(map(i -> 1:size(reorientation_mats[i], 1), 1:length(reorientation_mats))...)
  170. # Map each index in the fold to a partition
  171. partindvec = [ones(Int, l); [i*ones(Int, l - c) for i = 2:np]...]
  172. # Map each index in the folds to a column in a given partition
  173. partcolindvec = [1:l; [1:(l - c) for i = 2:np]...]
  174. # A note on indexing: the ith column of partition j in the splits is mapped to experiment expindvec[partitionInds[j][i]] and row within that experiment colindvec[partitionInds[j][i]]
  175. # Build the wavelet vectors in each fold
  176. X1 = zeros(size(reorientation_mats[1], 2), l)
  177. for (ind, i) in enumerate(partitionInds[1])
  178. X1[:, ind] = reorientation_mats[expindvec[i]][colindvec[i], :]
  179. end
  180. # Choose the overlap points randomly from the first fold
  181. cinds = randperm(l)[1:c]
  182. Xc = X1[:, cinds]
  183. Xps = [X1, ]
  184. # Loop to extract each of the partition matrics
  185. for j = 2:np - 1
  186. Xj = zeros(size(reorientation_mats[1], 2), l)
  187. for (ind, i) in enumerate(partitionInds[j])
  188. Xj[:, ind] = reorientation_mats[expindvec[i]][colindvec[i], :]
  189. end
  190. # Append the overlap region
  191. Xj[:, l - c + 1:end] .= Xc
  192. push!(Xps, Xj)
  193. end
  194. # Build the possibly small excess set of points
  195. Xj = zeros(size(reorientation_mats[1], 2), length(partitionInds[np]) + c)
  196. for (ind, i) in enumerate(partitionInds[np])
  197. Xj[:, ind] = reorientation_mats[expindvec[i]][colindvec[i], :]
  198. end
  199. # Append the overlap region
  200. Xj[:, length(partitionInds[np]) + 1:end] .= Xc
  201. push!(Xps, Xj)
  202. ## Calculate the pairwise distance matrices
  203. Dps = similar(Xps)
  204. @time for i = 1:np
  205. Dps[i] = pairwise(SqEuclidean(1e-8), Xps[i])
  206. println("Evaluated distance matrix $(i) / $(np)")
  207. end
  208. # Calculate the MDS on each split
  209. λs = Vector{Float64}[]
  210. Vs = Matrix{Float64}[]
  211. Xs = Matrix{Float64}[]
  212. # With the current set up this takes about 8 minutes to run! Increase the number of folds to speed up
  213. @time for i = 1:np
  214. # Current MDS Matrix
  215. D2 = Dps[i]
  216. # Calculate the Gram matrix
  217. G = 0.5 * (-D2 .+ mean(D2, dims = 1) .+ mean(D2, dims = 2) .- mean(D2))
  218. # Eigen decomposition of the Gram matrix
  219. E = eigen(Symmetric(G))
  220. # Make sure the eigenvalue are ordered appropriately
  221. sp = sortperm(E.values, rev = true)
  222. λ = E.values[sp]
  223. # Flip if necessary
  224. if abs(λ[end]) > abs(λ[1])
  225. λ = -λ[end:-1:1]
  226. sp = reverse(sp)
  227. end
  228. # We keep save first 5 MDS coordinates
  229. V = E.vectors[:, sp[1:5]]
  230. X = V' .* sqrt.(λ[1:5])
  231. push!(λs, λ)
  232. push!(Vs, V)
  233. push!(Xs, X)
  234. println("Evaluated MDS $(i) / $(np)")
  235. end
  236. # Plot the variance explained
  237. fig = Figure()
  238. ax = Axis(fig[1, 1])
  239. for i = 1:np
  240. scatter!(ax, cumsum(abs2.(λs[i]) ./ sum(abs2, λs[i]))[1:14], color = plotGray)
  241. end
  242. ax.xticks = 1:14
  243. xlims!(ax, 0.5, 14.5)
  244. lines!(ax, [0.5, 14.5], [0.9, 0.9], color = plotGray, linestyle = :dash)
  245. lines!(ax, [0.5, 14.5], [0.95, 0.95], color = plotGray, linestyle = :dash)
  246. lines!(ax, [0.5, 14.5], [0.99, 0.99], color = plotGray, linestyle = :dash)
  247. mλ = mean(λs[1:end - 1])
  248. scatter!(ax, cumsum(abs2.(mλ) ./ sum(abs2, mλ)), color = plotBlue, markersize = 25)
  249. ax.xlabel = "Number of dimensions"
  250. ax.ylabel = "Variance explained"
  251. #save("/Users/alaasdairhastewell/Dropbox (MIT)/Research/Pterosperma/Pterosperma Plots/all_attactor_dimension.pdf", fig)
  252. display(fig)
  253. # Align the seperate MDS results
  254. Xtarget = Xs[1][:, cinds]' # X values from
  255. mTarget = mean(Xtarget, dims = 1)
  256. sclXtarget = Xtarget .- mTarget
  257. Xs_aligned = [Xs[1], ]
  258. for i = 2:np
  259. Xcur = Xs[i][:, size(Xs[i], 2) - c + 1:end]'
  260. mCur = mean(Xcur, dims = 1)
  261. #sclXcur = Xcur .- mCur
  262. U, S, V = svd(sclXtarget'*Xcur)
  263. rotMat = V*U'
  264. transVec = mTarget[:] - rotMat' * mCur[:]
  265. Xtrans = Xs[i][:, 1:size(Xs[i], 2) - c]'
  266. XcurAligned = Matrix((Xtrans * rotMat .+ transVec')')
  267. push!(Xs_aligned, XcurAligned)
  268. @show sqrt(mean(abs2, (Xcur * rotMat .+ transVec') - Xtarget)) / sqrt(mean(abs2, Xtarget))
  269. end
  270. # Get the MDS coordinates for each experiment
  271. globalMDScoords = Matrix{Float64}[]
  272. for expIndCur = 1:length(reorientation_mats)
  273. tmp = zeros(size(Xs_aligned[1], 1), size(reorientation_mats[expIndCur], 1))
  274. for (ind, partinds) in enumerate(partitionInds)
  275. for (ind2, i) in enumerate(partinds)
  276. if expindvec[i] == expIndCur
  277. tmp[:, colindvec[i]] .= Xs_aligned[ind][:, ind2]
  278. end
  279. end
  280. end
  281. push!(globalMDScoords, tmp)
  282. end
  283. ## Color by MDS split
  284. using GLMakie; GLMakie.activate!()
  285. pltaz = 0.4809765624999961
  286. pltel = 0.32449374999999997
  287. #CairoMakie.activate!()
  288. fig = Figure(size = (800, 500), fontsize = 15)
  289. ax = Axis3(fig[1, 1], aspect = :data, xgridvisible = false, zgridvisible = false, ygridvisible = false)
  290. for i = 1:np
  291. scatter!(ax, -Xs_aligned[i][1, :], -Xs_aligned[i][2, :], -Xs_aligned[i][3, :], color = cmaps[[1, 5, 9][i]][end], markersize = 2)
  292. end
  293. Legend(fig[1, 2], [MarkerElement(color = cmaps[1][end], markersize = 15, marker = :circle), MarkerElement(color = cmaps[5][end], markersize = 15, marker = :circle), MarkerElement(color = cmaps[9][end], markersize = 15, marker = :circle)], ["Split 1", "Split 2", "Split 3"], framevisible = false)
  294. ax.xlabel = "MDS 1"
  295. ax.ylabel = "MDS 2"
  296. ax.zlabel = "MDS 3"
  297. ax.elevation = pltel
  298. ax.azimuth = pltaz
  299. #save("MDS_split.pdf", fig)
  300. fig
  301. ## Color by wavenumber
  302. fig = Figure(size = (800, 500), fontsize = 15)
  303. ax = Axis3(fig[1, 1], aspect = :data, xgridvisible = false, zgridvisible = false, ygridvisible = false)
  304. for i = 1:length(globalMDScoords)
  305. scatter!(ax, -globalMDScoords[i][1, :], -globalMDScoords[i][2, :], -globalMDScoords[i][3, :], color = nonstat_disptrajs[i][1][crplen + 1:end - crplen], markersize = 2, colorrange = (0.05, 0.2), colormap = :bamako)
  306. end
  307. Colorbar(fig[1, 2], colorrange = (0.025, 0.075), label = "Wavenumber μm⁻¹", colormap = :bamako, height = Relative(0.5))
  308. ax.xlabel = "MDS 1"
  309. ax.ylabel = "MDS 2"
  310. ax.zlabel = "MDS 3"
  311. ax.elevation = pltel
  312. ax.azimuth = pltaz
  313. #save("MDS_wavenumber.pdf", fig)
  314. fig
  315. ## Color by the frequency
  316. fig = Figure(size = (800, 500), fontsize = 15)
  317. ax = Axis3(fig[1, 1], aspect = :data, xgridvisible = false, zgridvisible = false, ygridvisible = false)
  318. for i = 1:length(globalMDScoords)
  319. scatter!(ax, -globalMDScoords[i][1, :], -globalMDScoords[i][2, :], -globalMDScoords[i][3, :], color = nonstat_disptrajs[i][2][crplen + 1:end - crplen], markersize = 2, colorrange = (6, 125), colormap = :batlow)
  320. end
  321. Colorbar(fig[1, 2], colorrange = (6, 125), label = "Frequency (Hz)", colormap = :batlow, height = Relative(0.5))
  322. ax.xlabel = "MDS 1"
  323. ax.ylabel = "MDS 2"
  324. ax.zlabel = "MDS 3"
  325. ax.elevation = pltel
  326. ax.azimuth = pltaz
  327. #save("MDS_frequency.pdf", fig)
  328. fig
  329. fig
  330. ## Color by MDS4
  331. fig = Figure(size = (800, 500), fontsize = 15)
  332. ax = Axis3(fig[1, 1], aspect = :data, xgridvisible = false, zgridvisible = false, ygridvisible = false)
  333. for i = 1:length(globalMDScoords)
  334. scatter!(ax, -globalMDScoords[i][1, :], -globalMDScoords[i][2, :], -globalMDScoords[i][3, :], color = globalMDScoords[i][4, :], markersize = 2, colorrange = (-0.075, 0.075), colormap = :bam)
  335. end
  336. Colorbar(fig[1, 2], colorrange = (-0.075, 0.075), label = "MDS 4", colormap = :bam, height = Relative(0.5))
  337. ax.xlabel = "MDS 1"
  338. ax.ylabel = "MDS 2"
  339. ax.zlabel = "MDS 3"
  340. ax.elevation = pltel
  341. ax.azimuth = pltaz
  342. #save("MDS_mds4.pdf", fig)
  343. fig
  344. ##
  345. # Equal count Histogram
  346. num_per_bin = 1000
  347. indata = vcat(getindex.(nonstat_disptrajs, 1)...)
  348. sp = sortperm(indata)
  349. edgeinds = sp[1:num_per_bin:end]
  350. binedges = indata[edgeinds]
  351. binheights = (num_per_bin / length(sp)) ./ diff(binedges)
  352. if (sp[1:num_per_bin:end])[end] != sp[end]
  353. tmpcount = length(sp) - findfirst(==(edgeinds[end]), sp)
  354. push!(binedges, indata[sp[end]])
  355. push!(binheights, (tmpcount / length(sp)) / (binedges[end] - binedges[end - 1]))
  356. end
  357. fig = Figure()
  358. ax = Axis(fig[1, 1])
  359. barplot!(ax, (binedges[1:end - 1] .+ binedges[2:end])/2, binheights, strokewidth = 0, gap = 0, width = (binedges[2:end] .- binedges[1:end - 1]), color = plotGray)
  360. ylims!(ax, 0.0, 20.0)
  361. xlims!(ax, 0.0, 0.55)
  362. ax.xlabel = "Wavenumber μm⁻¹"
  363. ax.ylabel = "PDF"
  364. #save("wavenumer_histogram.pdf", fig)
  365. bincenters_wavenumber = (binedges[1:end - 1] + binedges[2:end])/2
  366. binheights_wavenumber = copy(binheights)
  367. fig
  368. ##
  369. # Equal count Histogram
  370. num_per_bin = 1000
  371. indata = vcat(getindex.(nonstat_disptrajs, 2)...)
  372. sp = sortperm(indata)
  373. edgeinds = sp[1:num_per_bin:end]
  374. binedges = indata[edgeinds]
  375. binheights = (num_per_bin / length(sp)) ./ diff(binedges)
  376. if (sp[1:num_per_bin:end])[end] != sp[end]
  377. tmpcount = length(sp) - findfirst(==(edgeinds[end]), sp)
  378. push!(binedges, indata[sp[end]])
  379. push!(binheights, (tmpcount / length(sp)) / (binedges[end] - binedges[end - 1]))
  380. end
  381. fig = Figure()
  382. ax = Axis(fig[1, 1])
  383. barplot!(ax, (binedges[1:end - 1] .+ binedges[2:end])/2, binheights, strokewidth = 0, gap = 0, width = (binedges[2:end] .- binedges[1:end - 1]), color = plotGray)
  384. ylims!(ax, 0.0, 0.025)
  385. xlims!(ax, 0.0, 245)
  386. ax.xlabel = "Frequency Hz"
  387. ax.ylabel = "PDF"
  388. bincenters_frequency = (binedges[1:end - 1] + binedges[2:end])/2
  389. binheights_frequency = copy(binheights)
  390. #save("frequency_histogram.pdf", fig)
  391. fig
  392. ##
  393. hist_savemat = [reshape(["Freq bin center", "Freq bin count", "Wavenum bin center", "Wavenum bin count"], 1, :); [bincenters_frequency binheights_frequency bincenters_wavenumber binheights_wavenumber]]
  394. writedlm("dispersion_trajectory_histograms_$(dispkind)_$(today()).csv", hist_savemat, ',')
  395. mds_savemat
  396. labels = ["Experiment"; "Frame"; ("MDS" .* string.(1:5))...; "partition"; "softmax_ks"; "softmax_fs"; "mean_ks"; "mean_fs"]
  397. disptrajmat = [vcat(map(x -> x[1][crplen + 1:end - crplen], nonstat_disptrajs_softmax)...) vcat(map(x -> x[2][crplen + 1:end - crplen], nonstat_disptrajs_softmax)...) vcat(map(x -> x[1][crplen + 1:end - crplen], nonstat_disptrajs_mean)...) vcat(map(x -> x[2][crplen + 1:end - crplen], nonstat_disptrajs_mean)...)]
  398. exppartvec = zeros(size(disptrajmat, 1))
  399. exppartvec[partitionInds[1]] .= 1
  400. exppartvec[partitionInds[2]] .= 2
  401. exppartvec[partitionInds[3]] .= 3
  402. mds_savemat = [reshape(labels, 1, :); [nonstat_data.datasetnames[expindvec] colindvec vcat(globalMDScoords'...) exppartvec disptrajmat]]
  403. writedlm("mds_variance_explained_$(today()).csv", cumsum(abs2.(mλ) ./ sum(abs2, mλ)), ',')
  404. writedlm("mds_coordinates_with_metadata_$(today()).csv", mds_savemat, ',')

wavelet_analysis.jl, under CC-BY-4.0 · at the source

Overview

  1. Living Systems Institute & Department of Mathematics and Statistics, University of Exeter, Exeter, UK
  2. NSF-Simons National Institute for Theory and Mathematics in Biology, Chicago, IL, USA
  3. Department of Mathematics, Massachusetts Institute of Technology, Cambridge, MA USA
Institutions: University of Exeter (United Kingdom); Massachusetts Institute of Technology (United States)
Journal: Nature communications, volume 17, issue 1, article 8445
Dates: received 24 December 2025; accepted 19 June 2026; published online 8 July 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1038/s41467-026-75076-8 · PMID 42420268 · PMCID PMC13478290 · OpenAlex W4413470643
Open access: gold, a free copy (OpenAlex)
Status: code verified
Methods: Spectral & time-frequency
Keywords: Biological physics, Cell migration
MeSH: Cilia*, Movement (* major topic)
Topic: Protist diversity and phylogeny (Molecular Biology, Biochemistry, Genetics and Molecular Biology), according to OpenAlex
Funding: EC | Horizon 2020 Framework Programme (853560); Schmidt Sciences, LLC (Polymath award) MIT MathWorks Professorship Fund; NSF-Simons National Institute for Theory and Mathematics in Biology (NITMB) Fellowship supported via grants from the NSF
Citations: cited by 1 paper (Europe PMC); 74 references in the paper

Abstract

Most animals coordinate behaviour using neural computations. Yet, single-celled organisms also exhibit stimulus-responsive, even cognitive, actions. To understand how a single cell can coordinate and drive complex behaviours without any neural encoding, we study an algal protist – a motile cell with four extremely long cilia. The organism displays a surprisingly rich locomotor repertoire, emerging from the intricate dynamics of the cilia, which form a tight bundle when swimming. We use high-speed quantitative live imaging to extract the spectrum of possible ciliary beating patterns and derive a dispersion relation coupling the temporal frequency and spatial wavelength of cilia oscillations. We further reconstruct the manifold embedded in the behavioural space, showing that despite the range and complexity of ciliary beating modes, the underlying behavioural manifold is intrinsically low-dimensional with non-trivial topological structure. Dynamic transitions in motility patterns are encoded as trajectories in this space, thus reducing the macroscopic behavioural states to the underlying microscopic dynamics of the cilia.

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

Repository

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

figshare 29941784

License: CC-BY-4.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Languages: Julia (6), Python (1)
Size: 14 files, 7 scripts
Software Heritage: not checked
Found in: “Code availability”
Holds: README
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Makie (2 files), h5py (1 file), Matplotlib (1 file), NumPy (1 file), pandas (1 file), SciPy (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
8 files
At the source:

Code availability

Custom tracking and analysis codes associated with this manuscript are found at 10.6084/m9.figshare.29941784.

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

Tracing map

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

What the map holds:

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

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

Data

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

Data availability

All datasets necessary to reproduce the work detailed in this manuscript are found at 10.6084/m9.figshare.29941784. Source Data associated with all main and Supplementary Figs. are provided with this paper.

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

Versions

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

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 4 authors, 2 keywords, 2 MeSH terms, 3 funders, 55 references.

Cite

This paper

Boggon, A. K., Hastewell, A. D., Dunkel, J., & Wan, K. Y. (2026). Embodied behavioural complexity in a ciliated microorganism. Nature communications, 17(1), 8445. https://doi.org/10.1038/s41467-026-75076-8

BibTeX

@article{boggon2026embodied,
author = {Boggon, Alexander K and Hastewell, Alasdair D and Dunkel, Jörn and Wan, Kirsty Y},
title = {{Embodied behavioural complexity in a ciliated microorganism}},
journal = {Nature communications},
year = {2026},
month = jul,
volume = {17},
number = {1},
pages = {8445},
publisher = {Nature Publishing Group},
issn = {2041-1723},
doi = {10.1038/s41467-026-75076-8},
url = {https://doi.org/10.1038/s41467-026-75076-8},
pmid = {42420268},
pmcid = {PMC13478290}
}

RIS

TY - JOUR
AU - Boggon, Alexander K
AU - Hastewell, Alasdair D
AU - Dunkel, Jörn
AU - Wan, Kirsty Y
TI - Embodied behavioural complexity in a ciliated microorganism
T2 - Nature communications
J2 - Nat Commun
PY - 2026
DA - 2026/07/08
VL - 17
IS - 1
SP - 8445
SN - 2041-1723
PB - Nature Publishing Group
DO - 10.1038/s41467-026-75076-8
UR - https://doi.org/10.1038/s41467-026-75076-8
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41467-026-75076-8",
"type": "article-journal",
"title": "Embodied behavioural complexity in a ciliated microorganism",
"container-title": "Nature communications",
"author": [
{
"family": "Boggon",
"given": "Alexander K"
},
{
"family": "Hastewell",
"given": "Alasdair D"
},
{
"family": "Dunkel",
"given": "Jörn"
},
{
"family": "Wan",
"given": "Kirsty Y"
}
],
"container-title-short": "Nat Commun",
"volume": "17",
"issue": "1",
"page": "8445",
"DOI": "10.1038/s41467-026-75076-8",
"PMID": "42420268",
"PMCID": "PMC13478290",
"ISSN": "2041-1723",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41467-026-75076-8",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
8
]
]
}
}

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

Similar papers

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

[1] doi:10.1126/sciadv.adv3770 [code]
Evolution of a central dopamine circuit underlies adaptation of a light-evoked sensorimotor response in the blind cavefish.
Journal: Science advances
In common: h5py, pandas, SciPy, 2 other tools, 1 reference
[2] doi:10.1162/imag.a.1341 [code]
Massively parallelized brain tractography using compute clusters, supercomputers, and graphics processing units.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: Makie, pandas, SciPy, 2 other tools
[3] doi:10.1038/s41593-026-02342-9 [code]
Interpretable abstractions of artificial neural networks predict behavior and neural activity during human information gathering.
Journal: Nature neuroscience
In common: Makie, pandas, SciPy, 2 other tools
[4] doi:10.1002/mrm.70378 [code]
Investigating the Sensitivity of the Diffusion MRI Signal to Magnetization Transfer and Permeability via Monte-Carlo Simulations.
Journal: Magnetic resonance in medicine
In common: Makie, pandas, SciPy, 2 other tools
[5] doi:10.7554/elife.89629 [code]
Active dendrites enable robust spiking computations despite timing jitter.
Journal: eLife
In common: Makie, pandas, Matplotlib, 1 other tool
[6] doi:10.1038/s41467-026-75352-7 [code]
Mechanosensory encoding of surface mechanics optimizes locomotion.
Journal: Nature communications
In common: pandas, SciPy, Matplotlib, 1 other tool, 1 reference
[7] doi:10.1038/s41467-026-76522-3 [code]
Transcriptomic analysis of spinal V1 interneurons informs their multifunctional role in motor output.
Journal: Nature communications
In common: pandas, SciPy, Matplotlib, 1 other tool, 1 reference
[8] doi:10.1002/advs.77986 [code]
DUET-seq: An Open-Source Droplet Platform for High-Fidelity Joint Chromatin and Transcriptome Profiling Reveals Temporal Regulatory Decoupling in Single Cells.
Journal: Advanced science (Weinheim, Baden-Wurttemberg, Germany)
In common: h5py, pandas, SciPy, 2 other tools
[9] doi:10.7554/elife.110588 [code]
Opening the black box toward a modular approach to spike sorting.
Journal: eLife
In common: h5py, pandas, SciPy, 2 other tools
[10] doi: [code]
Real-time closed-loop feedback system for mouse mesoscale cortical signal and movement control
Journal: eLife
In common: h5py, pandas, SciPy, 2 other tools

Contribute

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

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

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.