Living polymers
In [1]:
import numpy as np
import readdy
import matplotlib.pyplot as plt
import os
from collections import defaultdict
In [2]:
readdy.__version__
Out[2]:
In [3]:
system = readdy.ReactionDiffusionSystem(box_size=[100, 100, 100])
In [4]:
system.topologies.add_type("Polymer")
system.add_topology_species("Head", .002)
system.add_topology_species("Tail", .002)
In [5]:
system.topologies.configure_harmonic_bond("Head", "Tail", force_constant=50, length=1.)
system.topologies.configure_harmonic_bond("Tail", "Tail", force_constant=50, length=1.)
system.topologies.configure_harmonic_bond("Head", "Head", force_constant=50, length=1.)
system.topologies.configure_harmonic_angle("Head", "Tail", "Tail", force_constant=10, equilibrium_angle=np.pi)
system.topologies.configure_harmonic_angle("Tail", "Tail", "Tail", force_constant=10, equilibrium_angle=np.pi)
In [6]:
# dissociation
def dissociation_rate_function(topology):
    edges = topology.get_graph().get_edges()
    return .000005 * float(len(edges)) if len(edges) > 2 else 0.
def dissociation_reaction_function(topology):
    recipe = readdy.StructuralReactionRecipe(topology)
    edges = topology.get_graph().get_edges()
    vertices = topology.get_graph().get_vertices()
    
    # at least a structure like
    # v1 -- v2 -- v3 -- v4
    if len(edges) > 2:
        # find the end particles
        counts = defaultdict(int)
        for e in edges:
            pix1 = e[0].get().particle_index
            pix2 = e[1].get().particle_index
            counts[pix1] += 1
            counts[pix2] += 1
        
        # the end particles are the ones that have exactly one edge leading from/to them
        endpoints = []
        for pix in counts.keys():
            if counts[pix] == 1:
                endpoints.append(pix)
        assert len(endpoints) == 2, "the number of end points should always be 2 \
                but was {} (counts: {})".format(len(endpoints), counts)
        # draw an edge excluding the edges leading to the both ends
        edge_index = np.random.randint(0, len(edges) - 2)
        removed_edge = None
        # for each edge in the graph
        for e in edges:
            pix1 = e[0].get().particle_index
            pix2 = e[1].get().particle_index
            # check if it belongs to one of the end vertices
            if pix1 not in endpoints and pix2 not in endpoints:
                # if not, reduce edge_index until 0
                if edge_index == 0:
                    # remove this edge
                    recipe.remove_edge(e[0], e[1])
                    removed_edge = e
                    break
                else:
                    edge_index -= 1
        assert removed_edge is not None
        pix1 = removed_edge[0].get().particle_index
        pix2 = removed_edge[1].get().particle_index
        assert pix1 not in endpoints
        assert pix2 not in endpoints
        # since the edge was removed we now have two topologies and need to set the correct particle types
        recipe.change_particle_type([vix for vix, v in enumerate(vertices) if v.particle_index == pix1][0], "Head")
        recipe.change_particle_type([vix for vix, v in enumerate(vertices) if v.particle_index == pix2][0], "Head")
    else:
        print("this should not have happened")
    return recipe
system.topologies.add_structural_reaction("dissociation", "Polymer", 
                                          dissociation_reaction_function, dissociation_rate_function)
In [7]:
system.topologies.add_spatial_reaction("Association: Polymer(Head) + Polymer(Head) -> Polymer(Tail--Tail)", 
                                       rate=1.0, radius=1.0)
In [8]:
simulation = system.simulation(kernel="CPU")
In [9]:
# randomly place some polymers of length 4
n_polymers = 500
for head in range(n_polymers):
    head_position = 80. * np.random.random((1, 3)) - 40.
    offset1 = 2.*np.random.random((1, 3))-1.
    offset1 /= np.linalg.norm(offset1)
    tail1 = head_position + offset1
    offset2 = 2.*np.random.random((1, 3))-1.
    offset2 /= np.linalg.norm(offset2)
    tail2 = head_position + offset1 + offset2
    offset3 = 2.*np.random.random((1, 3)) - 1.
    offset3 /= np.linalg.norm(offset3)
    head_position2 = head_position + offset1 + offset2 + offset3
    top = simulation.add_topology("Polymer", ["Head", "Tail", "Tail", "Head"],
                                  np.array([head_position, tail1, tail2, head_position2]).squeeze())
    top.get_graph().add_edge(0, 1)
    top.get_graph().add_edge(1, 2)
    top.get_graph().add_edge(2, 3)
In [10]:
simulation.output_file = 'living_polymers.h5'
simulation.observe.topologies(300)
simulation.record_trajectory(300)
simulation.progress_output_stride = 10
simulation.show_progress = True
In [11]:
timestep = 1.
if os.path.exists(simulation.output_file):
    os.remove(simulation.output_file)
simulation.run(50000, timestep)
In [12]:
# read back topologies
t = readdy.Trajectory(simulation.output_file)
time, topology_records = t.read_observable_topologies()
In [13]:
# gather average length of topologies for respective time step
avg_length = []
# for each time step
for topologies in topology_records:
    # gather average polymer length
    avg_length.append(0)
    for top in topologies:
        avg_length[-1] += len(top.edges)
    avg_length[-1] /= len(topologies)
f, ax = plt.subplots(nrows=1, ncols=1)
ax.plot(time, avg_length)
ax.set_title('average length')
ax.set_xlabel('time')
ax.set_ylabel('# beads')
plt.show()
In [14]:
from IPython.display import YouTubeVideo
YouTubeVideo('1fZqbZRQnEQ')
Out[14]: