Skip to content

Commit 7da0d02

Browse files
Put Alexandra's script in data
1 parent cf06da4 commit 7da0d02

File tree

1 file changed

+314
-0
lines changed

1 file changed

+314
-0
lines changed

data/generate_arterial_tree.py

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
import networkx as nx
2+
import numpy as np
3+
import random
4+
import copy
5+
6+
### Creation of a tree-like network ###
7+
# This is an adaption of code created by Alexandra Vallet (University of Oslo)
8+
# See https://gitlab.com/ValletAlexandra/NetworkGen for the original
9+
10+
"""
11+
Murray's law
12+
We consider that the sum of power 3 of daughter vessels radii equal to the power three of the parent vessel radius
13+
D0^3=D1^3 + D2^3
14+
We consider that the ratio between two daughter vessels radii is gam
15+
D2/D1=gam
16+
Then we have
17+
D2=D0(gam^3+1)^(-1/3)
18+
D1=gam D2
19+
20+
We consider that the length L of a vessel segment in a given network is related to its diameter d via L = λd,
21+
here the positive constant λ is network-specific
22+
23+
The angle of bifurcation can be expressed based on blood volume conservation and minimum energy hypothesis
24+
FUNG, Y. (1997). Biomechanics: Circulation. 2nd edition. Heidelberg: Springer.
25+
HUMPHREY, J. and DELANGE, S. (2004). An Introduction to Biomechanics. Heidelberg: Springer.
26+
VOGEL, S. (1992). Vital Circuits. Oxford: Oxford University Press.
27+
See here for derivation : https://www.montogue.com/blog/murrays-law-and-arterial-bifurcations/
28+
Then it depends on the diameters
29+
"""
30+
31+
32+
def make_arterial_tree(
33+
N, radius0=1, gam=0.8, lmbda=8, signs=False, uniform_lengths=False
34+
):
35+
"""
36+
N (int): number of levels in the arterial tree
37+
radius0 (float): radius of first vessel
38+
gam (float): ratio between daughter vessel radii
39+
signs (list): vector of choices (+-1) of vessel direction. If no vector is given this is assigned randomly.
40+
uniform_lengths (bool): uniform branch length
41+
42+
Uniform lengths is typically only of interest in numerical tests
43+
Assigning signs is useful for reproducability of results.
44+
"""
45+
46+
# Parameters
47+
# Origin location
48+
p0 = [0, 0, 0]
49+
50+
# Initial direction
51+
direction = [0, 1, 0]
52+
53+
# First vessel diameter
54+
D0 = 2 * radius0
55+
56+
# By convention we chose gam <=1 so D1 will always be smaller or equal to D2
57+
if gam > 1:
58+
raise Exception("Please choose a value for gamma lower or equal to 1")
59+
60+
# Surface normal function
61+
# The surface normal here is fixed because we want to stay in the x,y plane.
62+
# But this could be the normal of any surface.
63+
def normal(x, y, z):
64+
return [0, 0, 1]
65+
66+
#### Creation of the graph
67+
68+
# Create a networkx graph
69+
G = nx.DiGraph()
70+
71+
# Create the first vessel
72+
#########################
73+
L = D0 * lmbda
74+
75+
G.add_edge(0, 1)
76+
77+
nx.set_node_attributes(G, p0, "pos")
78+
nx.set_edge_attributes(G, D0 / 2, "radius")
79+
80+
G.nodes[1]["pos"] = np.asarray(p0) + np.asarray(direction)*L
81+
new_node_ix = 1
82+
83+
#### Iteration to create the other vessels following a given law
84+
85+
# list of the vessels from the previous generation
86+
previous_edges = [(0, 1)]
87+
88+
for igen in range(1, N):
89+
current_edges = []
90+
for e in previous_edges:
91+
# Parent vessel properties
92+
previousvessel = [G.nodes[e[0]]["pos"], G.nodes[e[1]]["pos"]]
93+
D0 = G.edges[e]["radius"] * 2
94+
95+
# Daughter diameters
96+
D2 = D0 * (gam**3 + 1) ** (-1 / 3)
97+
D1 = gam * D2
98+
# Daughter lengths
99+
L2 = lmbda * D2
100+
L1 = lmbda * 0.6 * D1
101+
102+
if uniform_lengths:
103+
L1, L2 = L, L
104+
# Bifurcation angles
105+
# angle for the smallest vessel
106+
cos1 = (D0**4 + D1**4 - (D0**3 - D1**3) ** (4 / 3)) / (
107+
2 * D0**2 * D1**2
108+
)
109+
angle1 = np.degrees(np.arccos(cos1))
110+
# angle for the biggest vessel
111+
cos2 = (D0**4 + D2**4 - (D0**3 - D2**3) ** (4 / 3)) / (
112+
2 * D0**2 * D2**2
113+
)
114+
angle2 = np.degrees(np.arccos(cos2))
115+
116+
# randomly chopse which vessel go to the right/left
117+
if not signs:
118+
sign1 = random.choice((-1, 1))
119+
else:
120+
sign1 = signs[0]
121+
del signs[0]
122+
sign2 = -1 * sign1
123+
124+
branch1 = [sign1, angle1, L1]
125+
branch2 = [sign2, angle2, L2]
126+
127+
parent_edge_v2 = e[1] # vertex we want to connect to
128+
129+
# Check that the new edge does not overlap other edges
130+
for sign, angle, L in [branch1, branch2]:
131+
new_node_pos = compute_vessel_endpoint(
132+
previousvessel, normal(*previousvessel[1]), sign * angle, L
133+
)
134+
135+
all_pos = np.asarray(list(nx.get_node_attributes(G, "pos").values()))
136+
intersecting_lines = 0
137+
C = Point(all_pos[parent_edge_v2])
138+
D = Point(new_node_pos)
139+
140+
non_neighbor_edges = copy.deepcopy(list(G.edges()))
141+
142+
neighbor_edges = copy.deepcopy(
143+
list(G.in_edges(parent_edge_v2)) + list(G.out_edges(parent_edge_v2))
144+
)
145+
146+
for en in list(set(neighbor_edges)):
147+
non_neighbor_edges.remove(en)
148+
149+
for v1, v2 in non_neighbor_edges:
150+
A = Point(all_pos[v1])
151+
B = Point(all_pos[v2])
152+
if doIntersect(A, B, C, D):
153+
intersecting_lines += 1
154+
155+
156+
#if no edges overlap with this new one
157+
# we go ahead and add it
158+
if intersecting_lines < 1:
159+
new_node_ix += 1
160+
161+
new_edge = (e[1], new_node_ix)
162+
G.add_edge(*new_edge)
163+
164+
# Set the location according to length and angle
165+
G.nodes[new_node_ix]["pos"] = new_node_pos
166+
167+
# Set radius
168+
G.edges[new_edge]["radius"] = D1 / 2
169+
170+
# Add to the pool of vessels for this generation
171+
current_edges.append(new_edge)
172+
173+
previous_edges = current_edges
174+
175+
return G
176+
177+
178+
class Point:
179+
def __init__(self, xx):
180+
self.x = xx[0]
181+
self.y = xx[1]
182+
183+
# Shamelessly copied from stackoverflow :-)
184+
# Given three collinear points p, q, r, the function checks if
185+
# point q lies on line segment 'pr'
186+
def onSegment(p, q, r):
187+
if (
188+
(q.x <= max(p.x, r.x))
189+
and (q.x >= min(p.x, r.x))
190+
and (q.y <= max(p.y, r.y))
191+
and (q.y >= min(p.y, r.y))
192+
):
193+
return True
194+
return False
195+
196+
197+
def orientation(p, q, r):
198+
# to find the orientation of an ordered triplet (p,q,r)
199+
# function returns the following values:
200+
# 0 : Collinear points
201+
# 1 : Clockwise points
202+
# 2 : Counterclockwise
203+
204+
# See https://www.geeksforgeeks.org/orientation-3-ordered-points/amp/
205+
# for details of below formula.
206+
207+
val = (float(q.y - p.y) * (r.x - q.x)) - (float(q.x - p.x) * (r.y - q.y))
208+
if val > 0:
209+
210+
# Clockwise orientation
211+
return 1
212+
elif val < 0:
213+
214+
# Counterclockwise orientation
215+
return 2
216+
else:
217+
218+
# Collinear orientation
219+
return 0
220+
221+
222+
# This code is contributed by Ansh Riyal
223+
def doIntersect(p1, q1, p2, q2):
224+
225+
# Find the 4 orientations required for
226+
# the general and special cases
227+
o1 = orientation(p1, q1, p2)
228+
o2 = orientation(p1, q1, q2)
229+
o3 = orientation(p2, q2, p1)
230+
o4 = orientation(p2, q2, q1)
231+
232+
# General case
233+
if (o1 != o2) and (o3 != o4):
234+
return True
235+
236+
# Special Cases
237+
238+
# p1 , q1 and p2 are collinear and p2 lies on segment p1q1
239+
if (o1 == 0) and onSegment(p1, p2, q1):
240+
return True
241+
242+
# p1 , q1 and q2 are collinear and q2 lies on segment p1q1
243+
if (o2 == 0) and onSegment(p1, q2, q1):
244+
return True
245+
246+
# p2 , q2 and p1 are collinear and p1 lies on segment p2q2
247+
if (o3 == 0) and onSegment(p2, p1, q2):
248+
return True
249+
250+
# p2 , q2 and q1 are collinear and q1 lies on segment p2q2
251+
if (o4 == 0) and onSegment(p2, q1, q2):
252+
return True
253+
254+
# If none of the cases
255+
return False
256+
257+
258+
259+
def translation(p0,direction,length) :
260+
#normalise the direction vector
261+
direction=normalize(direction)
262+
263+
# compute the location of the end of the new vessel
264+
pnew=[(p0[i]+direction[i]*length) for i in range(len(p0))]
265+
return pnew
266+
267+
def rotate_in_plane(x,n,angle):
268+
""" angle : rotation degree """
269+
270+
rotation_radians = np.radians(angle)
271+
272+
rotation_axis = np.array(n)
273+
274+
rotation_vector = rotation_radians * rotation_axis
275+
276+
from scipy.spatial.transform import Rotation
277+
rotation = Rotation.from_rotvec(rotation_vector)
278+
279+
rotated_vec = rotation.apply(x)
280+
281+
return rotated_vec
282+
283+
284+
def normalize(x):
285+
return [x[i] / np.linalg.norm(x) for i in range(len(x))]
286+
287+
def project_onto_plane(x, n):
288+
d = np.dot(x, n) / np.linalg.norm(n)
289+
p = [d * normalize(n)[i] for i in range(len(n))]
290+
return [x[i] - p[i] for i in range(len(x))]
291+
292+
293+
def compute_vessel_endpoint (previousvessel, surfacenormal,angle,length) :
294+
""" From a previous vessel defined in 3D, the brain surface at the end of the previous vessel, angle and length :
295+
compute the coordinate of the end node of the current vessel """
296+
297+
# project the previous vessel in the current plane
298+
# and get the direction vector of the previous vessel
299+
pm1=previousvessel[0]
300+
p0=previousvessel[1]
301+
302+
vector_previous=[p0[i]-pm1[i] for i in range(len(pm1))]
303+
304+
previousdir = project_onto_plane(vector_previous, surfacenormal)
305+
306+
307+
# compute the direction vector of the new vessel with the angle
308+
newdir=rotate_in_plane(previousdir,surfacenormal,angle)
309+
310+
# compute the location of the end of the new vessel
311+
pnew=translation(p0,newdir,length)
312+
313+
314+
return pnew

0 commit comments

Comments
 (0)