Preventing and dealing with collisions

The emphasis on interactions with the environment entails that for most models dealing with collisions is an important part of the code.

Standard behavior

The robust approach is to let collisions happen and deal with the resulting CollisionError. The code below from the Beginner’s Tutorial shows an example where new_pos is varied randomly till no collision occurs:

def manage_front(self,constellation):
    ...
    count = 0 # counts number of add_child trials
    while count < 100:
        extension = self.unit_heading_sample(width=20)
        new_pos = self.end + extension * 5. # compute position of child end
        # check for possible collisions
        try:
            new_front = self.add_child(constellation,new_pos) # make a new front and store it
            ...
        except CollisionError as error:
            count += 1
            continue # pick another new_pos, no attempt to correct the error
        except (GridCompetitionError, InsideParentError, VolumeError):
            count += 1
            continue # pick another new_pos, no attempt to correct the error
    print ("Warning: failed extension for of",self.get_neuron_name(constellation))

Obivously this simple approach is not guaranteed to succeed, especially in crowded environments. It is always important to deal with failure of the method, in this example a warning is printed.

Getting more information about collisions

To deal more intelligently with collisions it is important to know which Front caused the collision, this information is available in the CollisionError:

def manage_front(self,constellation):
    ...
        except CollisionError as error:
            print (self,"collides with",error.collider,"with distance",error.distance)
    ...

Note that standard behavior is to return only the first Front identified as causing a collision, there may be other Fronts that also cause collisions and these may even be closer by. Usually collisions with older fronts will be detected first.

It is possible to force a search for all colliding fronts before triggering an error:

def manage_front(self,constellation):
    constellation.only_first_collision = False
    ...
        except CollisionError as error:
            if error.only_first:
                print (self,"collides with",error.collider,"with distance",error.distance)
            else:
                print (self,"collides with:")
                for i in range(len(error.collider)):
                    print ("  #",i,":",error.collider[i],"with distance",error.distance[i])
    ...

The constellation.only_first_collision attribute is a boolean that is initialized to True. If this is set to False before the call to add_child the simulator will check for all collisions with proposed new_front before returning with CollisionError. Note that coding this correctly is not simple:

  1. constellation.only_first_collision is local to each parallel processor and cannot be set globally. There are two strategies possible to using it:

    • either set it at the begin of each manage_front call as in the example above. This will affect all add_child calls and slow down the simulation.

    • change it to False just before the try and except statements for a selected add_child call and reset to True afterwards, this will affect only that one add_child call.

  2. depending on the setting of constellation.only_first_collision CollisionError returns either a Front or a [Front,] as collider, same for distance. The collider list is unsorted.

  3. because the setting of constellation.only_first_collision may be ambiguous CollisionError contains its value used in its first_only attribute and will always print correct information.

Based on the information provided by CollisionError sophisticated collision resolution routines can be written.

Automatic collision resolution

Some fairly simple collision conditions can be very hard to solve properly by random search. An example is a dendrite or axon trying to grow past a much larger soma, biological growth cones will eventually succeed in making an arc around such a structure, but this requires a sophisticated simulation of chemical cues to work in NeuroDevSim. Instead, the solve_collision method provides a phenomenological solution that respects the original direction of growth. It is called as:

points = self.solve_collision(constellation,new_pos,error)

solve_collision returns a list of Point that were free at the time of the call. To generate the solution proposed the add_branch method should be used, which will create a series of a few fronts if possible:

def manage_front(self,constellation):
    ...
    while count < max_count:
        new_pos = ...
        try:
            new_front = self.add_child(constellation,new_pos)
            self.disable(constellation) # success -> disable this front
            return
        except CollisionError as error:
            points = self.solve_collision(constellation,new_pos,error)
            if points: # one or more points was returned
                try:
                    new_fronts = self.add_branch(constellation,points)
                    # at least one new front made
                    self.disable(constellation) # success -> disable this front
                    return
                except CollisionError as error:
                    print (self.get_neuron_name(constellation),self,"solve_collision collides with",error.collider)
                    count += 1
                    continue # generate another new_pos, no attempt to correct the error
                except (GridCompetitionError,InsideParentError,VolumeError):
                    count += 1
                    continue # generate another new_pos, no attempt to correct the error
            else:
                count += 1
                continue # generate another new_pos
        except (GridCompetitionError,InsideParentError,VolumeError):
            count += 1
            continue # generate another new_pos, no attempt to correct the error
        ...

Note that solve_collision may fail and return an empty list. add_branch will try to instantiate fronts for every coordinate returned by solve_collision but this may fail. If at least one front can be made add_branch will return normally and the length of the new_fronts list returned gives the number of Front created, otherwise it will return with a new CollisionError. The reason that add_branch may fail partially or completely is that other processors may be instantiating new Front at coordinates needed after solve_collision returns and before or while add_branch is called.

Examples of the use of solve_collision can be found in the Migration notebook.