Alpha shape of a protein

Copy-pastable code (if you have PyCall + NumPy + SciPy set up)

using BioMakie
using GLMakie
using GLMakie: Slider
using SplitApplyCombine
using GeometryBasics
using Meshes
using BioStructures
using PyCall
using Conda
scipy = pyimport_conda("scipy", "scipy")
np = pyimport_conda("numpy", "numpy")
collections = pyimport_conda("collections", "collections")
py"""
    from scipy.spatial import Delaunay
    import numpy as np
    from collections import defaultdict
    def alpha_shape_3D(pos, alpha):
        tetra = Delaunay(pos)
        tetrapos = np.take(pos,tetra.simplices,axis=0)
        normsq = np.sum(tetrapos**2,axis=2)[:,:,None]
        ones = np.ones((tetrapos.shape[0],tetrapos.shape[1],1))
        a = np.linalg.det(np.concatenate((tetrapos,ones),axis=2))
        Dx = np.linalg.det(np.concatenate((normsq,tetrapos[:,:,[1,2]],ones),axis=2))
        Dy = -np.linalg.det(np.concatenate((normsq,tetrapos[:,:,[0,2]],ones),axis=2))
        Dz = np.linalg.det(np.concatenate((normsq,tetrapos[:,:,[0,1]],ones),axis=2))
        c = np.linalg.det(np.concatenate((normsq,tetrapos),axis=2))
        r = np.sqrt(Dx**2+Dy**2+Dz**2-4*a*c)/(2*np.abs(a))
        tetras = tetra.simplices[r<alpha,:]
        TriComb = np.array([(0, 1, 2), (0, 1, 3), (0, 2, 3), (1, 2, 3)])
        Triangles = tetras[:,TriComb].reshape(-1,3)
        Triangles = np.sort(Triangles,axis=1)
        TrianglesDict = defaultdict(int)
        for tri in Triangles:
            TrianglesDict[tuple(tri)] += 1
        Triangles=np.array([tri for tri in TrianglesDict if TrianglesDict[tri] ==1])
        EdgeComb=np.array([(0, 1), (0, 2), (1, 2)])
        Edges=Triangles[:,EdgeComb].reshape(-1,2)
        Edges=np.sort(Edges,axis=1)
        Edges=np.unique(Edges,axis=0)
        Vertices = np.unique(Edges)
        return Vertices,Edges,Triangles
    """
indexshift(idxs) = (idxs).+=1
function getalphashape(coords::Matrix, alpha::T) where {T<:Real}
    verts,edges,tris = py"alpha_shape_3D($(coords),$(alpha))"
    return [indexshift(verts),indexshift(edges),indexshift(tris)]
end
function getspherepoints(cords::Matrix, radius::Real)
	pnts = [GeometryBasics.Point{3,Float64}(cords[i,:]) for i in 1:size(cords,1)] |> Observable
	spheres = GeometryBasics.Point{3,Float64}[]
	lift(pnts) do p
		for i in 1:size(p,1)
			sp = GeometryBasics.decompose(GeometryBasics.Point{3,Float64},GeometryBasics.Sphere(p[i],radius),4) |> unique
			for ii in 1:size(sp,1)
				push!(spheres,sp[ii])
			end
		end
	end
	return [[spheres[i].data...] for i in 1:size(spheres,1)] |> combinedims |> transpose |> collect
end
function linesegs(arr::AbstractArray{T,3}) where T<:AbstractFloat
    new_arr::AbstractArray{Point3f0} = []
    for i in 1:size(arr,1)
        push!(new_arr, Makie.Point3f0(arr[i,1,:]))
        push!(new_arr, Makie.Point3f0(arr[i,2,:]))
    end
    return new_arr |> combinedims |> transpose |> collect
end
struc = retrievepdb("2vb1")
chn = struc[1]["A"] |> Observable
atms = collectatoms(struc, standardselector) |> Observable
cords = @lift coordarray($atms)' |> collect
fig = Figure(resolution = (800,600))
layout = fig[1,1] = GridLayout(10, 9)
strucname = struc.name[1:4]
sc_scene = layout[1:10,1:6] = LScene(fig; show_axis = false)
structxt = layout[1,7:8] = Label(fig, text = "Structure ID:  $(strucname)", fontsize = 35)
alpha1 = layout[5,7:9] = Slider(fig, range = 1.5:0.5:9.0, startvalue = 2.5)
alphatxt1 = lift(alpha1.value) do s1; string("alpha = ", round(s1, sigdigits = 2)); end
alphatext = layout[4,7:9] = Label(fig, text = alphatxt1, fontsize = 22)
alphaval = alpha1.value
radii1 = layout[7,7:9] = Slider(fig, range = 1.5:0.5:9.0, startvalue = 2.5)
radiixt1 = lift(radii1.value) do s1; string("atom radius = ", round(s1, sigdigits = 2)); end
radiitext = layout[6,7:9] = Label(fig, text = radiixt1, fontsize = 22)
radiival = radii1.value;
spnts = @lift getspherepoints($cords,$radiival)
proteinshape = @lift let pnts = $spnts; getalphashape(pnts,$alphaval); end
alphaverts = @lift $spnts[$(proteinshape)[1],:]
alphaedges = @lift $spnts[$(proteinshape)[2],:] |> linesegs
function surfacearea(coordinates, connectivity)
    totalarea = 0.0
    for i = 1:size(connectivity,1)
        totalarea += measure(Ngon(Meshes.Point{3,Int64}.(coordinates[connectivity[i,1],:],
                        coordinates[connectivity[i,2],:], coordinates[connectivity[i,3],:])...))
    end
    return totalarea
end
surfarea = @lift surfacearea($spnts, $(proteinshape)[3])
surfatext = layout[2,7:9] = Label(fig, text = lift(X->string("surface area = ", round(Int64, X), "  Ų"), surfarea), fontsize = 22)
linesegments!(sc_scene, alphaedges, color = :gray, transparency = true)
meshscatter!(sc_scene, cords, markersize = 0.3, color = :blue)
meshscatter!(sc_scene, alphaverts, markersize = 0.3, color = :green)

alphashape

Alpha shape walkthrough

First are the regular imports.

using BioMakie
using GLMakie
using GLMakie: Slider
using SplitApplyCombine
using GeometryBasics
using BioStructures

Special imports (Python)

SciPy and NumPy are required for this alpha shape algorithm. They need to be installed in your Conda/Python environment.

using PyCall
using Conda
scipy = pyimport("scipy")
np = pyimport("numpy")
collections = pyimport("collections")

Define the alpha shape algorithm.

py"""
    from scipy.spatial import Delaunay
    import numpy as np
    from collections import defaultdict

    def alpha_shape_3D(pos, alpha):
        tetra = Delaunay(pos)
        tetrapos = np.take(pos,tetra.simplices,axis=0)
        normsq = np.sum(tetrapos**2,axis=2)[:,:,None]
        ones = np.ones((tetrapos.shape[0],tetrapos.shape[1],1))
        a = np.linalg.det(np.concatenate((tetrapos,ones),axis=2))
        Dx = np.linalg.det(np.concatenate((normsq,tetrapos[:,:,[1,2]],ones),axis=2))
        Dy = -np.linalg.det(np.concatenate((normsq,tetrapos[:,:,[0,2]],ones),axis=2))
        Dz = np.linalg.det(np.concatenate((normsq,tetrapos[:,:,[0,1]],ones),axis=2))
        c = np.linalg.det(np.concatenate((normsq,tetrapos),axis=2))
        r = np.sqrt(Dx**2+Dy**2+Dz**2-4*a*c)/(2*np.abs(a))
        tetras = tetra.simplices[r<alpha,:]
        TriComb = np.array([(0, 1, 2), (0, 1, 3), (0, 2, 3), (1, 2, 3)])
        Triangles = tetras[:,TriComb].reshape(-1,3)
        Triangles = np.sort(Triangles,axis=1)
        TrianglesDict = defaultdict(int)
        for tri in Triangles:
            TrianglesDict[tuple(tri)] += 1
        Triangles=np.array([tri for tri in TrianglesDict if TrianglesDict[tri] ==1])
        EdgeComb=np.array([(0, 1), (0, 2), (1, 2)])
        Edges=Triangles[:,EdgeComb].reshape(-1,2)
        Edges=np.sort(Edges,axis=1)
        Edges=np.unique(Edges,axis=0)
        Vertices = np.unique(Edges)
        return Vertices,Edges,Triangles
    """

Define julia alpha shape function

Python is base 0 and Julia is base 1 so we first have to define a function to shift the indices.

indexshift(idxs) = (idxs).+=1
function getalphashape(coords::Matrix, alpha::T) where {T<:Real}
    verts,edges,tris = py"alpha_shape_3D($(coords),$(alpha))"
    return [indexshift(verts),indexshift(edges),indexshift(tris)]
end

Points from atomic radii spheres

Define a function to get points from spheres at a given radius around coordinates and a function to get line segments from a set of coordinates.

function getspherepoints(cords::Matrix, radius::Real)
	pnts = [GeometryBasics.Point{3,Float64}(cords[i,:]) for i in 1:size(cords,1)] |> Observable
	spheres = GeometryBasics.Point{3,Float64}[]

	lift(pnts) do p
		for i in 1:size(p,1)
			sp = GeometryBasics.decompose(GeometryBasics.Point{3,Float64},GeometryBasics.Sphere(p[i],radius),4) |> unique
			for ii in 1:size(sp,1)
				push!(spheres,sp[ii])
			end
		end
	end

	return [[spheres[i].data...] for i in 1:size(spheres,1)] |> combinedims |> transpose |> collect
end
function linesegs(arr::AbstractArray{T,3}) where T<:AbstractFloat
    new_arr::AbstractArray{Point3f0} = []
    for i in 1:size(arr,1)
        push!(new_arr, Makie.Point3f0(arr[i,1,:]))
        push!(new_arr, Makie.Point3f0(arr[i,2,:]))
    end
    return new_arr |> combinedims |> transpose |> collect
end

Load a structure and set up the figure

Load the structure with BioStructures.jl and get a coordinates Observable. Then set up the Figure and Layout.

struc = retrievepdb("2vb1")
chn = struc[1]["A"] |> Observable
atms = collectatoms(struc, standardselector) |> Observable
cords = @lift coordarray($atms)' |> collect
fig = Figure(resolution = (800,600))
layout = fig[1,1] = GridLayout(10, 9)

Add text and interactive elements

It can be helpful to run this line by line to see what is happening.

strucname = struc.name[1:4]
sc_scene = layout[1:10,1:6] = LScene(fig; show_axis = false)
structxt = layout[1,7:8] = Label(fig, text = "Structure ID:  $(strucname)", fontsize = 35)
alpha1 = layout[5,7:9] = Slider(fig, range = 1.5:0.5:9.0, startvalue = 2.5)
alphatxt1 = lift(alpha1.value) do s1; string("alpha = ", round(s1, sigdigits = 2)); end
alphatext = layout[4,7:9] = Label(fig, text = alphatxt1, fontsize = 22)
alphaval = alpha1.value
radii1 = layout[7,7:9] = Slider(fig, range = 1.5:0.5:9.0, startvalue = 2.5)
radiixt1 = lift(radii1.value) do s1; string("atom radius = ", round(s1, sigdigits = 2)); end
radiitext = layout[6,7:9] = Label(fig, text = radiixt1, fontsize = 22)
radiival = radii1.value;
nothing #hide

Alpha shape

Lift the sphere points Observable to get the alpha shape vertices and edges. Our getalphashape function returns us both at once

spnts = @lift getspherepoints($cords,$radiival)
proteinshape = @lift let pnts = $spnts; getalphashape(pnts,$alphaval); end
alphaverts = @lift $spnts[$(proteinshape)[1],:]
alphaedges = @lift $spnts[$(proteinshape)[2],:] |> linesegs

Surface area

Define a function to get the surface area of a set of coordinates and connectivity. The surface area changes when the alpha value or atom radius is changed.

using Meshes
function surfacearea(coordinates, connectivity)
    totalarea = 0.0
    for i = 1:size(connectivity,1)
        totalarea += measure(Ngon(Meshes.Point3.(coordinates[connectivity[i,1],:],
                        coordinates[connectivity[i,2],:], coordinates[connectivity[i,3],:])...))
    end
    return totalarea
end
surfarea = @lift surfacearea($spnts, $(proteinshape)[3])
surfatext = layout[2,7:9] = Label(fig, text = lift(X->string("surface area = ", round(Int64, X), "  Ų"), surfarea), fontsize = 22)

Plot the shape

Finally, plot the mesh shape. Moving the sliders will update the plot. It is laggy, but it works to construct surfaces in real time. You may want to click on the slider rather than dragging it. Speed may be improved in the future.

linesegments!(sc_scene, alphaedges, color = :gray, transparency = true)

To show where the atoms are run the following line.

meshscatter!(sc_scene, cords, markersize = 0.3, color = :blue)

To show the alpha shape vertices run the following line.

meshscatter!(sc_scene, alphaverts, markersize = 0.3, color = :green)

Save the figure as a png file.

save("alphashape.png", fig)

This page was generated using Literate.jl.