analysis.segmentation module

Segmentation algorithm used to identify the different structures that are formed in the encounter. This file can be called from the command line to make an illustrative plot of the algorithm.

Source code
"""Segmentation algorithm used to identify the different structures
that are formed in the encounter. This file can be called from the
command line to make an illustrative plot of the algorithm.
"""

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches

import utils


def segmentEncounter(data, plot=False, mode='all'):
    """Segment the encounter into tail, bridge, orbitting and
    stolen particles as described in the report.

    Parameters:
        data: A data instance as saved by the simulation to a pickle file
        plot: If true the segmentation will be plotted and shown. Highly
            useful for debugging.
        mode (string): If mode is 'all' all parts of the encounter will be
            identified. If mode is 'bridge' only the bridge will be
            identified. This is useful when there may be no tail.

    Returns:
        masks (tupple): tupple of array corresponding to the masks of the 
            (bridge, stolen, orbitting, tail) particles. One can then use
            e.g. data['r_vec'][bridgeMask].
        shape (tupple): tupple of (distances, angles) as measured from the
            center of mass and with respect to the x axis. They define the
            shape of the tail
        length (float): total length of the tail.
    """
    nRings = 100 # number of rings to use when segmenting the data

    # Localize the central masses
    r_vec = data['r_vec']
    centers = r_vec[data['type'][:,0]=='center']
    rCenters_vec = centers[1] - centers[0]
    rCenters = np.linalg.norm(rCenters_vec)
    rCenters_unit = rCenters_vec/np.linalg.norm(rCenters_vec)
    # Take particles to be on the tail a priori and
    # remove them as they are found in other structures
    particlesLeft = np.arange(0, len(r_vec))


    if plot:
        colour = '#c40f4c'
        f, axs = plt.subplots(1, 3,  figsize=(9, 4), sharey=False)
        f.subplots_adjust(hspace=0, wspace=0)
        axs[0].scatter(r_vec[:,0], r_vec[:,1], c=colour, alpha=0.1, s=0.1)
        axs[0].axis('equal')
        utils.plotCenterMasses(axs[0], data)
        axs[0].axis('off')

    # Step 1: project points to see if they are part of the bridge
    parallelProjection = np.dot(r_vec - centers[0], rCenters_unit)
    perpendicularProjection = np.linalg.norm(r_vec - centers[0][np.newaxis] 
        - parallelProjection[:,np.newaxis] * rCenters_unit[np.newaxis], axis=-1)
    bridgeMask = np.logical_and(np.logical_and(0.3*rCenters < parallelProjection,
        parallelProjection < .7*rCenters), perpendicularProjection < 2)

    # Remove the bridge
    notInBridge = np.logical_not(bridgeMask)
    r_vec = r_vec[notInBridge]
    particlesLeft = particlesLeft[notInBridge]

    if mode == 'bridge':
        return (bridgeMask, None, None, None), None, None

    # Step 2: select stolen particles by checking distance to centers
    stolenMask = (np.linalg.norm(r_vec - centers[0][np.newaxis], axis=-1) 
        > np.linalg.norm(r_vec - centers[1][np.newaxis], axis=-1))
    # Remove the stolen part
    notStolen = np.logical_not(stolenMask)
    r_vec = r_vec[notStolen]
    particlesLeft, stolenMask = particlesLeft[notStolen], particlesLeft[stolenMask]

    # Step 3: segment data into concentric rings (spherical shells really)
    r_vec = r_vec - centers[0]
    r = np.linalg.norm(r_vec, axis=-1)
    edges = np.linspace(0, 30, nRings) # nRings concentric spheres 
    indices = np.digitize(r, edges) # Classify particles into shells

    if plot:
        axs[1].scatter(r_vec[:,0], r_vec[:,1], c=colour, alpha=.1, s=.1)
        axs[1].axis('equal')
        axs[1].scatter(0, 0, s=100, marker="*", c='black', alpha=.7)
        axs[1].axis('off')
    
    # Step 4: find start of tail
    start = None
    for i in range(1, nRings+1):
        rMean = np.mean(r[indices==i])
        rMean_vec = np.mean(r_vec[indices==i], axis=0)
        parameter = np.linalg.norm(rMean_vec)/rMean

        if plot:
            circ = patches.Circle((0,0), edges[i-1], linewidth=0.5,edgecolor='black',facecolor='none', alpha=.7)
            axs[1].add_patch(circ)
            txtxy = edges[i-1] * np.array([np.sin(i/13), np.cos(i/13)])
            axs[1].annotate("{:.2f}".format(parameter), xy=txtxy, backgroundcolor='#ffffff55')
        
        if start is None and parameter>.8 : 
            start = i #Here starts the tail
            startDirection = rMean_vec/np.linalg.norm(rMean_vec)
            if not plot: break;

    if start is None: #abort if nothing found
        raise Exception('Could not identify tail')

    # Step 5: remove all circles before start
    inInnerRings = indices < start
    # Remove particles on the opposite direction to startDirection. 
    # in the now innermost 5 rings. Likely traces of the bridge.
    oppositeDirection = np.dot(r_vec, startDirection) < 0
    in5InnermostRings = indices <= start+5
    orbitting = np.logical_or(inInnerRings, 
        np.logical_and(oppositeDirection, in5InnermostRings))
    orbittingMask = particlesLeft[orbitting]
    r_vec = r_vec[np.logical_not(orbitting)]
    tailMask = particlesLeft[np.logical_not(orbitting)]

    if plot:
        axs[2].scatter(r_vec[:,0], r_vec[:,1], c=colour, alpha=0.1, s=0.1)
        axs[2].axis('equal')
        axs[2].scatter(0, 0, s=100, marker="*", c='black', alpha=.7)
        axs[2].axis('off')

    # Step 6: measure tail length and shape
    r = np.linalg.norm(r_vec, axis=-1)
    indices = np.digitize(r, edges)
    # Make list of barycenters
    points = [list(np.mean(r_vec[indices==i], axis=0))
        for i in range(1, nRings) if len(r_vec[indices==i])!=0]
    points = np.array(points)
    # Calculate total length
    lengths = np.sqrt(np.sum(np.diff(points, axis=0)**2, axis=1))
    length = np.sum(lengths)
    # Shape (for 2D only)
    angles = np.arctan2(points[:,1], points[:,0])
    distances = np.linalg.norm(points, axis=-1)
    shape = (distances, angles)

    if plot:
        axs[2].plot(points[:,0], points[:,1], c='black', linewidth=0.5, marker='+')

    if plot:
        plt.show()

    return (bridgeMask, stolenMask, orbittingMask, tailMask), shape, length


if __name__ == "__main__":
    data = utils.loadData('200mass', 10400)
    segmentEncounter(data, plot=True)

Functions

def segmentEncounter(data, plot=False, mode='all')

Segment the encounter into tail, bridge, orbitting and stolen particles as described in the report.

Parameters

data
A data instance as saved by the simulation to a pickle file
plot
If true the segmentation will be plotted and shown. Highly useful for debugging.
mode : string
If mode is 'all' all parts of the encounter will be identified. If mode is 'bridge' only the bridge will be identified. This is useful when there may be no tail.

Returns

masks : tupple
tupple of array corresponding to the masks of the (bridge, stolen, orbitting, tail) particles. One can then use e.g. data['r_vec'][bridgeMask].
shape : tupple
tupple of (distances, angles) as measured from the center of mass and with respect to the x axis. They define the shape of the tail
length : float
total length of the tail.
Source code
def segmentEncounter(data, plot=False, mode='all'):
    """Segment the encounter into tail, bridge, orbitting and
    stolen particles as described in the report.

    Parameters:
        data: A data instance as saved by the simulation to a pickle file
        plot: If true the segmentation will be plotted and shown. Highly
            useful for debugging.
        mode (string): If mode is 'all' all parts of the encounter will be
            identified. If mode is 'bridge' only the bridge will be
            identified. This is useful when there may be no tail.

    Returns:
        masks (tupple): tupple of array corresponding to the masks of the 
            (bridge, stolen, orbitting, tail) particles. One can then use
            e.g. data['r_vec'][bridgeMask].
        shape (tupple): tupple of (distances, angles) as measured from the
            center of mass and with respect to the x axis. They define the
            shape of the tail
        length (float): total length of the tail.
    """
    nRings = 100 # number of rings to use when segmenting the data

    # Localize the central masses
    r_vec = data['r_vec']
    centers = r_vec[data['type'][:,0]=='center']
    rCenters_vec = centers[1] - centers[0]
    rCenters = np.linalg.norm(rCenters_vec)
    rCenters_unit = rCenters_vec/np.linalg.norm(rCenters_vec)
    # Take particles to be on the tail a priori and
    # remove them as they are found in other structures
    particlesLeft = np.arange(0, len(r_vec))


    if plot:
        colour = '#c40f4c'
        f, axs = plt.subplots(1, 3,  figsize=(9, 4), sharey=False)
        f.subplots_adjust(hspace=0, wspace=0)
        axs[0].scatter(r_vec[:,0], r_vec[:,1], c=colour, alpha=0.1, s=0.1)
        axs[0].axis('equal')
        utils.plotCenterMasses(axs[0], data)
        axs[0].axis('off')

    # Step 1: project points to see if they are part of the bridge
    parallelProjection = np.dot(r_vec - centers[0], rCenters_unit)
    perpendicularProjection = np.linalg.norm(r_vec - centers[0][np.newaxis] 
        - parallelProjection[:,np.newaxis] * rCenters_unit[np.newaxis], axis=-1)
    bridgeMask = np.logical_and(np.logical_and(0.3*rCenters < parallelProjection,
        parallelProjection < .7*rCenters), perpendicularProjection < 2)

    # Remove the bridge
    notInBridge = np.logical_not(bridgeMask)
    r_vec = r_vec[notInBridge]
    particlesLeft = particlesLeft[notInBridge]

    if mode == 'bridge':
        return (bridgeMask, None, None, None), None, None

    # Step 2: select stolen particles by checking distance to centers
    stolenMask = (np.linalg.norm(r_vec - centers[0][np.newaxis], axis=-1) 
        > np.linalg.norm(r_vec - centers[1][np.newaxis], axis=-1))
    # Remove the stolen part
    notStolen = np.logical_not(stolenMask)
    r_vec = r_vec[notStolen]
    particlesLeft, stolenMask = particlesLeft[notStolen], particlesLeft[stolenMask]

    # Step 3: segment data into concentric rings (spherical shells really)
    r_vec = r_vec - centers[0]
    r = np.linalg.norm(r_vec, axis=-1)
    edges = np.linspace(0, 30, nRings) # nRings concentric spheres 
    indices = np.digitize(r, edges) # Classify particles into shells

    if plot:
        axs[1].scatter(r_vec[:,0], r_vec[:,1], c=colour, alpha=.1, s=.1)
        axs[1].axis('equal')
        axs[1].scatter(0, 0, s=100, marker="*", c='black', alpha=.7)
        axs[1].axis('off')
    
    # Step 4: find start of tail
    start = None
    for i in range(1, nRings+1):
        rMean = np.mean(r[indices==i])
        rMean_vec = np.mean(r_vec[indices==i], axis=0)
        parameter = np.linalg.norm(rMean_vec)/rMean

        if plot:
            circ = patches.Circle((0,0), edges[i-1], linewidth=0.5,edgecolor='black',facecolor='none', alpha=.7)
            axs[1].add_patch(circ)
            txtxy = edges[i-1] * np.array([np.sin(i/13), np.cos(i/13)])
            axs[1].annotate("{:.2f}".format(parameter), xy=txtxy, backgroundcolor='#ffffff55')
        
        if start is None and parameter>.8 : 
            start = i #Here starts the tail
            startDirection = rMean_vec/np.linalg.norm(rMean_vec)
            if not plot: break;

    if start is None: #abort if nothing found
        raise Exception('Could not identify tail')

    # Step 5: remove all circles before start
    inInnerRings = indices < start
    # Remove particles on the opposite direction to startDirection. 
    # in the now innermost 5 rings. Likely traces of the bridge.
    oppositeDirection = np.dot(r_vec, startDirection) < 0
    in5InnermostRings = indices <= start+5
    orbitting = np.logical_or(inInnerRings, 
        np.logical_and(oppositeDirection, in5InnermostRings))
    orbittingMask = particlesLeft[orbitting]
    r_vec = r_vec[np.logical_not(orbitting)]
    tailMask = particlesLeft[np.logical_not(orbitting)]

    if plot:
        axs[2].scatter(r_vec[:,0], r_vec[:,1], c=colour, alpha=0.1, s=0.1)
        axs[2].axis('equal')
        axs[2].scatter(0, 0, s=100, marker="*", c='black', alpha=.7)
        axs[2].axis('off')

    # Step 6: measure tail length and shape
    r = np.linalg.norm(r_vec, axis=-1)
    indices = np.digitize(r, edges)
    # Make list of barycenters
    points = [list(np.mean(r_vec[indices==i], axis=0))
        for i in range(1, nRings) if len(r_vec[indices==i])!=0]
    points = np.array(points)
    # Calculate total length
    lengths = np.sqrt(np.sum(np.diff(points, axis=0)**2, axis=1))
    length = np.sum(lengths)
    # Shape (for 2D only)
    angles = np.arctan2(points[:,1], points[:,0])
    distances = np.linalg.norm(points, axis=-1)
    shape = (distances, angles)

    if plot:
        axs[2].plot(points[:,0], points[:,1], c='black', linewidth=0.5, marker='+')

    if plot:
        plt.show()

    return (bridgeMask, stolenMask, orbittingMask, tailMask), shape, length