West-Brown-Enquist distribution network and Kleiber scaling
The distribution network of West, Brown, and Enquist, a space-filling tree of invariant capillaries.
- Name:
west_brown_enquist - Field:
biology/physiology - Formalism: static solution
- References: West, Brown, and Enquist 1997; Ribeiro and Netto 2024
Select this system
from skcomplex.systems import create_simulation, create_system, describe
system = create_system('west_brown_enquist')
entry = describe('west_brown_enquist')
trajectory = create_simulation('west_brown_enquist').prepare().run().trajectory_
Factory
@register_system(
field='biology/physiology',
tags=('allometry', 'kleiber law', 'fractal network', 'static', 'metabolic scaling'),
references=('West, Brown, and Enquist 1997', 'Ribeiro and Netto 2024'),
observables=_network_totals,
)
def west_brown_enquist(depth: int = 6, *, n: int = 2, r_c: float = 1.0, l_c: float = 1.0) -> System:
"""Return the distribution network of West, Brown, and Enquist, a space-filling tree of invariant capillaries.
The tree branches `n` ways at every level down to the capillaries at level `depth`. The metabolic rate is
proportional to the number of capillaries, `n^depth`, and the body mass to the blood volume, which the
area-preserving and space-filling vessels make `V = pi r_c^2 l_c n^depth (n^(depth / 3) - 1) / (n^(1 / 3) - 1)`,
which grows as `n^(4 depth / 3)` for deep trees. So the metabolic rate scales as the mass to the power 3/4,
Kleiber's law, which the exponent between consecutive depths approaches from below. An `Experiment` over `depth`
fits it.
Args:
depth:
The level of the capillaries, the number of branchings.
n:
The branching ratio.
r_c:
The radius of a capillary.
l_c:
The length of a capillary.
Returns:
system:
The system, with the radii `vessels.r`, lengths `vessels.l`, and volumes `vessels.volume` on the edges of
the tree and the observables `blood_volume` and `capillaries` of the catalog, which a run passes in
`observables`.
Examples:
>>> from skcomplex.systems.biology.physiology import west_brown_enquist
>>> west_brown_enquist(3).elements[0].prepare().size_
14
"""
graph = nx.balanced_tree(n, depth)
space = NetworkSpace(graph).prepare()
_, children = space.edges_[None]
levels = nx.single_source_shortest_path_length(graph, 0)
vessels = GroupElement(
'vessels',
element=BranchingVesselElement,
space=space,
on='edges',
values={
'level': np.array([levels[int(child)] for child in children], dtype=float),
'depth': float(depth),
'n': float(n),
'r_c': r_c,
'l_c': l_c,
},
)
return System(elements=[vessels], space=space, time=StaticTime())
Components
Elements:
BranchingVesselElement, fromElement
class BranchingVesselElement(Element):
"""Vessel of a space-filling branching network, sized by its level so that the network minimizes transport energy.
Every vessel branches into `n` daughters. Area-preserving branching gives the radii the ratio `n^(-1/2)` between
levels and space filling the lengths the ratio `n^(-1/3)`, and the capillaries at the last level `depth` have the
same radius `r_c` and length `l_c` in every organism, so `r = r_c n^((depth - level) / 2)`, `l = l_c n^((depth -
level) / 3)`, and the volume of the vessel is `pi r^2 l`.
Args:
label:
The label of the vessel or of the group on the edges.
visual:
How the vessel is drawn, or `None` for `default_visual_`.
r:
The radius.
l:
The length.
volume:
The volume.
terminal:
Whether the vessel is a capillary.
level:
The level of the vessel, 1 for the aorta's daughters and `depth` for the capillaries.
depth:
The level of the capillaries.
n:
The branching ratio.
r_c:
The radius of a capillary.
l_c:
The length of a capillary.
Examples:
>>> from skcomplex.systems.biology.physiology import BranchingVesselElement
>>> BranchingVesselElement('vessel', level=3.0).level
3.0
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
r: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
l: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
volume: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
terminal: Annotated[float, StateSpec(domain=RealSet.closed(0, 1))] = 0.0,
level: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
depth: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
n: Annotated[float, ParameterSpec(domain=RealSet.open(1, oo))] = 2.0,
r_c: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
l_c: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
) -> None:
super().__init__(label, visual=visual)
self.r = r
self.l = l
self.volume = volume
self.terminal = terminal
self.level = level
self.depth = depth
self.n = n
self.r_c = r_c
self.l_c = l_c
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the radius, the length, the volume, and the capillary indicator of the vessel.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
height = s.depth - s.level
return [
EquationLaw(s.r == s.r_c * s.n ** (height / 2), target=s.r),
EquationLaw(s.l == s.l_c * s.n ** (height / 3), target=s.l),
EquationLaw(s.volume == pi * s.r**2 * s.l, target=s.volume),
EquationLaw(s.terminal == where(s.level > s.depth - 1 / 2, 1, 0), target=s.terminal),
]