fetoflow 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fetoflow/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ from .geometry_utils import (
2
+ create_geometry,
3
+ calcLength,
4
+ create_anastomosis,
5
+ create_venous_mesh,
6
+ calculate_branching_angles
7
+ )
8
+ from .bc_utils import generate_boundary_conditions
9
+ from .matrix_builder import create_matrices, create_small_matrices
10
+ from .resistance_utils import (
11
+ calculate_capillary_equivalent_resistance,
12
+ calculate_resistance,
13
+ calculate_convolute_resistance,
14
+ calculate_viscosity_factor_from_radius
15
+ )
16
+ from .file_parsing_utils import read_nodes, read_elements, define_fields_from_files
17
+ from .helper_functions import (
18
+ getRadii,
19
+ getEdgeData,
20
+ getNode,
21
+ getNumVessels,
22
+ getRadius,
23
+ getVesselLength,
24
+ )
25
+ from .pressure_flow_utils import (
26
+ pressures_and_flows,
27
+ )
28
+ from.solve_utils import(
29
+ solve_small_system,
30
+ solve_system,
31
+ update_small_matrix,
32
+ iterative_solve_small
33
+ )
fetoflow/bc_utils.py ADDED
@@ -0,0 +1,107 @@
1
+ from warnings import warn
2
+ def generate_boundary_conditions(inlet_pressure=None, inlet_flow=None, outlet_pressure=None):
3
+ # TODO: IF ONLY INLET FLOW, SET OUTLET PRESSURE TO 0 AND WE LOOK AT THE PRESSURE DROP. THEN NOTE THIS SOMEWHERE.
4
+ if inlet_pressure and inlet_flow:
5
+ raise TypeError(f"Cannot have both an inlet pressure and inlet flow defined. Inlet pressure: {inlet_pressure}Inlet Flow: {inlet_flow}")
6
+ elif inlet_pressure is None and inlet_flow is None:
7
+ raise TypeError("No inlet boundary condition defined. Must define one of inlet_pressure or inlet flow for a valid boundary condition.")
8
+ bcs = {}
9
+
10
+ if outlet_pressure is None and inlet_flow is None:
11
+ raise TypeError(
12
+ "No valid outlet pressure for boundary condition. "
13
+ "Currently Reprosim does not support outlet flow boundary conditions and must have a defined outlet pressure."
14
+ )
15
+ elif outlet_pressure is None and inlet_flow is not None:
16
+ warn("No outlet pressure defined with flow inlet. Setting outlet pressure to 0.")
17
+
18
+
19
+ # Outlet Pressure
20
+ if outlet_pressure:
21
+ if not (isinstance(outlet_pressure, int) or isinstance(outlet_pressure, float)):
22
+ raise TypeError(f"Outlet pressure type '{type(outlet_pressure)}' is not valid. Valid types include float or int.")
23
+ elif not outlet_pressure >= 0:
24
+ raise ValueError(f"Outlet pressure value of '{outlet_pressure}' is not valid. Must be greater than or equal to 0.")
25
+
26
+ bcs["outlet"] = {"pressure": outlet_pressure}
27
+
28
+ # TODO Add outlet flows potentially?
29
+
30
+ # Inlet Pressure
31
+ if inlet_pressure:
32
+ if not (isinstance(inlet_pressure, int) or isinstance(inlet_pressure, float) or isinstance(inlet_pressure, dict)):
33
+ raise TypeError(
34
+ f"Inlet pressure type '{type(inlet_pressure)}' is not valid. Valid types include float or int for single inputs and dict for multiple inputs."
35
+ )
36
+ elif isinstance(inlet_pressure, int) or isinstance(inlet_pressure, float):
37
+ if not inlet_pressure > 0:
38
+ raise ValueError(f"Invalid inlet pressure of '{inlet_pressure}' Pa. Must be greater than 0.")
39
+ bcs["inlet"] = {"pressure": inlet_pressure}
40
+ elif isinstance(inlet_pressure, dict):
41
+ inlet_pressures = {}
42
+ for key, value in inlet_pressure.items():
43
+ if not isinstance(key, int) or not key > 0:
44
+ raise ValueError(f"Invalid Node Id {key} for multiple inlet pressures. Must be a positive integer.")
45
+ if not (isinstance(value, float) or isinstance(value, int)) or not value > 0:
46
+ raise ValueError(f"Invalid inlet pressure for node {key} of '{inlet_pressure}' Pa. Must be greater than 0.")
47
+ # Fix indexing here (1-based for IPNODE and IPELEM to 0 based for NetworkX)
48
+ # TODO: Update this if we change input file types.
49
+ inlet_pressures[key - 1] = value
50
+ bcs["inlet"] = {"pressure": inlet_pressures}
51
+
52
+ # Inlet flow
53
+ if inlet_flow:
54
+ if not (isinstance(inlet_flow, int) or isinstance(inlet_flow, float) or isinstance(inlet_flow, dict)):
55
+ raise TypeError(
56
+ f"Inlet flow type '{type(inlet_flow)}' is not valid. Valid types include float or int for single inputs and dict for multiple inputs."
57
+ )
58
+ elif isinstance(inlet_flow, int) or isinstance(inlet_flow, float):
59
+ if not inlet_flow > 0:
60
+ raise ValueError(f"Invalid inlet flow of '{inlet_flow}' Pa. Must be greater than 0.")
61
+ bcs["inlet"] = {"flow": inlet_flow}
62
+ elif isinstance(inlet_flow, dict):
63
+ inlet_flows = {}
64
+ for key, value in inlet_flow.items():
65
+ if not isinstance(key, int) or not key > 0:
66
+ raise ValueError(f"Invalid Element Id {key} for multiple inlet flows. Must be a positive integer.")
67
+ if not (isinstance(value, float) or isinstance(value, int)) or not value > 0:
68
+ raise ValueError(f"Invalid inlet flow for element {key} of '{inlet_flow}'. Must be greater than 0.")
69
+ # Fix indexing here (1-based for IPNODE and IPELEM to 0 based for NetworkX)
70
+ # TODO: Update this if we change input file types.
71
+ inlet_flows[key - 1] = value
72
+ bcs["inlet"] = {"flow": inlet_flows}
73
+
74
+ return bcs
75
+
76
+
77
+ """
78
+ # TODO: Some form that allows:
79
+ - inlet type
80
+ - inlet BC's
81
+
82
+ - multiple inlet BC's either flow or pressure (for now say that can only do same type cause otherwise this is hell)
83
+ - if multiple inlet BC's:
84
+ - must provide the inlet ID's they are referring to. MATCH THESE ID's with Placentagen.
85
+ NOTE: THIS MEANS WE HAVE TO UPDATE INDEXING SLIGHTLY TO MATCH, AS IPELEM FILES HAVE 1-BASED INDEXING AND NETWORKX HAS 0 BASED INDEXING
86
+
87
+
88
+
89
+ - outlet pressure.
90
+ - defaults to setting this outlet pressure at all outlet nodes (this makes the most sense to me, can be changed later)
91
+
92
+
93
+
94
+ function inputs:
95
+
96
+ inlet_pressure - value or dict[node_id, pressure]
97
+ inlet_flow - value or dict [edge_id, flow]
98
+ outlet_pressure - value
99
+
100
+
101
+
102
+ function output:
103
+
104
+ bcs: dict[inlet/outlet, dict[pressure/flow, value or dict[node/element_id, pressure/flow]]]
105
+ (if pressure must be node in inner dict, if flow must be element.)
106
+
107
+ """
@@ -0,0 +1,77 @@
1
+ def read_nodes(filename):
2
+ if not isinstance(filename,str):
3
+ raise(TypeError("File name should be a string!"))
4
+ if not filename[-7:] == ".ipnode":
5
+ raise(TypeError("This function expects a .ipnode file."))
6
+ with open(filename, "r") as f:
7
+ lines = f.readlines()
8
+ nodes = {}
9
+ numVars = int(lines[4].split()[-1])
10
+ baseStep = numVars + 2
11
+ i = 4 + 2 * numVars + 2
12
+ while i < len(lines):
13
+ node_id = int(lines[i].split()[-1])
14
+ nversions = int(lines[i + 1].split()[-1])
15
+ i_step = baseStep + nversions * numVars + (nversions * numVars if nversions > 1 else 0)
16
+ c_step = numVars * nversions - 1
17
+ coords = []
18
+ for c in range(1 + nversions, i_step, c_step):
19
+ coord = lines[i + c].split()[-1]
20
+ coords.append(float(coord))
21
+ nodes[node_id - 1] = coords # 0-based indexing for the networkX geometry.
22
+ i += i_step
23
+ return nodes
24
+
25
+
26
+ def read_elements(filename):
27
+ if not isinstance(filename,str):
28
+ raise(TypeError("File name should be a string!"))
29
+ if not filename[-7:] == ".ipelem":
30
+ raise(TypeError("This function expects a .ipelem file."))
31
+
32
+ with open(filename, "r") as f:
33
+ lines = f.readlines()
34
+ elems = []
35
+ i = 5
36
+ while i < len(lines):
37
+ intraElementStep = 5
38
+ nodes = tuple(int(x) - 1 for x in lines[i + intraElementStep].split()[-2:]) # Translate to 0-based indexing.
39
+ elems.append(nodes)
40
+ while len(lines[i].split()) != 0:
41
+ i += 1
42
+ if i + 1 == len(lines):
43
+ return elems
44
+ i += 1
45
+ return elems
46
+
47
+
48
+ def define_fields_from_files(files: dict[str]):
49
+ """
50
+ Defines field(s) as specified in ipfield file(s).
51
+ """
52
+ if not isinstance(files, dict):
53
+ raise (TypeError("files must be a dictionary in the format files[field_name] = filename"))
54
+ fields = {}
55
+ for field in files.keys():
56
+ file_name = files[field]
57
+ if not file_name[-7:] == ".ipfiel":
58
+ ext_start = -(str.__reversed__(file_name).find(".") + 1)
59
+ if ext_start is not None:
60
+ raise(TypeError(f"This function expects a .ipfiel file, got {file_name[ext_start:]}"))
61
+ else:
62
+ raise(TypeError(f"This function expects a .ipfiel file. No file extension found."))
63
+ with open(file_name, "r") as f:
64
+ i = 7 # ignore metadata
65
+ lines = f.readlines()
66
+ max_digits = len(lines[3][lines[3].find(":") + 1 :].strip())
67
+ currentField = {}
68
+ while i < len(lines):
69
+ j = i + 2
70
+ id = (
71
+ int(lines[i][-max_digits-1:].strip()) - 1
72
+ ) # assuming for now these correspond to element ids, -1 to 0 based
73
+ val = float(lines[j][lines[j].find(":")+1:].strip())
74
+ currentField[id] = val
75
+ i += 4
76
+ fields[field] = currentField
77
+ return fields
@@ -0,0 +1,322 @@
1
+ import networkx as nx
2
+ import numpy as np
3
+
4
+
5
+ def create_geometry(
6
+ nodes,
7
+ elements,
8
+ inlet_radius,
9
+ strahler_ratio_arteries,
10
+ arteries_only=False,
11
+ outlet_vein_radius=None,
12
+ strahler_ratio_veins=None,
13
+ fields=None,
14
+ default_mu=0.33600e-02,
15
+ default_hematocrit=0.45,
16
+ ):
17
+ G = nx.DiGraph()
18
+ num_terminal_arterial_nodes = 0
19
+ # Read in Nodes and Edges
20
+ for node_id, coordinates in nodes.items():
21
+ G.add_node(node_id, x=coordinates[0], y=coordinates[1], z=coordinates[2])
22
+ for edge_id, (node_from, node_to) in enumerate(elements):
23
+ if fields:
24
+ res = fields.get("resistance")
25
+ if res:
26
+ res = fields.get(edge_id, 0)
27
+ else:
28
+ res = 0
29
+ # .get returns None by default if not found
30
+ radius = fields.get("radius")
31
+ if radius:
32
+ radius = radius.get(edge_id)
33
+ else:
34
+ radius = None
35
+ else:
36
+ res = 0.0
37
+ radius = None
38
+ length = calcLength(G, node_from, node_to)
39
+ G.add_edge(
40
+ node_from,
41
+ node_to,
42
+ edge_id=edge_id,
43
+ resistance=res,
44
+ length=length,
45
+ radius=radius,
46
+ strahler=None,
47
+ vessel_type="artery",
48
+ bifurcation_angle=0,
49
+ mu=default_mu,
50
+ hematocrit=default_hematocrit,
51
+ viscosity_factor=1,
52
+ )
53
+
54
+ # Find all input nodes (to ensure we give every element a strahler ordering)
55
+ input_nodes = []
56
+ for node in G.nodes():
57
+ if len(G.in_edges(node)) == 0:
58
+ input_nodes.append(node)
59
+ # print(node)
60
+ # Add artery radii via strahler ordering
61
+ max_strahler = 0
62
+ for input_node in input_nodes:
63
+ for u, v in G.out_edges(input_node):
64
+ G = update_strahlers(G, u, v) # update for each input node should work
65
+ max_strahler = max(max_strahler, G[u][v]["strahler"])
66
+
67
+ if fields and fields.get("radius"):
68
+ for u, v in G.edges():
69
+ if G[u][v]["radius"] is None:
70
+ elem_strahler = G[u][v]["strahler"]
71
+ # need to update R according to the last specified radius in the subtree
72
+ radius_found = False
73
+ inlet_radius_updated = inlet_radius
74
+ out_node = u
75
+ sub_tree_strahler = max_strahler
76
+ while not radius_found:
77
+ edges_to_check = list(G.in_edges(out_node))
78
+ # should only be one edge coming in
79
+ if len(edges_to_check) == 0:
80
+ radius_found = True # no previously set radii in vessel's predecessors
81
+ else:
82
+ in_node = edges_to_check[0][0]
83
+ current_rad = G[in_node][out_node]["radius"]
84
+ if current_rad is not None and current_rad != 0:
85
+ inlet_radius_updated = current_rad
86
+ radius_found = True
87
+ sub_tree_strahler = G[in_node][out_node]["strahler"]
88
+ else:
89
+ out_node = in_node
90
+ G[u][v]["radius"] = inlet_radius_updated * strahler_ratio_arteries ** (elem_strahler - sub_tree_strahler)
91
+ else:
92
+ for u, v in G.edges():
93
+ elem_strahler = G[u][v]["strahler"]
94
+ G[u][v]["radius"] = inlet_radius * strahler_ratio_arteries ** (elem_strahler - max_strahler)
95
+
96
+ # Create the venous mesh
97
+ if not arteries_only:
98
+ num_artery_nodes = G.number_of_nodes() # use for scaling to keep numeric based indexing
99
+ num_artery_edges = G.number_of_edges()
100
+ # Get terminal nodes
101
+ terminal_nodes = [node for node, out_degree in G.out_degree() if out_degree == 0]
102
+ num_terminal_arterial_nodes = len(terminal_nodes)
103
+ venous_mesh = create_venous_mesh(
104
+ G,
105
+ num_artery_nodes,
106
+ num_artery_edges,
107
+ num_terminal_arterial_nodes,
108
+ outlet_vein_radius,
109
+ strahler_ratio_veins,
110
+ max_strahler,
111
+ )
112
+ # list of artery terminal node indices
113
+ # add venous mesh to graph
114
+ assert max(G.nodes) < min(venous_mesh.nodes), "Venous mesh node ids overlap with arterial node ids."
115
+ G = nx.compose(G, venous_mesh)
116
+ # Add edges to each terminal node with equivalent capillary network resistance
117
+ edge_id_tracker = len(elements) # Use this to easily increment edge_id correctly for the new edges I am adding.
118
+ for i, arterial_node in enumerate(terminal_nodes):
119
+ venous_node = arterial_node + num_artery_nodes
120
+
121
+ assert len(G.in_edges(arterial_node)) == 1, f"Terminal artery node has {len(G.in_edges(arterial_node))} entering it, should be 1 only."
122
+ assert len(G.out_edges(venous_node)) == 1, f"Terminal vein node has {len(G.out_edges(venous_node))} exiting it, should be 1 only."
123
+
124
+ G.add_edge(
125
+ arterial_node,
126
+ venous_node,
127
+ edge_id=edge_id_tracker + i,
128
+ resistance=None,
129
+ length=None,
130
+ radius=0.0,
131
+ strahler=0.0,
132
+ vessel_type="capillary_equivalent",
133
+ mu=default_mu,
134
+ hematocrit=default_hematocrit,
135
+ viscosity_factor=1,
136
+
137
+ )
138
+ # Update length of these vessels - we calculate the resistance in 'calculate_resistance()' function.
139
+ G[arterial_node][venous_node]["length"] = calcLength(G, arterial_node, venous_node)
140
+ # Leave radius as 0 - this allows visualisations not including the capillary networks,
141
+ # which would only show a single vessel anyway (not the tree of the intermediate and terminal villi).
142
+
143
+ return G
144
+
145
+
146
+ def create_venous_mesh(
147
+ G,
148
+ num_artery_nodes,
149
+ num_artery_edges,
150
+ num_terminal_arterial_nodes,
151
+ outlet_vein_radius,
152
+ strahler_ratio_veins,
153
+ max_strahler,
154
+ ):
155
+ venous_mesh = G.copy()
156
+ # first n/2 nodes = artery nodes going down.
157
+ # Next n/2 nodes = vein nodes in same order artery nodes were (i.e. likely inlet first, getting smaller as we go).
158
+ nx.relabel_nodes(venous_mesh, lambda node_id: node_id + num_artery_nodes, copy=False) # 0-based indexing works here
159
+ # Update radii
160
+ for u, v in venous_mesh.edges():
161
+ # If anastomosis, remove edge as it doesn't exist in the veins
162
+ if venous_mesh[u][v]["vessel_type"] == "anastomosis":
163
+ G.remove_edge(u, v)
164
+ continue
165
+
166
+ # Edge ids for veins are after capillaries
167
+ venous_mesh[u][v]["edge_id"] = venous_mesh[u][v]["edge_id"] + num_artery_edges + num_terminal_arterial_nodes
168
+ venous_mesh[u][v]["vessel_type"] = "vein"
169
+ # Strahler ordering for veins
170
+ elemStrahler = venous_mesh[u][v]["strahler"]
171
+ venous_mesh[u][v]["radius"] = outlet_vein_radius * strahler_ratio_veins ** (elemStrahler - max_strahler)
172
+ # Reverse the direction of all arcs
173
+ venous_mesh = venous_mesh.reverse()
174
+
175
+ return venous_mesh
176
+
177
+
178
+ def update_strahlers(G, node_in, node_out):
179
+ # Input the input node(s) - or call on each input node. Updates the strahler field.
180
+ # This is a recursive function and will be slow for now.
181
+ child_edges = G.out_edges(node_out)
182
+ # # Base case: No children, strahler == 1.
183
+ if len(child_edges) == 0:
184
+ G[node_in][node_out]["strahler"] = 1
185
+ return G
186
+
187
+ max_child_strahler = 0
188
+ max_child_strahler_count = 0
189
+
190
+ for u, v in child_edges:
191
+ if G[u][v]["strahler"] is None:
192
+ G = update_strahlers(G, u, v) # Returns graph object with strahler updated.
193
+
194
+ if G[u][v]["strahler"] > max_child_strahler:
195
+ max_child_strahler = G[u][v]["strahler"]
196
+ max_child_strahler_count = 1
197
+ elif G[u][v]["strahler"] == max_child_strahler:
198
+ max_child_strahler_count += 1
199
+
200
+ assert max_child_strahler_count > 0
201
+ # Actually assign this arc's strahler now.
202
+ if max_child_strahler_count == 1:
203
+ G[node_in][node_out]["strahler"] = max_child_strahler
204
+ else:
205
+ G[node_in][node_out]["strahler"] = max_child_strahler + 1 # 2 arcs coming in with same max value
206
+
207
+ return G
208
+
209
+
210
+ def calcLength(G, u, v):
211
+ return np.sqrt(np.sum([(G.nodes[u][coord] - G.nodes[v][coord]) ** 2 for coord in ["x", "y", "z"]])) / 1000 # mm to m!
212
+ # TODO: Fix unit conversions and makr work for anything etc. i.e. specify units somewhere as an input argument at the start
213
+
214
+ def create_anastomosis(G, node_from, node_to, radius=None, mu=0.33600e-02):
215
+ # NOTE HERE: RADIUS IS IN mm!!!!!
216
+ # Todo: make sure this is clear.
217
+ # TODO: DIGRAPH STUFF???. No we can just have a negative flow along the anastomosis.
218
+ # TODO: probably write this as 2 separate functions, this one which is in here with the graph stuff, and one which is user-facing and calls this one
219
+ u = node_from - 1
220
+ v = node_to - 1 # Update from 1- to 0-based indexing\
221
+
222
+ if u not in G:
223
+ raise ValueError(
224
+ f"Node {node_from} (ipnode indexing)/Node {u} (networkX indexing) does not exist in the networkX graph. Perhaps you need to call create_geometry() first?"
225
+ )
226
+ if v not in G:
227
+ raise ValueError(
228
+ f"Node {node_to} (ipnode indexing)/Node {v} (networkX indexing) does not exist in the networkX graph. Perhaps you need to call create_geometry() first?"
229
+ )
230
+ if u == v:
231
+ raise ValueError(f"Anastomosis cannot connect the same node to itself. Node number is {node_from} (ipnode indexing)/{u} (networkX indexing).")
232
+
233
+ # only exists as aterial connection
234
+ G.add_edge(
235
+ u,
236
+ v,
237
+ edge_id=G.number_of_edges(),
238
+ resistance=0.0,
239
+ length=None,
240
+ radius=0.0,
241
+ strahler=0.0,
242
+ vessel_type="anastomosis",
243
+ mu=mu,
244
+ hematocrit=0.45,# TODO PARAMETERISE
245
+ viscosity_factor=1
246
+ )
247
+ # Old implementation:
248
+ # - defines a radius of the anastomosis which is used
249
+ # Our alternative:
250
+ # - Provide a warning if this happens, but just use the maxiumum radii of the vessels leaving the nodes connecting the anastomosis.
251
+ # TODO: Confirm this is fine implementation
252
+
253
+ G[u][v]["length"] = calcLength(G, node_from, node_to)
254
+
255
+ # For strahler, just take the max of any child strahler.
256
+ max_child_radius = 0.0
257
+ max_child_strahler = 0.0
258
+ for node in (u, v):
259
+ for child_u, child_v in G.out_edges(node):
260
+ child_radius = G[child_u][child_v]["radius"]
261
+ child_strahler = G[child_u][child_v]["strahler"]
262
+ if not child_radius or not child_strahler:
263
+ raise ValueError("Strahler values and radii have not been set yet. make sure you do this before creating an anastomosis.")
264
+ max_child_radius = max(max_child_radius, child_radius)
265
+ max_child_strahler = max(max_child_strahler, child_strahler)
266
+
267
+ if radius:
268
+ if not (isinstance(radius, int) or isinstance(radius, float)):
269
+ raise ValueError(f"Hyrtl anastomosis radius is invalid type {type(radius)}. Valid types are float or int")
270
+
271
+ G[u][v]["radius"] = radius / 1000 # mm to m!
272
+ else:
273
+ G[u][v]["radius"] = max_child_radius
274
+ # Strahler
275
+ G[u][v]["strahler"] = max_child_strahler # The code will previously break if strahlers have not been already set.
276
+ # Resistance calculation: #TODO make sure it updates properly if we have other viscosities etc.
277
+ # Note: if calcualte_resistance() is called after this function, the result will be overwritten. This is probably the order we want:
278
+ # - calculate_geometry()
279
+ # - create_venous_mesh()
280
+ # - create_anatsomosis()
281
+ # - calculate_resistance()
282
+ G[u][v]["resistance"] = 8 * mu * G[u][v]["length"] / (np.pi * G[u][v]["radius"] ** 4)
283
+
284
+ return G
285
+
286
+
287
+ def update_geometry_with_pressures_and_flows(G, pressures, flows):
288
+ for node_id in G.nodes():
289
+ G.nodes[node_id]["pressure"] = pressures.loc[node_id]["pressure"]
290
+ for u, v in G.edges():
291
+ G[u][v]["flow"] = flows.loc[G[u][v]["edge_id"]]["flow"]
292
+ return G
293
+
294
+ def calculate_branching_angles(G):
295
+ # double check...
296
+ for n in G.nodes():
297
+ out_edges = list(G.out_edges(n))
298
+ in_edges = list(G.in_edges(n))
299
+ if len(out_edges) > 1:
300
+ for __,out_node in out_edges:
301
+ out_vec = np.array([G.nodes[n][coord] - G.nodes[out_node][coord] for coord in ["x","y","z"]])
302
+ out_norm= out_vec/np.linalg.norm(out_vec)
303
+ in_vec = np.array([G.nodes[in_edges[0][0]][coord] - G.nodes[n][coord] for coord in ["x","y","z"]])
304
+ in_norm = in_vec/np.linalg.norm(in_vec)
305
+ dot = np.clip(in_norm @ out_norm,-1,1)
306
+ G[n][out_node]["bifurcation_angle"] = np.pi - np.arccos(dot) #phi_j from Mynard
307
+
308
+ elif len(in_edges) > 1:
309
+ for in_node,__ in in_edges:
310
+ in_vec = np.array([G.nodes[in_node][coord] - G.nodes[n][coord] for coord in ["x","y","z"]])
311
+ in_norm = in_vec/np.linalg.norm(in_vec)
312
+ out_vec = np.array([G.nodes[n][coord] - G.nodes[out_edges[0][1]][coord] for coord in ["x","y","z"]])
313
+ out_norm = out_vec/np.linalg.norm(out_vec)
314
+ dot = np.clip(in_norm @ out_norm,-1,1)
315
+ G[in_node][n]["bifurcation_angle"] = np.pi - np.arccos(dot) #phi_j from Mynard
316
+ else:
317
+ continue
318
+ return
319
+
320
+
321
+
322
+
@@ -0,0 +1,61 @@
1
+ import networkx as nx
2
+
3
+
4
+ def getRadii(G: nx.digraph):
5
+ """
6
+ Returns all radii of all vessels in the Arterial tree as a dictionary:
7
+ (nodeIn, nodeOut) : radius
8
+ """
9
+ radii = {}
10
+ for u, v in G.edges():
11
+ radii[(u, v)] = G[u][v]["radius"]
12
+ return radii
13
+
14
+
15
+ def getRadius(G: nx.digraph, nodes: tuple = None):
16
+ if nodes is not None:
17
+ u, v = nodes
18
+ return G[u][v]["radius"]
19
+ else:
20
+ raise ("Enter node values!")
21
+
22
+
23
+ def getNode(G, nodeID):
24
+ return G.nodes()[nodeID]
25
+
26
+
27
+ def getEdgeData(G, edge):
28
+ u, v = edge
29
+ return G.get_edge_data(u, v)
30
+
31
+
32
+ def getVesselLength(G, initialVessel: tuple):
33
+ """
34
+ Check this
35
+ """
36
+ u, v = initialVessel
37
+ length = G[u][v]["length"]
38
+ out = G.out_edges(v)
39
+ if len(out) == 1:
40
+ length += getVesselLength(G, (v, out[0]))
41
+ else:
42
+ return length
43
+
44
+
45
+ def getNumVessels(G):
46
+ """
47
+ Returns number of distinct vessels in graph
48
+ """
49
+ successors = nx.dfs_sucessors(G, source=1) # check if 0?
50
+ count = 0
51
+ if successors.get(1) == 1:
52
+ count += 1
53
+ for __, nodesOut in successors.items():
54
+ unique = len(nodesOut)
55
+ if unique > 1:
56
+ count += unique
57
+ return count
58
+
59
+
60
+ def branching_analytics():
61
+ pass