Embodied behavioural complexity in a ciliated microorganism.
The 4 matches
- [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] § 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] § Methods › Wavelet analysis and multidimensional scaling analysis ↔ wavelet_analysis.jl, lines 128–187 · score 0.55 · dispersion heatmap, wavelet transform, multidimensional
- [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
- cd(@__DIR__); import Pkg; Pkg.activate("")
- # Load relevant packages
- using LinearAlgebra, Statistics
- using ClassicalOrthogonalPolynomials, FFTW, RecurrenceRelationships
- using Optim
- using DelimitedFiles
- using ProgressMeter
- using CairoMakie, GLMakie; CairoMakie.activate!()
- using Roots
- using Metaheuristics
- using FFTW
- using Distances, Clustering, MultivariateStats
- import TOML
- include("chebyshev.jl")
- include("utils.jl")
- include("wavelet.jl")
- include("plotting.jl")
- ## Analysis parameters
- cwtfreqs = 2.0 .^LinRange(log2(5), log2(300), 101) # Log spaced frequencies to evaluate the wavelet transform at
- f = 1.0 # Morelet scale factor
- max_chebind = 20 # Maximum chebyshev index
- crplen = 20 # Length to crop wavelet transform at the boundaries
- # Experiments to pick out as examples
- target_names = ["F26(6)", "F20(4)", "F20(29)", "F20(16)", "F20(20)", "F20(25)", "F15(16)", "F7(4)", "F46(71)", "F27(4)", "F15(30)"]
- ##
- # Read in the data
- all_data = read_coefficients("LocalDataPath.toml")
- # Get all the nonstationary datasets
- nonstatInds = unique(push!(findall(.!in(["Stationary"]), all_data.datasettypes), findall(all_data.datasetnames .∈ (target_names, ))...))
- nonstat_data = all_data[nonstatInds]
- target_data = nonstat_data[target_names]
- # Get all the stationary datasets
- statInds = findall(in(["Stationary"]), all_data.datasettypes)
- stat_data = all_data[statInds]
- # Evaluate the wavelet transform
- nonstat_wavelets = CiliaWaveletTransforms(nonstat_data, cwtfreqs, f; max_cwtind = max_chebind, fps = 1000, zp = true)
- target_wavelets = CiliaWaveletTransforms(target_data, cwtfreqs, f; max_cwtind = max_chebind, fps = 1000, zp = true)
- ## Calculate the disperion plot
- cwtfreqs = nonstat_wavelets[1].cwtfreqs
- dispmat = Matrix{Float64}(undef, length(cwtfreqs), max_chebind)
- for i = 2:max_chebind + 1
- 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)[:]
- end
- # normalized version of the plot
- normdispmat = dispmat ./ sqrt.(sum(abs2, dispmat, dims = 1))
- # Calculate the Chebyshev wavenumber
- mL = Statistics.median(vcat(nonstat_data.Ls...))
- 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]]
- chebwavnum = 2*pi ./ chebwavlen
- # Get the trajectories on the wavelet transform
- dispkind = "softmax"
- if dispkind === "softmax"
- target_disptrajs = dispersion_trajectory_softmax.(target_wavelets, (chebwavnum, ), filtlen = 50)
- nonstat_disptrajs = dispersion_trajectory_softmax.(nonstat_wavelets, (chebwavnum, ), filtlen = 50)
- elseif dispkind === "mean"
- target_disptrajs = dispersion_trajectory.(target_wavelets, (chebwavnum, ))
- nonstat_disptrajs = dispersion_trajectory.(nonstat_wavelets, (chebwavnum, ))
- else
- error("Oops")
- end
- target_disptrajs_mean = dispersion_trajectory.(target_wavelets, (chebwavnum, ))
- nonstat_disptrajs_mean = dispersion_trajectory.(nonstat_wavelets, (chebwavnum, ))
- target_disptrajs_softmax = dispersion_trajectory_softmax.(target_wavelets, (chebwavnum, ), filtlen = 50)
- nonstat_disptrajs_softmax = dispersion_trajectory_softmax.(nonstat_wavelets, (chebwavnum, ), filtlen = 50)
- #target_disptrajs = dispersion_trajectory.(target_wavelets, (chebwavnum, ))
- #nonstat_disptrajs = dispersion_trajectory.(nonstat_wavelets, (chebwavnum, ))
- #= Uncomment if you want to save the analysis results
- # Save the results
- savelength = maximum(length.(getindex.(target_disptrajs, 1)))
- for i = 1:length(target_dispfs)
- push!(target_disptrajs[i][1], NaN*ones(savelength - length(target_disptrajs[i][1]))...)
- push!(target_disptrajs[i][2], NaN*ones(savelength - length(target_disptrajs[i][1]))...)
- end
- 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, :)], ',')
- writedlm("dispersion_04_08_25.csv", [["freq (Hz) / chebwavnum (um^-1)"; nonstat_wavelets.cwtfreqs] [chebwavnum'; dispmat]], ',')
- =#
- ##
- # Plot the result for the dispersion subsamples
- fig = Figure(size = (1000, 480), fontsize = 24)
- # Plot the dispersion heatmap
- LOGAX = false; LOGCB = false
- if LOGAX
- ax = Axis(fig[1, 1], aspect = AxisAspect(1), yscale = log10); ax.xlabel = "k (μm⁻¹)"; ax.ylabel = "f (Hz)" # yscale = log10
- else
- ax = Axis(fig[1, 1], aspect = AxisAspect(1)); ax.xlabel = "k (μm⁻¹)"; ax.ylabel = "f (Hz)" # yscale = log10
- end
- if LOGCB
- crange = (-2.35, -0.5)
- pltmat = log10.(normdispmat')
- hmp = heatmap!(ax, chebwavnum, nonstat_wavelets.cwtfreqs, pltmat, colormap = :nuuk, colorrange = crange)
- cb = Colorbar(fig[1, 2], hmp, label = "Log10 Normalized power")
- else
- crange = (0.0, 0.3)
- pltmat = normdispmat'
- hmp = heatmap!(ax, chebwavnum, nonstat_wavelets.cwtfreqs, pltmat, colormap = :nuuk, colorrange = crange)
- cb = Colorbar(fig[1, 2], hmp, label = "Normalized power")
- end
- # Reconstructions
- xs = LinRange(-1, 1, 1001)
- V = zeros(length(xs), 126); eval_vandermonde!(V, xs)
- for i = 1:9
- dispks = target_disptrajs[i][1]
- dispfs = target_disptrajs[i][2]
- crange = (1, size(target_data[i].allcfsx, 1))
- lines!(ax, dispks, dispfs, colormap = cmaps[i][2:end], color = 1:length(dispks), linewidth = 1.5, colorrange = crange)
- xrecon = V[:, 2:end] * target_data[i].allcfsx[:, 2:end]'
- yrecon = V[:, 2:end] * target_data[i].allcfsy[:, 2:end]'
- axtmp = Axis(fig[1, 3][divrem(i - 1, 3) .+ 1...], aspect = DataAspect()); hidedecorations!(axtmp)
- ylims!(axtmp, -100, 100); xlims!(axtmp, -100, 100)
- nplt = div(size(target_data[i].allcfsx, 1), 25) # Plot every 25 frames
- for j = size(target_data[i].allcfsx, 1):-nplt:1
- lines!(axtmp, Point2.(xrecon[:, j], yrecon[:, j]), color = j, colormap = cmaps[i][2:end], colorrange = crange, linewidth = 0.5)
- end
- end
- display(fig)
- ## Plot the results corresponding to the SI movies
- # Plot the result for the dispersion subsamples
- fig = Figure(size = (1000, 480), fontsize = 24)
- # Plot the dispersion heatmap
- ax = Axis(fig[1, 1], aspect = AxisAspect(1), yscale = log10); ax.xlabel = "k (μm⁻¹)"; ax.ylabel = "f (Hz)" # yscale = log10
- hmp = heatmap!(ax, chebwavnum, nonstat_wavelets.cwtfreqs, normdispmat', colormap = :nuuk, colorrange = (0, 0.3))
- cb = Colorbar(fig[1, 2], hmp, label = "Normalized power")
- # Reconstructions
- xs = LinRange(-1, 1, 1001)
- V = zeros(length(xs), 126); eval_vandermonde!(V, xs)
- cmapind = [1, 6]
- for i2 = 1:2
- i = i2 + 9
- dispks = target_disptrajs[i][1]
- dispfs = target_disptrajs[i][2]
- crange = (1, size(target_data[i].allcfsx, 1))
- lines!(ax, dispks, dispfs, colormap = cmaps[cmapind[i2]][2:end], color = 1:length(dispks), linewidth = 1.5, colorrange = crange)
- xrecon = V[:, 2:end] * target_data[i].allcfsx[:, 2:end]'
- yrecon = V[:, 2:end] * target_data[i].allcfsy[:, 2:end]'
- axtmp = Axis(fig[1, 3][i2, 1], aspect = DataAspect()); hidedecorations!(axtmp)
- ylims!(axtmp, -100, 100); xlims!(axtmp, -100, 100)
- nplt = div(size(target_data[i].allcfsx, 1), 25) # Plot every 25 frames
- for j = size(target_data[i].allcfsx, 1):-nplt:1
- lines!(axtmp, Point2.(xrecon[:, j], yrecon[:, j]), color = j, colormap = cmaps[cmapind[i2]][2:end], colorrange = crange, linewidth = 0.5)
- end
- end
- display(fig)
- ## Multidimensional scaling
- reorientation_mats = [hcat([cwav.abs_wavelet_transform[crplen + 1:end - crplen, :, i] for i = 2:max_chebind + 1]...) for cwav in nonstat_wavelets]
- for rmat = reorientation_mats
- rmat ./= sum(rmat, dims = 2)
- end
- # Split the data and perform MDS on each split
- using Random; Random.seed!(123456)
- # Total number of time points
- n = sum(size.(reorientation_mats, 1))
- # Map each time point to a unique linear time index (index of coli in expi = offset[i] + coli)
- offsets = [0; cumsum(size.(reorientation_mats, 1))]
- # Overlap
- c = 1666
- # Number of independent MSDs
- nfolds = 3
- m, r = divrem(n - c, nfolds)
- l = m + c
- # Maybe there is a small fold is not a perfect divisor
- np = ceil(Int, 1 + (n - l)/(l - c))
- # Generate a random permutation of the data
- rp = randperm(n)
- # Get the columns in each fold
- 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]]
- # Map each linear time index to an experiment ind
- expindvec = vcat(map(i ->i * ones(Int64, size(reorientation_mats[i], 1)), 1:length(reorientation_mats))...)
- # Map each linear time index to an experiment colum
- colindvec = vcat(map(i -> 1:size(reorientation_mats[i], 1), 1:length(reorientation_mats))...)
- # Map each index in the fold to a partition
- partindvec = [ones(Int, l); [i*ones(Int, l - c) for i = 2:np]...]
- # Map each index in the folds to a column in a given partition
- partcolindvec = [1:l; [1:(l - c) for i = 2:np]...]
- # 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]]
- # Build the wavelet vectors in each fold
- X1 = zeros(size(reorientation_mats[1], 2), l)
- for (ind, i) in enumerate(partitionInds[1])
- X1[:, ind] = reorientation_mats[expindvec[i]][colindvec[i], :]
- end
- # Choose the overlap points randomly from the first fold
- cinds = randperm(l)[1:c]
- Xc = X1[:, cinds]
- Xps = [X1, ]
- # Loop to extract each of the partition matrics
- for j = 2:np - 1
- Xj = zeros(size(reorientation_mats[1], 2), l)
- for (ind, i) in enumerate(partitionInds[j])
- Xj[:, ind] = reorientation_mats[expindvec[i]][colindvec[i], :]
- end
- # Append the overlap region
- Xj[:, l - c + 1:end] .= Xc
- push!(Xps, Xj)
- end
- # Build the possibly small excess set of points
- Xj = zeros(size(reorientation_mats[1], 2), length(partitionInds[np]) + c)
- for (ind, i) in enumerate(partitionInds[np])
- Xj[:, ind] = reorientation_mats[expindvec[i]][colindvec[i], :]
- end
- # Append the overlap region
- Xj[:, length(partitionInds[np]) + 1:end] .= Xc
- push!(Xps, Xj)
- ## Calculate the pairwise distance matrices
- Dps = similar(Xps)
- @time for i = 1:np
- Dps[i] = pairwise(SqEuclidean(1e-8), Xps[i])
- println("Evaluated distance matrix $(i) / $(np)")
- end
- # Calculate the MDS on each split
- λs = Vector{Float64}[]
- Vs = Matrix{Float64}[]
- Xs = Matrix{Float64}[]
- # With the current set up this takes about 8 minutes to run! Increase the number of folds to speed up
- @time for i = 1:np
- # Current MDS Matrix
- D2 = Dps[i]
- # Calculate the Gram matrix
- G = 0.5 * (-D2 .+ mean(D2, dims = 1) .+ mean(D2, dims = 2) .- mean(D2))
- # Eigen decomposition of the Gram matrix
- E = eigen(Symmetric(G))
- # Make sure the eigenvalue are ordered appropriately
- sp = sortperm(E.values, rev = true)
- λ = E.values[sp]
- # Flip if necessary
- if abs(λ[end]) > abs(λ[1])
- λ = -λ[end:-1:1]
- sp = reverse(sp)
- end
- # We keep save first 5 MDS coordinates
- V = E.vectors[:, sp[1:5]]
- X = V' .* sqrt.(λ[1:5])
- push!(λs, λ)
- push!(Vs, V)
- push!(Xs, X)
- println("Evaluated MDS $(i) / $(np)")
- end
- # Plot the variance explained
- fig = Figure()
- ax = Axis(fig[1, 1])
- for i = 1:np
- scatter!(ax, cumsum(abs2.(λs[i]) ./ sum(abs2, λs[i]))[1:14], color = plotGray)
- end
- ax.xticks = 1:14
- xlims!(ax, 0.5, 14.5)
- lines!(ax, [0.5, 14.5], [0.9, 0.9], color = plotGray, linestyle = :dash)
- lines!(ax, [0.5, 14.5], [0.95, 0.95], color = plotGray, linestyle = :dash)
- lines!(ax, [0.5, 14.5], [0.99, 0.99], color = plotGray, linestyle = :dash)
- mλ = mean(λs[1:end - 1])
- scatter!(ax, cumsum(abs2.(mλ) ./ sum(abs2, mλ)), color = plotBlue, markersize = 25)
- ax.xlabel = "Number of dimensions"
- ax.ylabel = "Variance explained"
- #save("/Users/alaasdairhastewell/Dropbox (MIT)/Research/Pterosperma/Pterosperma Plots/all_attactor_dimension.pdf", fig)
- display(fig)
- # Align the seperate MDS results
- Xtarget = Xs[1][:, cinds]' # X values from
- mTarget = mean(Xtarget, dims = 1)
- sclXtarget = Xtarget .- mTarget
- Xs_aligned = [Xs[1], ]
- for i = 2:np
- Xcur = Xs[i][:, size(Xs[i], 2) - c + 1:end]'
- mCur = mean(Xcur, dims = 1)
- #sclXcur = Xcur .- mCur
- U, S, V = svd(sclXtarget'*Xcur)
- rotMat = V*U'
- transVec = mTarget[:] - rotMat' * mCur[:]
- Xtrans = Xs[i][:, 1:size(Xs[i], 2) - c]'
- XcurAligned = Matrix((Xtrans * rotMat .+ transVec')')
- push!(Xs_aligned, XcurAligned)
- @show sqrt(mean(abs2, (Xcur * rotMat .+ transVec') - Xtarget)) / sqrt(mean(abs2, Xtarget))
- end
- # Get the MDS coordinates for each experiment
- globalMDScoords = Matrix{Float64}[]
- for expIndCur = 1:length(reorientation_mats)
- tmp = zeros(size(Xs_aligned[1], 1), size(reorientation_mats[expIndCur], 1))
- for (ind, partinds) in enumerate(partitionInds)
- for (ind2, i) in enumerate(partinds)
- if expindvec[i] == expIndCur
- tmp[:, colindvec[i]] .= Xs_aligned[ind][:, ind2]
- end
- end
- end
- push!(globalMDScoords, tmp)
- end
- ## Color by MDS split
- using GLMakie; GLMakie.activate!()
- pltaz = 0.4809765624999961
- pltel = 0.32449374999999997
- #CairoMakie.activate!()
- fig = Figure(size = (800, 500), fontsize = 15)
- ax = Axis3(fig[1, 1], aspect = :data, xgridvisible = false, zgridvisible = false, ygridvisible = false)
- for i = 1:np
- scatter!(ax, -Xs_aligned[i][1, :], -Xs_aligned[i][2, :], -Xs_aligned[i][3, :], color = cmaps[[1, 5, 9][i]][end], markersize = 2)
- end
- 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)
- ax.xlabel = "MDS 1"
- ax.ylabel = "MDS 2"
- ax.zlabel = "MDS 3"
- ax.elevation = pltel
- ax.azimuth = pltaz
- #save("MDS_split.pdf", fig)
- fig
- ## Color by wavenumber
- fig = Figure(size = (800, 500), fontsize = 15)
- ax = Axis3(fig[1, 1], aspect = :data, xgridvisible = false, zgridvisible = false, ygridvisible = false)
- for i = 1:length(globalMDScoords)
- 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)
- end
- Colorbar(fig[1, 2], colorrange = (0.025, 0.075), label = "Wavenumber μm⁻¹", colormap = :bamako, height = Relative(0.5))
- ax.xlabel = "MDS 1"
- ax.ylabel = "MDS 2"
- ax.zlabel = "MDS 3"
- ax.elevation = pltel
- ax.azimuth = pltaz
- #save("MDS_wavenumber.pdf", fig)
- fig
- ## Color by the frequency
- fig = Figure(size = (800, 500), fontsize = 15)
- ax = Axis3(fig[1, 1], aspect = :data, xgridvisible = false, zgridvisible = false, ygridvisible = false)
- for i = 1:length(globalMDScoords)
- 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)
- end
- Colorbar(fig[1, 2], colorrange = (6, 125), label = "Frequency (Hz)", colormap = :batlow, height = Relative(0.5))
- ax.xlabel = "MDS 1"
- ax.ylabel = "MDS 2"
- ax.zlabel = "MDS 3"
- ax.elevation = pltel
- ax.azimuth = pltaz
- #save("MDS_frequency.pdf", fig)
- fig
- fig
- ## Color by MDS4
- fig = Figure(size = (800, 500), fontsize = 15)
- ax = Axis3(fig[1, 1], aspect = :data, xgridvisible = false, zgridvisible = false, ygridvisible = false)
- for i = 1:length(globalMDScoords)
- scatter!(ax, -globalMDScoords[i][1, :], -globalMDScoords[i][2, :], -globalMDScoords[i][3, :], color = globalMDScoords[i][4, :], markersize = 2, colorrange = (-0.075, 0.075), colormap = :bam)
- end
- Colorbar(fig[1, 2], colorrange = (-0.075, 0.075), label = "MDS 4", colormap = :bam, height = Relative(0.5))
- ax.xlabel = "MDS 1"
- ax.ylabel = "MDS 2"
- ax.zlabel = "MDS 3"
- ax.elevation = pltel
- ax.azimuth = pltaz
- #save("MDS_mds4.pdf", fig)
- fig
- ##
- # Equal count Histogram
- num_per_bin = 1000
- indata = vcat(getindex.(nonstat_disptrajs, 1)...)
- sp = sortperm(indata)
- edgeinds = sp[1:num_per_bin:end]
- binedges = indata[edgeinds]
- binheights = (num_per_bin / length(sp)) ./ diff(binedges)
- if (sp[1:num_per_bin:end])[end] != sp[end]
- tmpcount = length(sp) - findfirst(==(edgeinds[end]), sp)
- push!(binedges, indata[sp[end]])
- push!(binheights, (tmpcount / length(sp)) / (binedges[end] - binedges[end - 1]))
- end
- fig = Figure()
- ax = Axis(fig[1, 1])
- 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)
- ylims!(ax, 0.0, 20.0)
- xlims!(ax, 0.0, 0.55)
- ax.xlabel = "Wavenumber μm⁻¹"
- ax.ylabel = "PDF"
- #save("wavenumer_histogram.pdf", fig)
- bincenters_wavenumber = (binedges[1:end - 1] + binedges[2:end])/2
- binheights_wavenumber = copy(binheights)
- fig
- ##
- # Equal count Histogram
- num_per_bin = 1000
- indata = vcat(getindex.(nonstat_disptrajs, 2)...)
- sp = sortperm(indata)
- edgeinds = sp[1:num_per_bin:end]
- binedges = indata[edgeinds]
- binheights = (num_per_bin / length(sp)) ./ diff(binedges)
- if (sp[1:num_per_bin:end])[end] != sp[end]
- tmpcount = length(sp) - findfirst(==(edgeinds[end]), sp)
- push!(binedges, indata[sp[end]])
- push!(binheights, (tmpcount / length(sp)) / (binedges[end] - binedges[end - 1]))
- end
- fig = Figure()
- ax = Axis(fig[1, 1])
- 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)
- ylims!(ax, 0.0, 0.025)
- xlims!(ax, 0.0, 245)
- ax.xlabel = "Frequency Hz"
- ax.ylabel = "PDF"
- bincenters_frequency = (binedges[1:end - 1] + binedges[2:end])/2
- binheights_frequency = copy(binheights)
- #save("frequency_histogram.pdf", fig)
- fig
- ##
- hist_savemat = [reshape(["Freq bin center", "Freq bin count", "Wavenum bin center", "Wavenum bin count"], 1, :); [bincenters_frequency binheights_frequency bincenters_wavenumber binheights_wavenumber]]
- writedlm("dispersion_trajectory_histograms_$(dispkind)_$(today()).csv", hist_savemat, ',')
- mds_savemat
- labels = ["Experiment"; "Frame"; ("MDS" .* string.(1:5))...; "partition"; "softmax_ks"; "softmax_fs"; "mean_ks"; "mean_fs"]
- 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)...)]
- exppartvec = zeros(size(disptrajmat, 1))
- exppartvec[partitionInds[1]] .= 1
- exppartvec[partitionInds[2]] .= 2
- exppartvec[partitionInds[3]] .= 3
- mds_savemat = [reshape(labels, 1, :); [nonstat_data.datasetnames[expindvec] colindvec vcat(globalMDScoords'...) exppartvec disptrajmat]]
- writedlm("mds_variance_explained_$(today()).csv", cumsum(abs2.(mλ) ./ sum(abs2, mλ)), ',')
- writedlm("mds_coordinates_with_metadata_$(today()).csv", mds_savemat, ',')
wavelet_analysis.jl, under CC-BY-4.0 · at the source
Overview
- Living Systems Institute & Department of Mathematics and Statistics, University of Exeter, Exeter, UK
- NSF-Simons National Institute for Theory and Mathematics in Biology, Chicago, IL, USA
- Department of Mathematics, Massachusetts Institute of Technology, Cambridge, MA USA
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
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
- 27 September 2026: the link answers (HTTP 200)
8 files
- Example_Codes.py, Python, 237 lines, 2 matches
- chebyshev.jl, Julia, 9 lines
- coefficients_fitting.jl, Julia, 904 lines
- plotting.jl, Julia, 34 lines
- utils.jl, Julia, 239 lines
- wavelet.jl, Julia, 190 lines
- wavelet_analysis.jl, Julia, 438 lines, 2 matches
- ReadMe.txt, Text, 86 lines
Code availability
Custom tracking and analysis codes associated with this manuscript are found at 10.6084/
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/
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://
BibTeX
@article{boggon2026embod
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/
url = {https://
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/
VL - 17
IS - 1
SP - 8445
SN - 2041-1723
PB - Nature Publishing Group
DO - 10.1038/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1038/
"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":
"volume": "17",
"issue": "1",
"page": "8445",
"DOI": "10.1038/
"PMID": "42420268",
"PMCID": "PMC13478290",
"ISSN": "2041-1723",
"publisher": "Nature Publishing Group",
"URL": "https://
"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 advancesIn 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 neuroscienceIn 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 medicineIn common: Makie, pandas, SciPy, 2 other tools
- [5] doi:10.7554/elife.89629 [code]
- Active dendrites enable robust spiking computations despite timing jitter.Journal: eLifeIn 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 communicationsIn 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 communicationsIn 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: eLifeIn common: h5py, pandas, SciPy, 2 other tools
- [10] doi: [code]
- Real-time closed-loop feedback system for mouse mesoscale cortical signal and movement controlJournal: eLifeIn 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.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 7 scripts, and 4 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:c3343f1ec6da0cd8…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[.
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.
