Force Inference for Biological Tissues

Forces and pressure play a key role in the development of an organism. These forces that are generated by the cells can act upon other cells in concert to yield dramatic changes in the architecture of a tissue. Forces may cause a tissue to stretch or rotate. Likewise, the internal pressure within the cells may cause the tissue to bulge or contract. Without precise regulation of forces and pressure by the cells it is not hard to imagine that the developmental processes will be severely impacted. Therefore, biologists and biophysicists who are studying animal development often need a measure of the distribution of forces and pressure in a tissue over time.

There are experimental methods for measuring forces. In “laser ablation” a high-powered laser beam can be used to ablate junction(s) between two or more cells that are under tension. The recoil velocity can be used to determine the magnitude of tension. Imagine cutting a guitar string that is under high tension. Once snapped it will spring back. The process is highly invasive, that is the junction in query has to be severed, rendering it less useful to study forces in a spatio-temporal manner.

In this post I would like to share with you the notion of inferring forces and pressure in a tissue using images. This technique, now commonly known as “Force Inference” was proposed by Ishihara and Sugimura (Journal of Theoretical Biology, 2012) as well as Brodland (2014). Force Inference allows us to determine forces without having to destroy tissues. And the idea is pretty simple. A cell is delimited by the junctions (edges) enclosing it and an edge can be represented by a line drawn between two vertices. We assume that a tissue at any given moment is in quasi-equilibrium and consequently the forces acting on the vertices sum to zero. Such an assumption makes sense since morphological changes in tissues occur over a long time-scale. The inertial and viscous effects are negligible. The forces acting on a vertex are both due to tension acting along the cell-cell junctions and the internal pressure of the cell.

Here is a Mathematica implementation that uses force balance over all vertices of an epithelia (sheet of cells) to determine the unknown tension and pressure. Note that the forces and pressure determined from this method only yields a relative estimate of the unknowns and not an absolute one. The script is based on the approach proposed by Ishihara

im1im2im3im4

(* ::Package:: *)

(* ::Section:: *)
(*Associated Functions*)


segmentImage[binarizedMask_?ImageQ,opt:"ConnectedComponents"|"Watershed":"Watershed",
threshCellsize_:\[Infinity]]:= Module[{seg,areas,indexMaxarea,maxArea,indsmallareas={},$ind},
seg = Switch[opt,"ConnectedComponents",
(* assuming we input 0 as foreground and 1 as background. ConnectedComponents is a more general segmentation framework *)
MorphologicalComponents[ColorNegate@binarizedMask, CornerNeighbors->False],
"Watershed",(* for epithelial cells *)
WatershedComponents[binarizedMask, CornerNeighbors->False]
];
areas=ComponentMeasurements[seg,"Area"];
{indexMaxarea,maxArea}=First@MaximalBy[areas,Last]/.Rule-> List;
indsmallareas = Keys@Cases[areas,HoldPattern[_-> 1.]];
If[maxArea >= threshCellsize||indsmallareas!= {},
$ind={indexMaxarea}~Join~indsmallareas;
seg=ArrayComponents[seg,Length@areas,Thread[$ind->0]]
];
seg
];


Options[associateVertices]= {"stringentCheck"-> True};
associateVertices[img_,segt_,maskDil_:2,OptionsPattern[]]:= With[{dim =Reverse@ImageDimensions@img,
stringentQ=OptionValue["stringentCheck"]},
Module[{pts,members,vertices,nearest,vertexset,likelymergers,imagegraph,imggraphweight,imggraphpts,vertexpairs,
posVertexMergers,meanVertices,Fn},
pts = ImageValuePositions[MorphologicalTransform[img,{"Fill","SkeletonBranchPoints"}], 1]; (* finding branch points *)
members = ParallelMap[Block[{elems},
 elems = Dilation[ReplaceImageValue[ConstantImage[0,Reverse@dim],#->1],1];
 DeleteCases[Union@Flatten@ImageData[elems*Image[segt]],0.]
 ]&,pts]; 
vertices = Cases[Thread[Round@members-> pts],HoldPattern[pattern:{__}/;Length@pattern >= 2 -> _]];
(* finding vertices with 2 or more neighbouring cells *)
nearest = Nearest[Reverse[vertices, 2]]; (* nearest func for candidate vertices *)
Fn = GroupBy[MapAt[Sort,(#-> nearest[#,{All,3}]&/@Values[vertices]),{All,2}],Last->First,#]&;
Which[Not@stringentQ,
 (* merge if candidate vertices are 2 manhattan blocks away. Not a stringent check for merging *)
 KeyMap[Union@*Flatten]@Fn[List@*N@*Mean]//Normal,
 stringentQ,
 (* a better check is to see the pixels separating the vertices are less than 3 blocks *)
 vertexset = Fn[Identity];
 (* candidates for merging*)
 likelymergers = Cases[Normal[vertexset],PatternSequence[{{__Integer}..}-> i:{__List}/;Length[i]>= 2]];
 (*defining graph properties of the image *)
 imagegraph = MorphologicalGraph@MorphologicalTransform[img,{"Fill"}];
 imggraphweight = AssociationThread[(EdgeList[imagegraph]/.UndirectedEdge->List )-> PropertyValue[imagegraph,EdgeWeight]];
 imggraphpts = Nearest@Reverse[Thread[VertexList[imagegraph]-> PropertyValue[imagegraph,VertexCoordinates]],2];
 (* corresponding nodes on the graph *)
 vertexpairs = Union@*Flatten@*imggraphpts/@(Values[likelymergers]);
 (* find pairs < than 3 edgeweights away, take a mean of vertices and update the association with mean position *)
 posVertexMergers = Position[Thread[Lookup[imggraphweight,vertexpairs]< 3],True];
 If[posVertexMergers != {},
  meanVertices=MapAt[List@*N@*Mean,likelymergers,Thread[{Flatten@posVertexMergers,2}]];
  Scan[(vertexset[#[[1]]]=#[[2]])&,meanVertices]
  ];
  KeyMap[Union@*Flatten]@vertexset//Normal]
  ]
]; 


(* ::Section:: *)
(*Force Inference*)


plotMaps[p_,segmentation_,edgeImg_,maxcellLabels_,dimTx_,vertexToCells_,
vertexCoordinatelookup_,edgeLabels_]:=Module[{cellToVertexLabels,cellToAllVertices,ptsEdges,k,v,ord,edgeptAssoc,poly,pts,
mean,ordering,orderpts,tvals,cols,pvals,removecollabels,collabels,pressurecolours},

cellToVertexLabels= Reverse[vertexToCells,2];
cellToAllVertices= GroupBy[Flatten[Thread/@cellToVertexLabels],First-> Last];
(* polygons *)
ptsEdges ={{1,1},Reverse@Dimensions[segmentation],{Last[Dimensions@segmentation],1},{1,First[Dimensions@segmentation]}};
{k,v}={Keys@#,Values[#][[All,2]]}&@ComponentMeasurements[segmentation,{"AdjacentBorderCount","Centroid"},#==2&];
ord=Flatten[Function[x,Position[#,Min[#]]&@Map[EuclideanDistance[#,x]&,ptsEdges]]/@v];
edgeptAssoc=Association[Rule@@@Thread[{k,ptsEdges[[ord]]}]];
poly=(
pts=vertexCoordinatelookup/@cellToAllVertices[#];
If[MemberQ[k,#],AppendTo[pts,edgeptAssoc[#]],pts];
mean=Mean[pts];
ordering=Ordering[ArcTan[Last@#-Last@mean,First@#-First@mean]&/@pts];
orderpts=pts[[ordering]];
Polygon@Append[orderpts,First@orderpts]
)&/@Range[maxcellLabels];

tvals=Rescale@p[[1;;Last@dimTx]];
cols=ColorData["Rainbow"][#]&/@tvals; 
Print["Tension map:"];
Print[Graphics[{Thickness[0.005],Riffle[cols,Line/@Values@edgeLabels]}]];
pvals=p[[ Last[dimTx]+1;;]];
removecollabels=Keys@ComponentMeasurements[segmentation,"AdjacentBorders",Length[#]>0&];
collabels=Complement[Range@maxcellLabels,removecollabels];
pressurecolours=ColorData["Rainbow"][#]&/@Rescale[(pvals[[collabels]])];
Print["Pressure map:"];
Print@Show[Graphics@Riffle[pressurecolours,poly[[collabels]]],edgeImg]; 
]


(* maximize Log-likelihood function *)
maximizeLogLikelihood[spArrayX_,spArrayY_,dimTx_,dimPx_]:= Module[{range=10.0^Range[-1.5,1.5,0.1],sol,spA,spg,spB,n,m,spb,\[Tau],
SMatrix,Q,R,H,h,logL,\[Mu],p},
Print[Style["\nwith maximum likelihood",Bold,18]];
sol=With[{ls=range},
(*spA=SparseArray@(Join[spArrayX,spArrayY]);*)
spA=SparseArray@(Flatten[Transpose[{spArrayX,spArrayY}],1]);
spg=SparseArray@(ConstantArray[1.,Last@dimTx]~Join~ConstantArray[0.,Last@dimPx]);
spB=SparseArray@DiagonalMatrix[spg];
n=First@Dimensions@spA;
m=(Length[#]-Total@#)&@Diagonal[spB\[Transpose].spB];
With[{DimspB=First[Dimensions@spB]},
spb=SparseArray@ConstantArray[0.,First[Dimensions@spA]];
Table[(\[Tau]=Sqrt[\[Mu]];
SMatrix=SparseArray@(Map[Flatten]@Transpose[{Join[spA,\[Tau] spB],Join[spb,\[Tau] spg]},{2,1}]);
{Q,R}=SparseArray/@QRDecomposition[SMatrix];
R=DiagonalMatrix[Sign[Diagonal@R]].R;
H=R[[;;#,;;#]]&@DimspB;
\!\(\*OverscriptBox[\(h\), \(\[RightVector]\)]\)=R[[;;#,#+1]]&@DimspB;
h=R[[#+1,#+1]]&@DimspB;
logL=-(n-m+1)*Log[h^2]+Total[Log[Diagonal[\[Mu] (spB\[Transpose].spB)]["NonzeroValues"]]]-
2*Total[Log[Diagonal[H[[1;;-2,1;;-2]]]["NonzeroValues"]]]
),{\[Mu],ls}]
]
];
Print[ListPlot[{sol,sol},Joined-> {True,False},PlotStyle->{{Red,Thick},{PointSize[0.02],Black}},AxesStyle->{{Black},{Black}},
AxesLabel->{"index \[Mu]","LogLikelihood"},Background->LightBlue]];
\[Mu]=Keys@@MaximalBy[Thread[range-> sol],Values,1];
Print["optimized value of \[Mu]: ",\[Mu]];
\[Tau]=Sqrt[\[Mu]];
With[{DimspB=First[Dimensions@spB]},
SMatrix=SparseArray@(Map[Flatten]@Transpose[{Join[spA,\[Tau] spB],Join[spb,\[Tau] spg]},{2,1}]);
{Q,R}=SparseArray/@QRDecomposition[SMatrix];
R=DiagonalMatrix[Sign[Diagonal@R]].R;
H=R[[;;#,;;#]]&@DimspB;
\!\(\*OverscriptBox[\(h\), \(\[RightVector]\)]\)=R[[;;#,#+1]]&@DimspB;
];
p=PseudoInverse[H].\!\(\*OverscriptBox[\(h\), \(\[RightVector]\)]\); p
];


formAndComputeMatrices[vertexCoordinatelookup_,inds_,colsOrder_,edgenum_,delV_,
vertexToCells_,vertexvertexConn_,maxcellLabels_,filteredvertices_,vertexAssoc_]:=Module[{tx,ty,tensinds,filteredvertexnum,relabelvert,
spArrayTx,spArrayTy,spArrayPx,spArrayPy,spArrayX,spArrayY,$filteredvertices},
{tx,ty}=Transpose[
With[{target=vertexCoordinatelookup[#[[2]]],source=vertexCoordinatelookup[#[[1]]]},
(target-source)/Norm[target-source]
]&/@inds];
Print["Tension coefficients computed: ",Style["\[CheckmarkedBox]",20]];
MapThread[Print[Style["counts of zero coefficients "<>#1,Red], Count[#2,0.]]&,{{"Tx: ","Ty: "},{tx,ty}}];
$filteredvertices=Complement[filteredvertices,delV];
filteredvertexnum=Length@$filteredvertices;
relabelvert=AssociationThread[$filteredvertices-> Range[Length@$filteredvertices]];
tensinds=Thread[{Lookup[relabelvert,Part[inds,All,1]],colsOrder}];
spArrayTx=spArrayTy=SparseArray@ConstantArray[0,{filteredvertexnum,edgenum}];
MapThread[(spArrayTx[[Sequence@@#1]]=#2)&,{tensinds,tx}];
MapThread[(spArrayTy[[Sequence@@#1]]=#2)&,{tensinds,ty}];
spArrayPx=spArrayPy=SparseArray@ConstantArray[0,{filteredvertexnum,maxcellLabels}];
MapThread[Print[Style[#1<> "coefficients stats: ",Blue],Counts@Map[Total@*Unitize,Normal[#2]]]&,
{{"Tx ", "Ty "},{spArrayTx,spArrayTy}}];
Print[Style["Tension coefficients dist: ",Bold],Histogram[{{},Join[spArrayTx["NonzeroValues"],spArrayTy["NonzeroValues"]]},20,
ImageSize->Small]];
Block[{neighbouringCells,bisectionlabels,bisectpts,centroid,orderingT,
vertexcoords,orderptsT,orderIndT,orderCells,kk=0,px,py},
With[{cellToVertexLabelsT= Reverse[vertexToCells,2],
edgeVertexPart=GroupBy[vertexvertexConn~Flatten~1 ,First-> Last]},
With[{cellToAllVerticesT= GroupBy[Flatten[Thread/@cellToVertexLabelsT],First-> Last]},
Do[
neighbouringCells= vertexToCells[[i,2]]; (* for vertex, the neighbouring cell labels *)
bisectionlabels=Intersection[cellToAllVerticesT[#],edgeVertexPart[i]]&/@neighbouringCells ;
If[Length[neighbouringCells]>2 && MatchQ[bisectionlabels,{Repeated[{_,_},{3,\[Infinity]}]}],
(vertexcoords=DeleteDuplicates[vertexCoordinatelookup[#]&/@Flatten@bisectionlabels];
centroid=Mean@vertexcoords;
orderingT=Ordering[ArcTan[Last@#-Last@centroid,First@#-First@centroid]&/@vertexcoords];
orderptsT=vertexcoords[[orderingT]];
orderIndT=Partition[Lookup[vertexAssoc,Append[orderptsT,First@orderptsT]],2,1];
 orderCells =
Last@@@Position[(x\[Function] Map[Intersection[x,#]&,orderIndT])/@(cellToAllVerticesT[#]&/@neighbouringCells),x_/;Length[x]==2];
neighbouringCells=neighbouringCells[[orderCells]];
bisectpts=Map[vertexCoordinatelookup,orderIndT,{2}];
{px,py}=Transpose[{(#[[2,2]]-#[[1,2]])/2,-(#[[2,1]]-#[[1,1]])/2}&/@bisectpts];
If[MemberQ[px,0.]||MemberQ[py,0.],kk++];
{px,py})
];
Scan[(spArrayPx[[ Sequence@@#[[1]] ]]=#[[2]])&,Thread[Thread[{relabelvert@i,neighbouringCells}]-> px]];
Scan[(spArrayPy[[ Sequence@@#[[1]] ]]=#[[2]])&,Thread[Thread[{relabelvert@i,neighbouringCells}]-> py]],
{i,$filteredvertices}]
]
];
Print["Pressure coefficients computed: ",Style["\[CheckmarkedBox]",20]];
Print[Style["Pressure coefficients zero: ",Red],kk ];
];
MapThread[Print[Style[#1<> "coefficients stats: ",Blue],Counts@Map[Total@*Unitize,Normal[#2]]]&,
{{"Px ", "Py "},{spArrayPx,spArrayPy}}];
Print[Style["pressure coefficients dist: ",Bold],
Histogram[{{},Join[spArrayPx["NonzeroValues"],spArrayPy["NonzeroValues"]]},ImageSize->Small]];
spArrayX=Join[spArrayTx,spArrayPx,2];
spArrayY=Join[spArrayTy,spArrayPy,2];
{spArrayX,spArrayY,Dimensions[spArrayTx],Dimensions[spArrayPx]}
]


ForceInference[filename_]:=Module[{Img,segmentation,maxcellLabels,cellsToVertices,vertexnum,edges,smalledges,maxedgeLabels,
edgeEndPoints,nearest,nearestedgeEndPoints,edge2pixLabels,pos,oldCoords,vertexAssoc,vertexToCells,filteredvertices,filteredvertexnum,
relabelvert,edgeLabels,edgenum,spArrayTx,spArrayTy,vertexCoordinatelookup,vertexpairs,vertexvertexConn,inds,edgelabelToVert,delV,
vertToedges,edgeImg,colsOrder,p,spArrayX,spArrayY,dimTx,dimPx},

LaunchKernels[];
Img= ColorConvert[Import[filename],"Grayscale"];
Print[Image[Img,ImageSize->Medium]];
segmentation = segmentImage[Img];
Print["Image segmented: ", Style["\[CheckmarkedBox]",20]];
maxcellLabels = Max@Values@ComponentMeasurements[segmentation,"LabelCount"];
cellsToVertices = associateVertices[Binarize@Img,segmentation];
Print["vertices found and associated: ", Style["\[CheckmarkedBox]",20]];
vertexnum=Length@cellsToVertices;
edges=MorphologicalComponents@ImageFilter[If[#[[3,3]] == 1 && Total[#[[2;;-2,2;;-2]],2] == 3, 1, 0]&,Img,2];
(* associate vertices with all edges. for pixel value 1 edge find two nearest pts. for all edges <3, merge the pts together;
 make changes to the cellToVertex *)
(* edges to be deleted *)
smalledges=Flatten[Position[Values@ComponentMeasurements[edges,"Count"],1|2]];
maxedgeLabels=Max@edges;
edgeEndPoints=With[{comp=Values@ComponentMeasurements[edges,"Mask"]},
ParallelTable[
If[Total[#] == 1,ImageValuePositions[#,1],
ImageValuePositions[MorphologicalTransform[#,"SkeletonEndPoints"],1]]&@Binarize@Image[comp[[i]]]
,{i,maxedgeLabels}]
];
(* for small edge: if one pixel delete *)
edges=Fold[If[Length@edgeEndPoints[[#2]]==1,#1/.#2 -> 0,#1]&,edges,smalledges];
nearest=Nearest@Flatten[Values[cellsToVertices],1];
nearestedgeEndPoints=Map[Flatten@*nearest,edgeEndPoints,{2}];
(* if edge is two pixels then put average value in the cellsToVertices: *)
edge2pixLabels=Keys@Cases[ComponentMeasurements[edges,"Count"],HoldPattern[_-> 2]];

If[edge2pixLabels!={},
(oldCoords=nearestedgeEndPoints[[#]];
pos=Position[cellsToVertices,#,Infinity]&/@oldCoords;
cellsToVertices=Fold[ReplacePart[#1,#2-> Mean@oldCoords]&,cellsToVertices,pos]
)&/@edge2pixLabels
];
edges=ArrayComponents[edges/.Thread[edge2pixLabels-> 0]];
Print["edges found and associated: ", Style["\[CheckmarkedBox]",20]];
cellsToVertices=Normal@AssociationMap[Reverse,GroupBy[cellsToVertices,Last-> First,Union@*Flatten]];
vertexnum=Length@cellsToVertices;
nearest=Nearest@Flatten[Values@cellsToVertices,1];
edgeEndPoints=Delete[edgeEndPoints,Partition[smalledges,1]];
nearestedgeEndPoints=Map[Flatten@*nearest,edgeEndPoints,{2}];
vertexAssoc= AssociationThread[Flatten[Values@cellsToVertices,1],Range@vertexnum];
vertexToCells=Reverse[MapAt[vertexAssoc[#]&,MapAt[Flatten,cellsToVertices,{All,2}],{All,2}],2];
(* Tension*)
filteredvertices=Keys@Select[<|vertexToCells|>,(Length[#]>2&)];
filteredvertexnum=Length@filteredvertices;
(* till above we have isolated all vertices that share three edges; we can relabel those filtered vertices to be the 
rows of the matrix *)
relabelvert=AssociationThread[filteredvertices-> Range[Length@filteredvertices]];
(* all edges are relabeled to have a unique identity *)
edgeLabels=AssociationThread[Range[Length@#]->#]&[nearestedgeEndPoints];
edgenum=Max[Keys@edgeLabels];
vertexCoordinatelookup=AssociationMap[Reverse,vertexAssoc];(* given the vertex label \[Rule] get the coordinates from the 
original lookup *)
vertexpairs=Map[vertexAssoc,nearestedgeEndPoints,{2}];
(* edge coordinates to vertex label. take vertices one by one and find all the edges it is a part of. None should be less than 3 *)
vertexvertexConn= ParallelTable[
Cases[vertexpairs,{OrderlessPatternSequence[i,p_]}:> {i,p}],
{i,filteredvertices}];
delV=Cases[vertexvertexConn,{{p_,_},{p_,_}}:> p];
vertexvertexConn=DeleteCases[vertexvertexConn,{_,_}];
inds=Flatten[vertexvertexConn,1];
(* edgelabel \[Rule] vertices *)
edgelabelToVert=Map[vertexAssoc,edgeLabels,{2}];
(*vertices \[Rule] edgelabel *)
vertToedges=Normal@AssociationMap[Reverse,edgelabelToVert];
colsOrder=Flatten[Cases[vertToedges,PatternSequence[{OrderlessPatternSequence@@#}-> p_]:> p,Infinity]&/@inds];
edgeImg=Graphics[{Thickness[0.005],Line@Lookup[vertexCoordinatelookup,edgelabelToVert[#]]&/@colsOrder}];
{spArrayX,spArrayY,dimTx,dimPx} = formAndComputeMatrices[vertexCoordinatelookup,inds,colsOrder,edgenum,delV,vertexToCells,
vertexvertexConn,maxcellLabels,filteredvertices,vertexAssoc];
p = maximizeLogLikelihood[spArrayX,spArrayY,dimTx,dimPx];
plotMaps[p,segmentation,edgeImg,maxcellLabels,dimTx,vertexToCells,vertexCoordinatelookup,edgeLabels];
];

Maximum Flows & Matching

I have found graphs to be the most intuitive mathematical concept. Graphs are intuitive in a sense that they seem closer to reality. For instance, relationships in communities can be conveniently represented as a graph rather than say a set of differential equations. However, before I begin with the post I wish to mention that a “graph” and a “plot” are not the same thing, albeit both are unfortunately used interchangeably in science. They are two different entities.

It is interesting to note that the whole field of Graph theory was born (1736) out of a need to solve the seven bridges of Konigsberg problem by the legendary mathematician Leonhard Euler. A graph is an abstraction that consists a set of vertices (elements) that are linked together by some weight. This mere abstraction allows us to model very complicated real world phenomena: from modeling signaling networks, social networks, transportation, internet, scheduling/assignments and even stochastic processes (in form of Markov Chains and Random Networks). Given their power, I wonder why graphs have not been incorporated as part of high-school math curriculum in the same way as calculus or linear algebra.

Graph theory is diverse and has its own language. In an earlier post we looked at some algorithms for traversing graphs and finding shortest paths. In this post I would like to demonstrate how the concept of maximum-flows in graph can be used to solve some interesting (real/fictional)-world problems.

What is maximum-flow in a network?

Suppose we have some graph with a starting vertex (source) and a target vertex (sink) and there exists a path between the two vertices. Now imagine the network resembles a complex system of pipes (blood vessels, sewage pipes etc.. I leave it to your taste) and a fluid is flowing through the network – from the source towards the sink. Furthermore, each pipe or edge in the network has a certain capacity for the flow. Some pipes are carrying flow to their full capacity whereas others not so much. The source has an infinite ability to supply fluid but is constrained by the capacities of the network. Your job is to change the dials and increase the flow in the system. This is the essence of the problem: how to tweak flow in the pipes in order to achieve a maximum flow towards the sink.

How to find maximum-flow

For a very small network (a few vertices) perhaps one can do it manually. However, the problem becomes tedious to solve for a complex network. Hence there should be a more systematic way to deal with the problem. One solution was proposed by Ford and Fulkerson (1956). Note: there are other algorithms for solving the problem.

Some underlying assumptions:
– conservation law holds for all vertices except source and sink. Simply, the flow out of the vertex is the same as flow into the vertex
– flows cannot exceed the maximum capacities of the edges
– flow out of source is equal to flow into sink (no losses)

The recipe for Ford-Fulkerson is as follows:

1. Set the flows in the graph as 0
2. create a residual network from the original network:
– forward edge in the residual network corresponds to edge capacity minus current flow in the original network
– backward edge corresponds to the current flow in the original network
– an edge operating at maximum capacity in the original network is demarcated by a backward edge
3. find a path from source to sink in the residual graph and the minimum flow (“b”) on the path
4. augment flows in the graph based on the path found in step 3, based on the value “b”:
– adjust flows in the original network by adding or subtracting “b” along the corresponding path
5. repeat step 2 to 4 and terminate when no path is found for augmentation

So far I have asked the reader to imagine a flow as a physical entity. It is important to bear in mind that these flows (edge-weights) may or may not be physical in nature and can be as abstract as the problem being dealt with. To take advantage of the abstraction that networks offer below I will discuss some examples where maximum flows can be utilized.

Applications of maximum-flows (in Wolfram Language/Mathematica):

Note: the examples are modified from Wolfram Mathematica Documentation

(1) maximize transport of cargo in old soviet union

Say we have a graph of the old railroad network of old Soviet Union and are asked to maximize the transport of goods from some production facilities (origin) to destinations. The facilities are on the east and destinations are situated on the west. We can conveniently use maximal flows to determine the optimum flow on each edge so as to maximize the transport of cargo to the destinations.

graph1

Below we have the graph with the flows and the values for all the edges registered in a table.

graph2graph3

(2) humanity’s survival and SKYNET

The year is 2035 and seeing humanity as a threat, Skynet has eliminated most of the world’s population. The remaining humans are forced to reside in 10 cities (CT1, CT2, CT3 …). Humanity’s survival now lies with CT1 – led by John Connor – and CT7 (with a graph theorist). In order to thwart Skynet all cities must cooperate however, it is absolutely critical that the resistance forces in the two cities have maximal communication at all times. How would you create a graph to establish such communication?

graph4

Flow networks to maximize communication between CT1 and CT7

graph5

 

(3) Choosing representatives

Suppose there are 4 clubs, 7 students and 3 disciplines. Each student can belong to more than one club but only one discipline. Each club is asked to pick a representative for the annual student body in a manner that the chosen members belonging to a given  discipline is at most Subscript[u,k] where k = 2,2,3. The only exception to the rule is club number 4, which can pick 2 candidates – a prestige given to the oldest club.

How can the students be possibly chosen from the network?

graph6

the five elected residents are R1, R2, R4, R5 and R6

graph7

 

(4) X-men teaching schedule

Besides fighting nemesis like Magneto and Apocalypse, X-men can be pretty busy at the Xavier school for gifted children. Several members of X-men have to teach some classes this semester to the young mutants. The courses include:
1. “Library”
2. “Controlling Mutant Powers”
3. “Cerebral 101”
4. “Python for Mutants”
5. “Simulator” (intensive class – needs 2 instructors)

The problem is that each X-men can be fit to teach more than one class.

Here is how we can propose a schedule so that all the classes can be taught simultaneously without conflicts

graph8

Teaching schedule for our superheroes

graph9

Now I hope you realize how graphs/networks can be useful

Merge Sort – a beautiful sorting algorithm

MergeSort belongs to the class of divide-and-conquer algorithms and is classified as a general purpose sorting algorithm. The algorithm was first proposed by the eminent mathematician, John Von Neumann. I first learned about this in an algorithms class. The idea is pretty simple yet powerful. Take a list of random numbers (Integers, Reals etc.) and split the list into two halves; break the new lists recursively until the lists are composed of individual elements; consecutive individuals are compared, sorted and concatenated into lists; compare and sort consecutive lists to build the final list (done in a recursive manner).

The diagram below depicts the process (courtesy: Wikimedia):

Merge_sort_algorithm_diagram

The code for MergeSort in Wolfram Language can be written in a very elegant manner (using recursion and pattern matching):

merge[{OrderlessPatternSequence[p : {__}, {}]}] := p;
merge[{{x_, p : ___}, {y_, q : ___}}] /; y < x := {y}~Join~merge[{{x, p},{q}}];
merge[{{x_, p : ___}, {y_, q : ___}}] /; x <= y := {x}~Join~merge[{{p}, {y, q}}];

mergeSort[arg_] := arg /; MatchQ[arg, {_} | {}];
mergeSort[x_List] := merge[mergeSort[{##}] & @@@ TakeDrop[x, Ceiling[Length[x]/2]]];

In this, the mergeSort function breaks the list recursively and merge function
compares,sorts and catenates the broken lists to form the final list

Here is a test for the algorithm:

mergeSort[{38, 27, 43, 3, 9, 82, 10}]

(*answer: {3, 9, 10, 27, 38, 43, 82} *)

(* here we sort a list of a randomly arranged numbers in the range 1-1000 *)
list = RandomSample@Range[1000];

randomlist
mergeSort[list] // ListPlot

sorted list

But I prefer to use the built-in “Sort” function to sort items which is of course several orders of magnitude faster – owing to internal optimization and some other clever algorithm.

UNET for Image Segmentation

For my research project I had to encounter a thorny problem. But before I tell about the problem I would like to briefly mention something about my research project. Basically I am using embryonic stem cells that self-organize to form spheroids (balls of cells) to study gastrulation events. In order to not bog down the readers with technical jargon, “gastrulation” is a process where the stem cells start to form the different layers; each layer then goes onto form the various tissues/organs, in the process unraveling the developmental plan of the entire organism. I am using experimental techniques and quantitative principles from biophysics and engineering to understand some aspects of this crucial process

Now coming back to the problem at hand, the gastruloids (image below) are quite rough in their appearance and not as beautiful as one would like them to be (only a mother can love such an image). Any means of quantifying these gastruloids requires me to initially segment them. When you see a time-lapse images of gastruloids it becomes apparent that they shed a lot of cells (for reasons I do not know yet). This adds considerable noise to the system; oftentimes to the point that – as a human – my eyes are fooled and run into the difficulty of finding the right contours for the spheroids. Here comes the disclosure: classical means/operations in image-processing (gradients and edge detection, filtering, morphological operations etc.. ) prove utterly futile for image segmentation in my case.

gastruloid

(A gastruloid – virtually a ball of cells with many shed around the periphery)

So what can you do to address the problem where even the best image processing tool in existence  – the human eyes – fails. This is precisely where you take help of neural networks. Neural networks are selling like hotcakes during the recent years. They have added life and hope to the once dead area of artificial intelligence. Again to avoid underlying technical details, neural networks is a paradigm utilized by the computer to mimic the working of a human brain by taking into account the complex interactions between the cells – but only digitally. There are many flavours of neural networks out there, each one geared towards performing a specific task. With advancements made in the area of deep learning/artificial intelligence, the neural nets have started to surpass humans in tasks that humans have been known to be best for i.e. classification tasks. A few recent examples that come to mind include Google’s AlphaGo beating the former World Go champion and an AI diagnosing skin cancer with an unprecedented accuracy.

I utilized one such flavour of neural networks (a deep convolutional network – termed as UNET) to solve my longstanding problem. I constructed the network in Wolfram-Language with external help from Alexey Golyshev. UNET is a deep convolutional network that has a series of convolutional and pooling operations in the contraction phase of the net (wherein the features are extracted) and a sequence of deconvolution & convolution operations in the expansion phase which then yields an output from the network. This output can be subjected to a threshold to ultimately generate a binarized mask (the image segmentation).

u-net-architecture - initial authors implementation

The architecture of UNET as provided by the author – image taken from:  https://lmb.informatik.uni-freiburg.de/people/ronneber/u-net/

I trained my network over my laptop GPU (Nvidia GTX 1050) by feeding an augmented data (a set of 300 images constructed from a small dataset) . The training was done in under 3 minutes !. The accuracy (computed as the Hamming Distance between two vectors) of the generated binary masks with respect to the ground truth (unseen data) for a set of 60 images was 98.55 %. And with this a task that previously required me to painstakingly trace the contour of the gastruloids manually can now be performed in a matter of milliseconds. All the saved time and perspiration to be utilized somewhere else?

The interesting aspect for me regarding the network was that despite my gastruloids being highly dynamic (morphing shape over time) I never had to explicity state it to the network. All the necessary features were learned from the limited number of images that I trained my network with. This is the beauty of the neural network.

Code:  UNET code on GITHUB (let me know if you wish to use it for your own task).

Below are the results obtained from the Wolfram implementation.

img1img2img3img4

Note: I have a python MXNET version of the UNET  @ python mxnet GITHUB

The wolfram version of UNET however seems to outperform the python version even though it also utilizes MXNET at the back-end for implementing neural networks. It should not come as a surprise because my guess is that the people at Wolfram Research may have done internal optimizations on top of the library

 

Lineage Mapper – an overlap based cell tracking approach

Biological cells are active entities that undergo complex morphological transformations, migration and/or division to generate new daughter cells. For biologists and biophysicists it is important to understand the dynamics of individual cells as well as the cell-cell interactions that pave way for driving self-assembly/organization starting from the microscopic, to the mesoscopic and ultimately, the macroscopic scale – patterning the whole organism.

In order to map such intricate interactions quantitatively, we often need to track biological cells across time-lapse images obtained from either video or fluorescent microscopy. Such tracking is done using a series of binarized image masks. The dynamic characteristics of cells – within a tissue – makes tracking them non-trivial. The ability of cells to swap their immediate neighbours in a randomized manner can further complexify the process. Furthermore, in a high density setting, neighbouring cells can come so close together that the binarized mask might represent them incorrectly as a single entity (a fused object).

While there are a plethora of algorithms available to perform cell-tracking, most are not robust enough. Let me explain. A simple scheme for cell-tracking requires a segmented mask (a matrix where all the pixels of a specific cell are delineated by a unique label) as input from the user. The segmented mask itself can be generated by utilizing various image-processing algorithms and steps. As is often the case, there is a feedback between the tracking and the segmentation scheme. Nevertheless, the two schemes should be decoupled to ensure robustness of the tracking scheme.

The Wolfram Language implementation of the overlap-based cell tracker is based on an article by Chalfoun et al, Scientific Reports, 2016 https://www.nature.com/articles/srep36984. The tracking algorithm can robustly track cells from a sequence of 2D images without any manual intervention. In addition, it corrects for any incorrect fusions/mergers of cells in an automated fashion and detects possible division (mitotic) events using stringent criterion for mother/daughter cells. The algorithm used for tracking is not specific to the segmentation scheme, which indeed makes lineage mapper more robust than many tracking schemes out there ! (Read the article for more detail). In short, the tracking scheme utilizes a minimization scheme over a cost matrix (the Hungarian Algorithm). In this implementation, I have used a graph theoretic approach to generate the same results.

The code below is the main tracking scheme (GitHub Code).

some examples illustrated below

 

masks are automatically corrected for incorrect mergers

trackedandmaskcorrected

Information about cell lineage, the appearance and disappearance extracted in form of table(s) and tree :

lineageTree&amp;table

When fusions are allowed (i.e. to track colony formation) we can also obtain a fusion tree together with the lineage tree:

fusions1

fusionstree

fusions2

Solving image mazes

Adapted from the following posts: (1) Vitaly’s post (2) Jon McLoone’s post

Navigating a 3-D Maze (image)

maze 1 vitaly

First solution can be found using Watershed (finds the connected components):

maze 2 vitalymaze 3 vitaly

Another solution can be found by doing a Thinning and Pruning Operation

maze 4 vitalymaze 5 vitaly

Navigating Blenheim Palace (using image processing functionality):

maze 1 jonThe maze can be navigated by eliminating any loops

maze 2 jonmaze 3 jon

Navigating Blenheim Palace (using graph theory):

maze 4 jonmaze 5 jonNow we have a graph to navigate through the maze. Finding the shortest path through the maze is now possible !

maze 6 jonmaze 7 jon

 

Supervised learning – recognizing handwritten digits

Wolfram Language incorporates the necessary tools to construct, train and use custom neural networks for learning. While sophisticated examples do exist in the Wolfram documentation for deep learning applications, here I present the way to recognize handwritten characters. I am currently exploring new ways to use these networks !

We take the MNIST dataset which is labeled (hence supervised learning) and train a neural network (built from a combination of different layers) to construct a 98.7 % accurate model for character recoginition !

Problem definition: given an image and its label to a trained network can we train and predict labels to characters supplied as inputs.

problem statement

A neural net is at a fundamental level a function where that function is able to yield an output, provided an input. The function (layers of networks) in supervised learning is first trained using a labeled dataset (in our case trainingData). The accuracy is tested by applying the function on a test dataset (in our case testData).

supervised-1-e1515584386604.pngThe network and its structure is uninitialized in the beginning. The great thing about Wolfram Language is that like other expressions, the network itself is a symbolic expression and hence lends itself to symbolic manipulations !!

supervised 2

We train the net with the trainingData setsupervised 2brandom images from the testData are fed to the trained network and predictions are made

supervised 3The neural net lenet is tested for the testData , giving us a 98.7 % accuracy. The confusion matrix is shown for the process.

supervised 4