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 +33 -0
- fetoflow/bc_utils.py +107 -0
- fetoflow/file_parsing_utils.py +77 -0
- fetoflow/geometry_utils.py +322 -0
- fetoflow/helper_functions.py +61 -0
- fetoflow/matrix_builder.py +207 -0
- fetoflow/pressure_flow_utils.py +361 -0
- fetoflow/resistance_utils.py +197 -0
- fetoflow/solve_utils.py +378 -0
- fetoflow-0.1.0.dist-info/METADATA +31 -0
- fetoflow-0.1.0.dist-info/RECORD +14 -0
- fetoflow-0.1.0.dist-info/WHEEL +5 -0
- fetoflow-0.1.0.dist-info/licenses/LICENSE +201 -0
- fetoflow-0.1.0.dist-info/top_level.txt +1 -0
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
|