API Reference

Every public name of Vasculature.jl, generated from its docstrings when the site is built. Four stages, each one function and one spec struct, and the tools around them. The Getting Started guide and the Examples show them in real pipelines.

Overview

Every organ walks the same four stages, each one function and one spec struct:

GrowthSpec    ─▶ grow       ─▶ the grown trees
FlowSpec      ─▶ flow       ─▶ every vessel's flow, every voxel's ground truth
TransportSpec ─▶ transport  ─▶ the iodine in every vessel and bed, every second
PhantomSpec   ─▶ phantom    ─▶ the CT phantom: labels, compartments, curves, ground truth

An organ is one instance of each spec (HEART_GROWTH, HEART_FLOW, …). Everything below is generated from the package's docstrings when the site is built, so it cannot drift from the code, and each entry links to its source. The Getting Started guide shows how the pieces fit together.

Grow

Grow a vascular tree from the imaged inlets into the organ's tissue.

src/spec.jl

GrowthSpecstructsource ↗
GrowthSpec(; nterm, flow_ml_min, terminal_flow_ml_min, tissue_ml, reference_ml, wall_clearance, gamma, root_caps,
           root_fixed, split, symmetry_floor, seed, T, device)
GrowthSpec(spec::GrowthSpec; kwargs...)

Everything that differs between organs when growing a vascular tree; everything else is the same for every organ (see grow). Every field is a keyword and every one is required: an organ is an instance (HEART_GROWTH, HEART_VEIN_GROWTH, LUNG_GROWTH, LUNG_VEIN_GROWTH, organs.jl), and a run varies one with the second form, e.g. GrowthSpec(LUNG_GROWTH; seed = 2, device = dev) (dev e.g. MtlArray).

  • nterm or flow_ml_min (one of them, the other nothing): the number of terminals, or the organ's blood flow (mL/min), from which the count follows as flow_ml_min / terminal_flow_ml_min.

  • terminal_flow_ml_min: the flow of one terminal, used with flow_ml_min.

  • tissue_ml: the organ's tissue volume (mL).

  • reference_ml: the tissue volume HCT-CCO's size-dependent constants are used unchanged at (its longest connection, territory reach, convergence tolerance, centreline step); they were set for adult human hearts (148.1 mL, the XCAT vmale_50 myocardium). Every organ gets size_scale = (tissue_ml / reference_ml)^(1/3). See CCOParams.size_scale.

  • wall_clearance: smallest distance (mm) from a terminal to the organ's boundary.

  • gamma: bifurcation exponent r₀^γ = r₁^γ + r₂^γ.

  • root_caps: root radius per inlet tree (mm), the imaged inlet radii; nothing leaves roots to the pressure law.

  • root_fixed: true sets the forest's radius scale so the binding root sits exactly at its cap after growth.

  • split: how a forest's inlet trees share the terminals. :free: by the growth's own competition. :bifurcation: the inlets are the daughters of one origin (the aortic root for the two coronaries), so they split the flow by the same bifurcation law as every other branch, share ∝ root_caps[k]^gamma: a tree takes terminals only while its share of the forest's flow is at or below that, so the trees grow in step and each claims the tissue it reaches. A single tree, or no root_caps, has nothing to split.

  • symmetry_floor: measured minimum daughter symmetry by mother diameter, (µm, ratio) knots; empty for none.

  • seed, T, device: random stream, number type, array device (MtlArray, CuArray, ... or identity for CPU).

src/organs.jl

HEART_GROWTHconstantsource ↗
HEART_GROWTH::Vasculature.GrowthSpec

The heart's arteries: XCAT vmale_50's left and right coronary trees grown into its myocardium, the count from the resting Fick flow over one terminal's flow (957,045 terminals), γ = 2.33. A variant: GrowthSpec(HEART_GROWTH; seed = 2).

HEART_VEIN_GROWTHconstantsource ↗
HEART_VEIN_GROWTH::Vasculature.GrowthSpec

The heart's veins: the coronary venous tree grown from XCAT's imaged veins into the coronary sinus ostium.

LUNG_GROWTHconstantsource ↗
LUNG_GROWTH::Vasculature.GrowthSpec

The lungs' arteries: one terminal per acinus (29,912, by micro-CT stereology), grown from XCAT's main pulmonary artery into the lungs.

LUNG_VEIN_GROWTHconstantsource ↗
LUNG_VEIN_GROWTH::Vasculature.GrowthSpec

The lungs' veins: one venule per acinus, grown from each lung's imaged pulmonary veins.

src/spec.jl

growfunctionsource ↗
grow(spec::GrowthSpec, domain, inlets; root = nothing, blockers = nothing, territories = nothing,
     base_groups = nothing) -> Engine

Grow a vascular forest from the imaged inlets (GXL files, one tree each: two coronary roots, a main pulmonary artery, several cerebral inlets, ...) into the tissue mask domain (a .vol file, 1 mm voxels). The algorithm is the same for every organ: HCT-CCO's staged growth (new vessels attach where its stage schedule allows), sizes tested at their final radius, no daughter turning back past 90°, terminals sampled continuously, imaged centrelines kept in place, Euclidean wall distance, full tree volume, batched on the chosen device. Only spec differs between organs.

  • root: a voxel inside the domain to start the domain's distance field from (optional).

  • blockers: (mask, origin_mm, cell_mm), a fine mask no new vessel may cross (heart chambers and great vessels, lung airways, brain ventricles, ...).

  • territories, base_groups: optional per-voxel tree-group bits and the group of each imaged segment, so each imaged vessel keeps the tissue nearest to it (see set_territories!).

src/domain.jl

MaskDomainmutable structsource ↗
MaskDomain{T}

Image-mask domain controller (upstream ImageMaskDomainCtrl<3>): the mask, its exact distance map, and the clearance min_dist (-m/--minDistanceToBorder).

set_territories!functionsource ↗
set_territories!(dom, allow::Array{UInt16,3})

Hierarchical territories on the domain grid. Bit k-1 of allow[v] lets segments of group k (see Engine.treeid) take a terminal at voxel v; 0 allows every group. Bit 15 (NOTERM) marks a voxel vessels may pass through but where no terminal is placed (for example tissue between an epicardial artery and the myocardium).

set_forbidden!functionsource ↗
set_forbidden!(dom, mask::Array{UInt8,3}, origin, h)

A fine forbidden mask (nonzero = forbidden, e.g. blood in a chamber), cell (1,1,1) centred at origin (mm), cell size h (mm). Every new centreline piece of an accepted bifurcation is then sampled at h/8 and the candidate is rejected if any sample lies in a forbidden cell. It is finer than the domain grid, so thin walls keep their terminals while no vessel crosses into what the mask forbids.

src/grow.jl

Enginemutable structsource ↗
mutable struct Engine

A growing tree: its model (CCOModel), spatial grid and domain. grow returns one; vtree turns it into the vessel list flow takes.

grow!functionsource ↗
grow!(E; parallel = true, progress = 0, growth = :batched, n_seq = 2_000,
      batch_frac = 0.01, batch_max = 16_384)

CoronaryGrowth::expandTree: add terminals until nterm. Candidates of each sampled location are evaluated in parallel; the minimum-volume one (first in candidate order on ties) wins.

growth = :batched (fast mode only) grows the first n_seq terminals the same way, then adds terminals in rounds of up to batch_frac × (current terminals) at once (at most batch_max): every location of a round is evaluated in parallel against the same tree, a conflict-free subset is committed, and the hydraulics are refreshed once per round. See docs/deviations.md. device places the evaluation of each round: an array constructor (CuArray, ROCArray, MtlArray, ...) or a KernelAbstractions backend; the default runs on CPU threads.

src/model.jl

CCOModelmutable structsource ↗
CCOModel{T}

Struct-of-arrays form of upstream HctccoModel: one entry per segment (the segment ends at its distal point pos[i] and starts at pos[parent[i]]), plus global hydraulic state.

src/params.jl

CCOParamsstructsource ↗
CCOParams(; kwargs...)

Run parameters. Defaults match the upstream HctCco command line and HctccoModel.

FieldUpstreamDefault
nterm-n, --nbTerm500
aperf-a, --aPerf20000.0
gamma-g, --gamma3.0
root_radius_max--rootRadiusMax1.588
term_flow_cv--termFlowCV0.0
side_branch_bias--sideBranchBias0.0
min_distance_to_border-m, --minDistanceToBorder5.0
extension_mode--extensionModelfalse
upstream_attach(parity only; see below)false
max_connectiongetMaxBranchLength() (fixed)25.0
size_scale(Vasculature only)1.0
num_neighborsmyNumNeighbor20
nb_max_searchexpandTree(nbMaxSearch)100
nb_try_candidateexpandTree(nbTryCandite)100
kamiya_itersisAddable(nbIter)100
kamiya_tolisAddable(tolerance)0.01
symmetry_floor(Vasculature only)none

symmetry_floor is a list of (mother diameter µm, minimum d_small/d_large) knots, at most 12, interpolated in log diameter and held flat beyond the ends. A candidate junction whose daughters are more asymmetric than the floor at the mother's final-scale diameter is rejected. Measured coronary junctions are more symmetric in small vessels than in large ones (VanBavel & Spaan 1992; Kalsho & Kassab 2004; Schwarz et al. 2020), while minimum-volume growth attaches terminal twigs to vessels of any size. Empty (the default) is upstream behaviour.

Growth is always staged (HCT-CCO's hierarchy-aware schedule; there is no unrestricted mode): for the first 10% of terminals a new vessel may attach to any segment, up to 35% to a terminal or one of its three nearest ancestors, then only to a terminal or a terminal's parent (extension_mode: 25% / 100%). The schedule is applied where candidates are chosen, as upstream's getN_NearestSegments does. Upstream's isAddable additionally rejects every non-terminal host at all stages, which empties the first two stages and stalls growth from an imaged tree with few listed terminals (docs/deviations.md §15). upstream_attach = true restores that check; it exists only for the bitwise parity test.

max_connection (mm) is the longest new vessel a candidate may create, upstream's fixed getMaxBranchLength of 25 ("same units as post-processing filter"). It is a numerical guard against implausibly long connections, not a measured quantity, and no organ sets its own value yet; on XCAT's lung, 60 mm gave the same trees as 25 mm (deviations §15).

size_scale s carries HCT-CCO's size-dependent constants to organs of another size. They were set for adult human hearts, so s = 1 there (every constant verbatim) and s = (V / V_ref)^(1/3) elsewhere, V the organ's tissue volume and V_ref the reference heart's: the longest connection (25 s), the territory reach (6 s) and the Kamiya volume tolerance (0.01 s³) scale with it; quantities set by cells or by the tissue unit (the minimum radius, the terminal perfusion volume) and the dimensionless stage schedule do not. Lengths are in domain units (mm on a 1 mm grid). See size_scale_for.

src/spec.jl

size_scalefunctionsource ↗
size_scale(spec::GrowthSpec) -> Float32

The organ's length against the reference, (tissue_ml / reference_ml)^(1/3); exactly 1 for the reference heart.

terminal_countfunctionsource ↗
terminal_count(spec::GrowthSpec) -> Int

spec.nterm when given; otherwise the organ's flow over the flow of one terminal, flow_ml_min / terminal_flow_ml_min.

mesh_domainfunctionsource ↗
mesh_domain(path, vertices, triangles; margin = 3) -> path

The growth domain of an organ given as a closed surface mesh (vertices 3 × n in mm, triangles 3 × m, 1-based): the voxels of a 1 mm grid whose centres lie inside the mesh, written to path as a .vol for grow.

src/view.jl

Unifiedstructsource ↗
Unified(wrap, copy)

A device that shares memory with the host (Apple silicon, integrated GPUs). wrap(x) returns a device array over the same memory as the host vector x without copying; copy(x) moves any other array there. Each batched round then hands the growing tree to the device in place instead of copying it, which halves the memory of a large tree. With GPUArray the GPU package's array type and its no-copy wrapper of host memory:

Unified(x -> unsafe_wrap(GPUArray{eltype(x), ndims(x)}, x), x -> GPUArray(x))

wrap and copy must return the same array type (the round's views mix wrapped and copied arrays).

Flow

Blood flow in the grown bed from its two pressures, the vessel sizes and the arterioles' tone.

src/perfusion/flow.jl

FlowSpecstructsource ↗
FlowSpec(; kwargs...)
FlowSpec(spec::FlowSpec; kwargs...)

Boundary conditions, tissue constants and vessel-wall mechanics of one organ's vascular bed. Flow is not prescribed: it follows from the geometry, the two pressures and the arteriolar diameters (see circuit). Every field is a keyword and every one is required; an organ is an instance, HEART_FLOW and LUNG_FLOW (organs.jl), each value commented with its sources in test/reference/ (the measurements, which results are also tested against). A variant of an instance: FlowSpec(LUNG_FLOW; p_in = 20). The values below are the heart's.

Boundary conditions and blood

  • p_in: mean inflow pressure. Heart: aortic pressure at the ostia, 90 mmHg (normal human mean arterial pressure; the PET subjects of the resistance anchor below had 86–89 mmHg, Kaufmann et al. 1999; Chilian et al. 1989 used 90 mmHg in the cat).

  • p_out: effective outflow pressure. Heart: 13.6 mmHg, the zero-flow pressure of the maximally dilated beating heart (Kanatsuka et al. 1990). It lumps coronary-sinus pressure and the waterfall effect of intramyocardial pressure into one steady back pressure.

  • regulation: :o2 (arteriolar tone follows the O₂ state of each bed, below) or :passive (no active tone: every state is the relaxed one).

  • Distensible vessels (beta_*, per cmH2O; 0 = rigid): every non-vasoactive vessel has diameter D = D₀·(1 + β·ΔP) (Huang et al. 2011 Eq. 1, D₀ the morphometric diameter), with ΔP its mean pressure minus the pressure around it: p_ext (alveolar) for vessels smaller than d_alveolus_um, p_pleural_cmH2O for larger ones; β takes its small/large value on the same split. Diameters follow the pressures inside the solve. p_ref_cmH2O is the transmural pressure at which the grown radii (and the sub-terminal tables below) hold, so ΔP is counted from it (0 for the lung, as in Huang et al. 2011's validated model).

  • sub_art, sub_ven: nothing, or an OrderTable of the vessels between a grown terminal and its capillaries, which the grown tree does not resolve: each bed gets, in series before and after its capillaries, every order narrower than its own terminal (arterial side) and its draining venule (venous side), with its tissue share of their number in parallel, Poiseuille with the in vivo viscosity, distending like the grown vessels. An organ's terminal unit then sets the grown resolution, and the measured anatomy below it carries the rest (lung: one terminal per acinus, Huang et al. 2011's orders below it).

  • venous_scale: :volume_share (a grown venous tree is scaled to share_ven/share_art of the arterial volume) or :grown (its radii stay as grown, e.g. anchored at an imaged root).

  • capillary: :tube (parallel functional capillaries, below) or :sheet (Fung & Sobin sheet flow, for alveolar walls): a bed of sheet area A between arterial pressure P_a and venous pressure P_v passes Q = S·A/(4µ·kf·L̄²·α)·[h(P_a)⁴ − h(P_v)⁴], with sheet thickness h(P) = max(h₀ + α·(P − p_ext), 0) (p_ext: alveolar pressure). sheet_h0_um, sheet_alpha_um_cmH2O, sheet_s (vascular-space-tissue ratio), sheet_area_m2_per_ml (sheet area per mL of tissue domain), sheet_mu_cP (apparent viscosity), sheet_kf (k·f) and sheet_path_um (mean path length L̄) set it; the sheet stops thickening above a transmural pressure of sheet_pmax_cmH2O. Each bed's resistance is then (P_a − P_v)/Q, updated with the pressures.

  • hd: discharge hematocrit 0.45. Tube hematocrit from the Fåhræus law (Pries et al. 1992).

Volumes and exchange

  • fb: intramyocardial blood volume fraction, mL/mL. Heart 0.09 (healthy human at rest by CMR, Nickander et al. 2019).

  • fb_scope: which vessels fb counts. :intramural: only the vessels inside the tissue, so the imaged (isbase) vessels, which run outside it, hold their own lumen volume on top (heart: CMR's blood volume is intramyocardial, and the epicardial arteries and veins lie on the surface). :whole: every vessel, imaged ones included (lung: Huang's volumes count the hilar vessels).

  • fb_dilated: the same blood volume fraction at maximal vasodilation (state = :stress). Heart 0.121 (the same healthy cohort under adenosine, Nickander et al. 2019). The small-vessel (capillary) volume does not change with adenosine (Crystal et al. 1981), so the extra blood is held by the unresolved arterioles and venules, in their volume shares; capillary resistance, which follows the capillary volume, is unchanged. Equal to fb for a passive organ.

  • epicardial_dilation: how much wider (fraction of the diameter) the imaged conduit arteries are at maximal vasodilation (state = :stress) than at rest: flow-mediated dilation, which a stenosed (diseased) segment lacks. Heart 0.13 (adenosine, QCA and IVUS in normal human coronaries); 0 leaves them rigid.

  • share_art, share_cap, share_ven: 0.288 / 0.317 / 0.395 of that volume (Kassab et al. 1994 cast volumes).

  • cap_ht_ratio: tube/discharge hematocrit 0.85 in unresolved microvessels (Bindschadler 2014).

  • cap_length_mm, cap_diameter_um: functional capillary 0.5 mm × 6 µm (Kassab & Fung 1994), used for the capillary-bed resistance.

  • ve: interstitial volume fraction. Heart 0.224 (ECV 0.273 minus plasma, Nickander et al. 2019).

  • ps, ps_slope: capillary permeability–surface product for iodinated contrast, PS = ps + ps_slope · F (mL/min per g; F the bed's blood flow per gram), so it grows as flow recruits exchange surface. Heart 0.22 + 0.35 F: the linear law of human dynamic CT fitted against PET (Alessio et al. 2019; Bindschadler et al. 2014's porcine law is 0.30 + 0.385 F). Each bed's first-pass extraction follows Renkin–Crone, E = 1 − exp(−PS·m/F_p): about 0.67 at rest and 0.52 at maximal dilation. Checks: the rest Patlak influx constant E·F_p is 0.32 mL/min/g against iopamidol's 0.37 in normal pig myocardium (Daghini et al. 2007); the extraction changes little when adenosine doubles flow, as in pig EBCT (Malyar et al. 2011); E = 0.33 at full vasodilation in dogs (Canty et al. 1991, flow unknown) and PS 0.13–0.19 for iohexol under coronary adenosine (Ritman 1994) lie below it.

  • rho: myocardial density 1.05 g/mL.

  • radius_cv: biological diameter variability. CCO sets every radius exactly from its flow; real vessels of one order scatter by 10–25% (Kassab et al. 1993, Table 2). Each segment's radius is multiplied by a fixed lognormal factor with this coefficient of variation.

Microvascular scale κ (resistance at maximal dilation)

  • Grown microvessels (arteries below d_art_micro 170 µm, capillaries, veins below d_ven_micro 150 µm, Chilian et al. 1989's classes) keep their Poiseuille resistances relative to each other, so flow heterogeneity still comes from the geometry, but one factor κ sets their absolute level: CCO's smallest arterioles are about 2.4× longer than Kassab's (CCO runs each terminal to the centre of its territory, where real tissue uses the capillary network), so their raw Poiseuille resistance is far too high. Vessels above the cut-offs are pure Poiseuille.

  • r_total_dilated (heart: 24 mmHg·min·g/mL): κ is set so that the whole bed's resistance at maximal dilation, inlet pressure ÷ flow per gram (as PET reports MAP ÷ MBF), equals that of healthy men (Kaufmann et al. 1999). Maximal flow is therefore calibrated. Resting flow is fixed by the O₂ balance (Fick: demand ÷ arterial O₂ content ÷ the calibrated extraction), so flow reserve, their ratio, follows from the two calibrations and is calibrated too. Stenosis thresholds, pressure–flow curves, transit times and the distributions of flow stay predictions.

  • r_micro_dilated (0 = off): the earlier anchor, Chilian's microvascular resistance at maximal dilation in the cat (4 mmHg·min·g/mL); used only when r_total_dilated is 0. With neither, κ = 1 (the lung: its resistance is a prediction).

Arteriolar wall mechanics (autoregulation; Carlson, Arciero & Secomb 2008, Arciero et al. 2008)

  • d_active: arteries whose passive diameter is below 150 µm carry smooth-muscle tone. In the beating dog heart adenosine and dipyridamole dilate only arterial microvessels below 150 µm (Kanatsuka et al. 1989), so maximal dilation does not reach the larger ones. Larger arteries are rigid conduits. Imaged segments follow the same rule by the radius the tree gives them: an input vessel is taken at its own size, not assumed to be large because it was imaged.

  • Wall tension at equilibrium: P·D/2 = T_pass(D) + A·T_act,max(D), with the diameter-dependent fits of Carlson & Secomb (2005) as tabulated by Fry et al. (2013), organ data wall_*: C_pass = 6.666·D₀ dyn/cm, C′pass = 12.52 − 0.027·D₀, C\act = 1.30·D₀^1.48 dyn/cm, C′act = 0.773 − 0.00059·D₀, C″act = 0.415 − 0.00080·D₀ (D₀ in µm, the passive diameter at 100 mmHg). At its own pressure each artery can therefore be no wider than its relaxed wall (A = 0) and no narrower than its fully contracted wall (A = 1); its diameter lies between them as 1 − A. This is its capacity.

  • Activation A = 1/(1 + e^{−S}), S = C_myo·T − g_meta·M + C_tone, with C_myo = 1.369/D₀ cm/dyn (Carlson & Secomb 2005; wall_cmyo): the myogenic and metabolic responses. The shear (flow-induced) response of Carlson et al. (2008) is left out: a CCO twig carries a real arteriole's flow in a vessel that is too long and too thin for it (see κ above), so its wall shear is not physiological, and shear plays a secondary role in coronary autoregulation (Namani et al. 2018).

  • Metabolic signal M: red cells release ATP at a rate ∝ 1 − atp_r1·SO₂ as they desaturate (Arciero et al. 2008, from Ellsworth's measurements). M is a bed's release relative to its release at the reference saturation, minus 1, conducted upstream along the vessel wall with length constant atp_l0 = 1 cm; each artery takes the mean over the beds it feeds (Namani et al. 2018). g_meta = 15 is the metabolic sensitivity: the size of the metabolic stimulus C_meta·S_CR in the control state of Carlson et al. (2008) (C_meta = 30 µM⁻¹cm⁻¹, S_CR ≈ 0.5 µM·cm). It is not fitted to any flow.

  • Oxygen: mvo2 0.116 mL O₂/min/g (5.2 µmol/g/min in healthy volunteers, McCommis et al. 2010), arterial saturation sao2, hemoglobin hb g/dL, 1.34 mL O₂ per g; venous saturation of each bed follows from its own flow (Fick).

  • oef: the reference. Coronary flow at rest is matched to O₂ demand, and the regulated quantity is coronary venous oxygen (Feigl 1983; Tune et al. 2004). In the healthy heart at rest the single tone constant C_tone is set so that the coronary-sinus blood has the measured resting O₂ extraction, 0.73 (McCommis et al. 2010). No flow is prescribed: each arteriole finds its own diameter from its wall mechanics, its pressure and the O₂ state of its own beds, so rest flow, its distribution and every response to pressure or a stenosis follow. Pass the healthy rest circuit as reference to keep C_tone fixed (then the extraction is free to change).

src/perfusion/organs.jl

HEART_FLOWconstantsource ↗
HEART_FLOW::Vasculature.Flow.FlowSpec

The coronary circulation and myocardium as a FlowSpec: every value measured, with its source in test/reference/. Tone is O₂-regulated. A variant: FlowSpec(HEART_FLOW; p_in = 70).

LUNG_FLOWconstantsource ↗
LUNG_FLOW::Vasculature.Flow.FlowSpec

The pulmonary circulation as a FlowSpec: passive vessels, alveolar sheet capillaries and the acini's vessels below each terminal; every value measured, with its source in test/reference/.

src/perfusion/pipeline.jl

flowfunctionsource ↗
flow(spec::FlowSpec, arteries::VTree, veins::VTree, tissue::AbstractMatrix; to = identity)
    -> (; rest, stress, terr, pairs, vleaf, terms, vterms, demand)

Blood flow in a grown vascular bed from its two pressures alone, the same for every organ; only spec differs (HEART_FLOW, LUNG_FLOW, or your own values). tissue is 3 × n points (mm), one per tissue voxel. Steps: each voxel's tissue is shared among its nearest terminals (terr, pairs); each capillary bed is paired with the venule it drains into (vleaf); then the circuit is solved. With spec.regulation = :o2 (arteriolar tone follows each bed's O₂ balance, heart) the bed is solved at maximal dilation first (stress, which sets the tone constant κ) and then at rest with that κ, under a fractal O₂-demand field (demand, Bassingthwaighte et al.); with :passive (lung) there is one state and rest === stress.

src/perfusion/flow.jl

Flow.VTreestructsource ↗
VTree

Arterial tree as flat arrays. Segment j runs from a[:, j] to b[:, j] (mm) with radius r[j] (mm). parent[j] == 0 marks a tree root.

vtreefunctionsource ↗
vtree(E) -> VTree

Converts a Vasculature Engine (index 1 is upstream's zero-length root sentinel).

Flow.merge_vtreesfunctionsource ↗
merge_vtrees(trees...) -> VTree

One forest from several trees (e.g. LCA and RCA). Each keeps its own root (parent == 0).

Flow.save_vtreefunctionsource ↗
save_vtree(path, t::VTree) -> path

Writes a vessel list to a compact binary file (load_vtree reads it back).

Flow.Circuitstructsource ↗
struct Circuit

A solved circuit of a grown bed at one state: its trees, every segment's flow, pressure, volume and transit, and its beds. flow returns one at rest and one at maximal dilation.

Flow.circuitfunctionsource ↗
circuit(tree, territory_ml; phys, state = :rest, veins = nothing, vleaf = nothing,
        reference = nothing, kappa = nothing, kappa_init = 1, stenosis = nothing, demand = nothing, to = identity) -> Circuit

Steady coronary circuit driven by pressures: inflow pressure phys.p_in at every ostium, outflow pressure phys.p_out at the coronary-sinus ostium. Every terminal i (perfusing territory_ml[i]) feeds a capillary bed whose resistance follows its capillary count (Kassab capillary dimensions), then drains into the venous tree.

Every artery with a passive diameter below phys.d_active, grown or imaged, is a vessel wall in force balance: its diameter is where the wall tension P·D/2 equals passive plus active smooth-muscle tension (see FlowSpec). Flows, pressures, oxygen saturations and the conducted metabolic signal are solved together with all those diameters until they agree.

  • state = :stress: maximal vasodilation (adenosine): activation 0 everywhere, so vessels sit on their passive pressure–diameter curves.

  • state = :rest: autoregulation. Activation follows the myogenic and metabolic stimuli. Without reference, this is the healthy reference state: the one tone constant is set so the coronary-sinus O₂ extraction equals phys.oef. With reference (a rest Circuit of the healthy heart), that constant is kept, so a changed geometry (a stenosis) or changed pressures give the autoregulated response; flow is limited only when arterioles run out of dilation.

The venous tree is either grown (veins, with vleaf[i] the venous segment receiving terminal i), or, by default, companion veins: each artery's vein has the same topology and holds share_ven/share_art of its volume. kappa reuses the microvascular scale of another circuit. Without it, κ is found by fixed-point iteration from kappa_init (a nearby circuit's κ makes that one or two solves).

demand: relative O₂ demand of each terminal's tissue (mean 1; default uniform). Local O₂ use varies across the wall and local flow follows it (Bassingthwaighte, Beard & Li 2001; Deussen 2001); see demand_field. A reference passes its own on.

stenosis: a list of (seg, area, length_mm): arterial segment seg narrows to the fraction area of its lumen area over length_mm. Its pressure loss is ΔP = f·Q + s·Q² (Young & Tsai 1973; Gould 1978): viscous f = 8πμL_s/A_s², separation s = ρ·k_e/(2A₀²)·(A₀/A_s − 1)² with k_e = 1.52 (counted once, at the narrowing's exit). It is solved with the rest of the network, so the flow through it (and its loss) is whatever the pressures and the distal arterioles make it.

to moves the solver's arrays to a device (for example to = MtlArray or CuArray), where the whole iteration then runs; the returned Circuit holds host arrays either way. pairing is internal (couple_veins): it keeps the solver between venous coupling passes.

Flow.with_extractionfunctionsource ↗
with_extraction(c::Circuit, E) -> Circuit

The same steady circuit (flows, diameters, volumes) with first-pass extraction E in every bed; E = 0 is an intravascular tracer. Flow does not depend on the tracer, so nothing is re-solved.

Flow.InputCurvestructsource ↗
InputCurve

Plasma iodine concentration at the coronary ostia, sampled every dt seconds from t = 0. Stores the cumulative integral A(t) and, on a log-spaced grid of back-flux rates k, the exchange integrals X_k(u) = ∫₀ᵘ C(s) e^{−k(u−s)} ds and Y_k(u) = ∫₀ᵘ X_k.

Flow.gamma_inputfunctionsource ↗
gamma_input(; t0, tpeak, peak_blood, alpha, hd, dt, tmax) -> InputCurve

Gamma-variate whole-blood iodine curve (mg/mL) arriving at t0, peaking at tpeak, converted to plasma concentration C_p = C_b/(1 − H_D). Times in seconds from the start of the simulation.

Flow.frame_massesfunctionsource ↗
frame_masses(c::Circuit, inp::InputCurve, times; to = identity) -> (artery, vein, tissue)

Iodine mass (mg) in every arterial segment, every vein of c.veins and every terminal's lumped bed (unresolved arterioles, capillaries, interstitium, unresolved venules) at each time in times. Matrices are (ntimes, n) so one segment's frames are contiguous.

to moves arrays to a device (for example to = MtlArray or CuArray); the kernels then run there and the results stay on that device. Each mass is a closed form of the cumulative input, so the cost per frame is O(segments) for arteries and terminals and O(Σ path length) for veins, with early exit where iodine has not arrived yet.

Flow.sinus_outflowfunctionsource ↗
sinus_outflow(c, inp, times; to = identity) -> Vector

Cumulative iodine (mg) that has left through the coronary sinus by each time.

src/perfusion/pipeline.jl

couple_veinsfunctionsource ↗
couple_veins(arteries, terr, veins, terms, vterms; phys, reach = 3, passes = 24, tol = 5e-4, to = identity)
    -> (vleaf, circuit, kappa)   # circuit === nothing with assemble = false

Which venule drains each capillary bed. Start from a balanced pairing (balanced_terminal): closest pairs first, no venule draining more than its share of the beds while a free one lies near (plain nearest-venule pairing left ~40% of grown venules draining nothing and loaded others with several). A capillary network connects each terminal arteriole to several venules, and blood leaves through the lowest-pressure one, so a bed whose paired venule sits above its arterial pressure (reversed flow) is re-paired with the lowest-pressure venous terminal within reach mm (widened by 1 mm per pass, at most 6 mm: a capillary network spans millimetres, not centimetres). Solved at maximal vasodilation, where the beds' own pressure drop is smallest. Stops when the reversed beds are at most tol of all beds, or when a pass no longer removes at least 10% of them. With assemble = false the final circuit is not built (only the pairing and κ are returned; κ is the maximal-dilation κ of the last pass, so a later stress circuit with that pairing starts its κ search there).

Transport

A contrast injection through the whole body and the organ's grown tree.

src/perfusion/transport.jl

TransportSpecstructsource ↗
TransportSpec(; phys, tissue_ml, bed, input, co, beat, f_coronary, tmax, dt, every, coronary)
TransportSpec(spec::TransportSpec; kwargs...)

Everything that sets where an injected bolus goes. Every field is a keyword and every one is required: an organ is an instance (HEART_TRANSPORT, LUNG_TRANSPORT, organs.jl), and a run varies one with the second form. phys is the organ's FlowSpec: its blood hematocrit (hd, used for the whole body), blood and interstitial fractions, contrast permeability and tissue density. tissue_ml is the organ's tissue volume (mL). bed is the body segment the organ is (:coronary for the heart, :lung for the lungs) and input the segment whose outflow feeds it (:lv for an organ fed by the aorta, :rv for the lungs). co is the cardiac output (mL/s; rest 81.6, adenosine stress Body.adenosine(); with the circuit of an organ that carries the whole output, the lungs, transport(spec, C) uses its own flow instead), beat the heart period (s), and f_coronary the coronary bed's share of the cardiac output where no grown heart sets it: the body's lumped heart when another organ is the bed, or transport(spec) without a circuit (0.033, ICRP 89 / Williams & Leggett 1989). With the heart's own circuit, transport(spec, C) uses coronary_share(C, co) instead. When the organ is not the heart, coronary gives the body's own lumped coronary bed as (; ml, fb, ve, ps, rho) (the heart's FlowSpec values and myocardial volume); otherwise nothing. tmax, dt and every are the simulated time, the step and the output spacing (s).

src/organs.jl

HEART_TRANSPORTconstantsource ↗
HEART_TRANSPORT::Vasculature.TransportSpec

Contrast transport for the heart: the grown coronary tree is the body's coronary bed, fed from the left ventricle.

LUNG_TRANSPORTconstantsource ↗
LUNG_TRANSPORT::Vasculature.TransportSpec

Contrast transport for the lungs: the grown pulmonary trees are the body's lung bed, fed from the right ventricle; the cardiac output is the lungs' own flow (organ_flow).

src/perfusion/transport.jl

transportfunction
transport(spec::TransportSpec, injection = Injection()) -> result of `Body.simulate`

Iodine everywhere in the body over time for one injection (dose, rate, saline flush, site): t, the outflow concentration of every segment (out), the mean blood and interstitial concentrations of every bed (mean, ecv), and the iodine injected, held and excreted by the kidneys.


transport(spec::TransportSpec, C::Circuit, injection = Injection(); passes = 10, tol = 1e-3, fine = 0.02, dt_out = 0.1,
          times = 0:60, to = identity)
    -> (; body, input, t_out, outflow, artery, vein, tissue, passes, change)

The injection through the whole body and the organ's own solved circuit C (from flow). When the organ is the heart (spec.bed === :coronary), the body's coronary share of the cardiac output is coronary_share(C, spec.co), so the body and the grown tree carry the same coronary flow (spec.f_coronary is not used). When the organ carries the whole cardiac output (a :central segment, the lungs), the cardiac output is the grown tree's own flow organ_flow(C) (spec.co is not used), so the body and the organ carry the same blood. The organ's arterial input is the plasma concentration leaving body segment spec.input. The organ's venous outflow is fed back into the body in place of its lumped segment spec.bed, so recirculation carries the organ's own transit; that changes the organ's input, so the feedback is repeated until the outflow's change, weighted by the organ's share of the cardiac output (its weight in the body's mixed blood, which is how it reaches the input), is at most tol of its peak (at most passes times; passes = 0 is the plain chain with the lumped segment). Every curve returned is then from the same final body run: the organ's input, its outflow and the body's segment that receives it agree. Returns the body result, the organ's plasma input curve, its venous plasma outflow concentration every dt_out s, the iodine (mg) in every artery, vein and capillary bed at times (host arrays), the passes run and the last pass's weighted relative change.

src/perfusion/body.jl

Body.Segmentstructsource ↗
Segment(name, from, to; kind, q, v, plug = 0, delay = 0, rd = 1, ve = 0, ps = 0, cl = 0)
  • kind: :bed (arterial node → venous node, has its own flow fraction q of cardiac output), :artery, :vein or :central (heart and lungs; carries the whole output). Artery and vein flows are derived from the beds unless q is given (a vessel that feeds only part of a bed, such as the hepatic artery).

  • v: blood volume, mL. plug: fraction of v moved by plug flow (first). delay: pure delay, s. The rest of v is n well-mixed stages with n set so that the segment's transit time has relative dispersion rd (SD / mean) at its nominal flow; rd = 1 with no plug is one well-mixed tank.

  • ve, ps: extracellular volume (mL) and permeability-surface product (mL/s) of a bed; cl: plasma clearance, mL/s (kidney: GFR).

  • c, r: venous compliance (mL/mmHg) and outflow resistance (mmHg·s/mL) of a vein that receives an injection. Extra inflow distends it: the stored volume ΔV raises its pressure by ΔV / c and its outflow by ΔV / (r c), so the injected volume leaves with time constant r·c and keeps leaving after the injector stops. 0 = rigid.

Body.Circulationstructsource ↗
Circulation(segments; co, hct, inlet = :ra_in)

co cardiac output (mL/s of blood), hct large-vessel hematocrit. inlet is the node where the venous return meets the heart; the injected volume travels with the venous flow up to it.

Body.Injectionstructsource ↗
Injection(; site, phases)
Injection(; site, conc, volume, rate, saline, saline_rate)

site is a node of the circulation (:antecubital_r, :antecubital_l, :jugular_r, :jugular_l, :femoral_r, :femoral_l, :svc_top for a central line, ...). phases is a list of (volume mL, rate mL/s, concentration mgI/mL) given in order from t = 0, so a test bolus, a biphasic protocol or a saline chaser are all phases. dead_space is the volume of the connecting tube and cannula between syringe and vein (Medrad low-pressure line 2.78 mL plus about 1.2 mL syringe dead space, [cowan2017]; long extension sets about 12 mL), plug flow, primed with contrast (prime = :contrast, the usual air purge) or saline. Default: iohexol 350, 50 mL at 5 mL/s, then 40 mL saline (Lee et al. 2022 adenosine CT perfusion protocol), right antecubital vein (avoids the left brachiocephalic vein, Tseng et al. 2007).

Body.transit_tablefunctionsource ↗
transit_table(C) -> Vector of NamedTuples

Nominal flow (mL/s), mean transit time (s), mixing stages and relative dispersion of every segment, for checking a circulation against measured transit times.

Body.simulatefunctionsource ↗
simulate(C, inj; tmax = 90, dt = 0.002, every = 0.1, detailed = Dict()) -> NamedTuple

Integrates the bolus. Returns t and, per segment name, out (outflow blood concentration, mgI/mL), mean (mean blood concentration inside the segment) and ecv (extracellular concentration of a bed), plus injected, excreted and held (in the body at tmax) iodine, mg. detailed[name] replaces segment name by a model: a function of t giving its outflow blood concentration.

Body.curvefunctionsource ↗
curve(r, name; what = :out) -> Vector{Float32}

Blood concentration (mgI/mL) leaving segment name (what = :out), inside it (:mean), or its extracellular concentration (:ecv).

Body.aiffunctionsource ↗
aif(r, site) -> (t, c)

Arterial input function at a named site: the outflow of that segment.

Body.adenosinefunctionsource ↗
adenosine(; co = 81.6, beat = 1.0, hr = 96/64, sv = 51.0/44.8) -> (; co, beat)

Cardiac output (mL/s) and beat length (s) during i.v. adenosine 140 µg/kg/min, from the resting values. In 25 healthy volunteers heart rate rose from 64 ± 8 to 96 ± 13 /min and stroke volume index from 44.8 ± 10.1 to 51.0 ± 11.8 mL/m², so cardiac output rises × 1.71 (Thomas et al. 2017, Table 2, [thomas2017]). Blood pressure barely moves (systolic −5, diastolic 0 mmHg), so the coronary model keeps its aortic pressure. Other series bracket the rise: +28% (abstract of the same group) and × 2.2–2.4 by gated Rb-82 PET, whose resting volumes read low [lassen2023]. Use as human_circulation(; adenosine()..., f_cor = q_cor / adenosine().co).

Body.human_circulationfunctionsource ↗
human_circulation(; co, hct, beat, ...) -> Circulation

The whole-body circulation: the heart and lungs, the aorta and every organ bed, and the named veins back to the right atrium, so a bolus recirculates.

Body.organ_onlyfunctionsource ↗
organ_only(organ::Segment; site = :inlet, delay, mtt, rd, co, hct) -> Circulation

A circulation with no heart or lungs: the injection site feeds one lumped path (pure delay plus a transit of mean mtt and relative dispersion rd) that ends at the organ's inlet node, and the organ drains to the outlet. Use it for a single-organ study when a whole-body model is not wanted; co here is the organ's own flow.

src/perfusion/transport.jl

circulationfunctionsource ↗
circulation(spec::TransportSpec) -> Body.Circulation

The whole-body circulation (Body.human_circulation) with the organ's own values in its body segment spec.bed and its blood everywhere.

coronary_sharefunctionsource ↗
coronary_share(C::Circuit, co) -> Float32

The coronary bed's share of the cardiac output co (mL/s) when the heart is the grown circuit C: its root flow / co.

organ_flowfunctionsource ↗
organ_flow(C::Circuit) -> Float32

The flow (mL/s) into the grown circuit C: the sum of its roots' flows.

Phantom

The voxelized dynamic phantom, and loading it for a CT simulator.

src/phantom.jl

PhantomSpecstructsource ↗
PhantomSpec(; times, tissue_material, blood_material, tissue_classes, vessel_classes, vessel_rmin_mm, delay_bin_ml,
            energy_keV, materials, pools)
PhantomSpec(spec::PhantomSpec; kwargs...)

Everything that sets an organ's dynamic CT phantom. Every field is a keyword and every one is required: an organ is an instance (HEART_PHANTOM, LUNG_PHANTOM, organs.jl); a variant is the second form.

  • times: the frame times (s) of the iodine curves.

  • tissue_material: the XrayAttenuation.Materials name of the perfused tissue (its base; iodine is added on top). A :tissue pool takes it too.

  • blood_material: the base of the grown and imaged vessels' classes.

  • tissue_classes: tissue voxels are grouped into this many kinetic classes (k-means on their joint curves over every state), so one compartment volume serves every state; at most 999 (ids 1001-1999).

  • vessel_classes, vessel_rmin_mm: grown vessels of radius ≥ vessel_rmin_mm are drawn into the tissue as blood, and the vessels a classifier names (imaged ones) take their grown segment's curve; all grouped into this many classes.

  • delay_bin_ml: :out pools are binned by the blood volume (mL) between the voxel and the segment's outlet.

  • energy_keV: energy of the base HU written for each material.

  • materials: static label → material name (labels 0-99; no iodine).

  • pools: the Pools a classifier can name, by index.

src/organs.jl

HEART_PHANTOMconstantsource ↗
HEART_PHANTOM::Vasculature.PhantomSpec

The heart's phantom: the myocardium in 64 kinetic classes, grown (radius ≥ 0.1 mm) and imaged coronary vessels in 64 classes, great-artery transit binned by 1 mL of upstream blood, frames every second for 60 s.

LUNG_PHANTOMconstantsource ↗
LUNG_PHANTOM::Vasculature.PhantomSpec

The lungs' phantom: the lung parenchyma in 64 kinetic classes and the grown lumens of radius ≥ 0.25 mm drawn as blood; otherwise as HEART_PHANTOM.

src/phantom.jl

phantomfunctionsource ↗
phantom(spec, scans, classify, grid; pairs, terr, nvox, out, roi = p -> 0, roi_names = [], roi_groups = [],
        tracking = [], facts = [], check = true) -> NamedTuple

Writes the phantom of a transported bed into the scenario folder out (the DynamicPhantom bundle: labels.u16.zst, rois.u8.zst, mbf_<state>.f32.zst, label_<state>.csv, ledger_<state>.csv, phantom.toml).

  • scans: one (state, C, T) per state ("rest", "stress"): the solved circuit and its transport result on spec.times.

  • classify(p) -> (kind, a, d) for a voxel centre p (mm, the trees' frame): one of VOX_STATIC, VOX_POOL, VOX_TISSUE, VOX_VESSEL (see above).

  • roi(p) -> 0:length(roi_names): the ground-truth compartment of the voxel at p (0: none). A compartment is a region of the organ with everything in it: tissue and every artery, vein and lumen that lies in it. Only tissue and vessel voxels take a compartment, so every compartment is a set of whole labels. roi_groups: name => ROI ids (e.g. the LV as the union of its territories).

  • grid: the output VoxelGrid (voxel centres, isotropic h mm).

  • pairs, terr, nvox: the territories (flow's pairs and terr) over the nvox tissue voxels the classifier numbers.

  • tracking: name => (x, y, z) (mm): bolus-tracking points; DynamicPhantom.scan_times reads their curves. A point in a vessel segment gives that segment a class of its own (one of vessel_classes), painted at the segment's blood concentration, so the point reads that segment's curve alone. (Every other vessel's mass is spread over its painted voxels; a tracked segment's painted iodine differs from its mass by its lumen's volume beyond the segment's.)

Labels: 0-99 static, 100-999 pools (and their delay bins), 1000 tissue without a territory (static), 1001… tissue (tissue_classes kinetic classes in each ROI), 2001… drawn vessels (vessel_classes classes per ROI, arteries and veins apart). A tissue or vessel label lies in one ROI. Its iodine is the mass of its members (beds spread over the tissue sub-voxels that remain after lumens are drawn; each vessel segment over its painted voxels) over their volume, so painting conserves mass, and its ground-truth MBF is its members' bed flow over their mass (a vessel's is 0). An artery or vein with no painted voxel gives its iodine to the beds it feeds and drains. A compartment's iodine and ground truth are therefore sums over its labels (DynamicPhantom.load gives them as roi_table): its MBF is its beds' flow over the mass of everything in it, vessels included.

The ledger (ledger_<state>.csv) is the organ's iodine at each time: delivered through the inlets, held in its arteries, beds and veins, out through its venous outlet, and painted (a tracked segment counted at its mass). With check, the build fails when delivered ≠ held + outflow or painted ≠ held (beyond 1% of the peak delivered mass). verbose logs each stage's seconds.

src/organs.jl

XCAT_BODY_POOLSconstantsource ↗
XCAT_BODY_POOLS::Vector{Vasculature.Pool}

The XCAT body around an organ as pools of the whole-body model: the chambers' blood, the great vessels and the organ beds. A classifier names a pool by its index here.

XCAT_STATICconstantsource ↗
XCAT_STATIC::Dict{Int64, String}

XCAT labels painted as static materials: air, the airway wall's cartilage, soft tissue.

src/perfusion/dynamic_phantom.jl

DynamicPhantom.compartment_classesfunctionsource ↗
compartment_classes(X, K; iters = 40, seed = 1) -> (assign, centres)

k-means of voxel curves into K kinetic classes. X is (features × voxels): typically every state's (and every scenario's) curve of one voxel stacked into one column, in HU so the error reads in HU. Clustering on the joint curves keeps one class homogeneous in every state and scenario, so one compartment volume serves them all. k-means++ seeding on a subsample; assign[v] is the class of voxel v, centres[:, k] the class mean curve.

DynamicPhantom.class_errorfunctionsource ↗
class_error(X, assign, centres) -> (; rms_worst, rms_mean, p99, max)

Error of replacing each voxel's curve by its class curve: RMS over voxels at each feature (time point), the worst and mean of those, and the 99th percentile and maximum of the absolute error, in the units of X.

DynamicPhantom.VolumeWritermutable structsource ↗
VolumeWriter(path, dims; T = UInt16, level = 10)

Streams a compartment volume to path one z slice at a time (column-major: x fastest, then y, then z), through zstd if path ends in .zst (a compartment volume is mostly uniform regions: 0.2 mm XCAT-size volumes shrink about 100×), raw otherwise. write_slice!(w, slice) appends one slice; close(w) finishes. w.counts[id + 1] counts voxels per id.

DynamicPhantom.write_bundlefunctionsource ↗
write_bundle(out; grid, files, states, times, labels, ledger, toml, energy_keV = 70)

Writes the tables of a phantom folder (the volumes are written by the caller with VolumeWriter):

  • labels: one NamedTuple per label, with id, name, roi, kind, base_material, voxels, mass_g, mbf (one value per state, or nothing) and curve (state index → vector of iodine, mg/mL, over times; nothing for a static label).

  • ledger: state index → NamedTuple of equal-length columns (the first one t_s).

  • toml: a Dict written to phantom.toml, with grid, files, states and energy_keV added.

DynamicPhantom.read_volumefunctionsource ↗
read_volume(path, dims, T; zrange = 1:dims[3], xrange = 1:dims[1], yrange = 1:dims[2]) -> Array{T, 3}

Reads a volume of the bundle (zstd or raw), only the box xrange × yrange × zrange.

DynamicPhantom.loadfunctionsource ↗
load(dir; volume = true, rois = false, zrange = nothing, xrange = nothing, yrange = nothing) -> Dyn

Reads one scenario folder. volume reads labels.u16.zst, rois also rois.u8.zst; zrange (slice numbers, 1 = the first) reads only those z slices, and xrange, yrange (voxel numbers) only that box of each (origin_mm is then the box's). ph.curves[(state, label)] is a label's iodine (mg/mL), ph.mbf[(state, label)] its ground-truth MBF, ph.table[label] its name, base material, kind and ROI, ph.roi_table[(state, name)] a compartment's (or group's) voxels, mass, ground-truth MBF and iodine mass curve (sums over its labels), ph.ledger[state] the organ's iodine ledger, ph.reference[(state, "roi:<name>")] a tracking ROI's ΔHU curve (scan_times reads it), ph.meta the phantom.toml.

DynamicPhantom.iodinefunctionsource ↗
iodine(ph::Dyn, state, t)

iodine(ph, state, t) -> Dict(label => mg/mL) for every label (0 for static labels); linear between samples.

DynamicPhantom.hufunctionsource ↗
hu(ph, state, t; energy_keV = the phantom's) -> Dict(label => HU)

Every label's CT number at time t (s) and energy_keV: its base material with its iodine added by the format's mixing rule. A lookup of the label volume through it is the idealized (noise- and blur-free) image of the phantom.

DynamicPhantom.scan_timesfunctionsource ↗
scan_times(ph; state, roi = its only tracking ROI, threshold_hu = 80, lead_s = 1, step = 0.5)

Bolus-tracking times of a loaded phantom from its tracking ROI (ΔHU), sampled every step s (the tracking scan's cadence) on the curve interpolated between the stored frames: arrival (first sample ≥ +threshold_hu), peak (lead_s before the ROI maximum, the usual scan time), peak_time (the maximum) and peak_hu. roi is a reference key, "roi:<tracking name>" (e.g. "roi:descending aorta"); a phantom with several tracking points must name one.

DynamicPhantom.hu_per_mg_mlfunctionsource ↗
hu_per_mg_ml(material, energy_keV) -> HU per mg/mL

The CT number change (HU) that 1 mg/mL of iodine adds to XA.Materials entry material at energy_keV.

DynamicPhantom.materials_atfunctionsource ↗
materials_at(ph, state, t; max_materials = 64) -> (; lut, materials, err, max_err)

The material table of the phantom at time t, at most max_materials long (64 keeps BasisSimulator's :dd_fast projector; more falls back to its slower :dd). Different base materials are never merged, and a label that carries iodine is merged only with labels of its own base material and ROI: the fewest merges that fit, each merged material at its members' voxel-weighted mean iodine, so the total iodine and every ROI's iodine are kept exactly. err is each label's iodine error (mg/mL), max_err the largest.

DynamicPhantom.bs_phantomfunctionsource ↗
bs_phantom(ph, BS; state, t, max_materials = 64) -> (BS.Phantom, materials_at result)

The BasisSimulator.jl phantom at time t: a 0-based UInt8 material mask plus the material table, voxel size in cm. Pass the BasisSimulator module as BS (this file does not depend on it).

src/phantom.jl

Poolstructsource ↗
Pool(name, material, segment, what; scale = 1, ml = 0, through = segment, lead_ml = 0, lead_through = segment,
     upstream = false)

A compartment filled from the whole-body model: segment names a Body segment, what which of its curves: :out (its outflow blood, shifted by the voxel's transit, pool_delay), :mean (the segment's mean blood), :organ (an organ bed: its blood and extracellular iodine over its tissue volume ml), or :tissue (the organ's own perfused tissue, its mean). scale multiplies the curve (e.g. a blood fraction). material is the XrayAttenuation.Materials base.

An :out pool is positional: each voxel carries the blood volume (mL) between it and the segment's outlet along its vessel. That volume moves at the flow of segment through; lead_ml more lie before the pool's vessel begins, at the flow of lead_through (the aortic arch before the descending aorta). An upstream pool lies before the outlet (a vein draining into a chamber), so its voxels see the outlet's curve earlier.

pool_delayfunctionsource ↗
pool_delay(P::Pool, r, ml) -> Float32

The transit (s) of a voxel of :out pool P lying ml of blood from the outlet of P.segment along its vessel, in the state of Body.simulate result r: each stretch of blood over the flow through it (central volume principle, plug flow), P.lead_ml at the flow of P.lead_through and ml at the flow of P.through. Negative for an upstream pool, whose voxels see the outlet's curve earlier. One anatomy thus gives each state its own transits, shorter where the flow is higher.

pool_curvefunctionsource ↗
pool_curve(P::Pool, r, t; delay = 0, tissue = 0) -> Float32

Pool P's iodine (mg per mL) at time t (s) in a Body.simulate result r (a transport result's body): :out is the segment's outflow delay s earlier (pool_delay), :mean its mean blood, :organ its blood and extracellular iodine over P.ml, :tissue the organ's own tissue value tissue. The one definition of the body's compartments, for phantoms and pictures.

VOX_STATICconstantsource ↗
VOX_STATIC::UInt8

A voxel kind classify(p) returns: a static material; a is its label in PhantomSpec.materials.

VOX_POOLconstantsource ↗
VOX_POOL::UInt8

A voxel kind classify(p) returns: a blood pool of the body model; a is its index in PhantomSpec.pools and d the blood (mL) upstream of it along its vessel.

VOX_TISSUEconstantsource ↗
VOX_TISSUE::UInt8

A voxel kind classify(p) returns: perfused tissue; a is its tissue voxel's number (0 where no territory reaches).

VOX_VESSELconstantsource ↗
VOX_VESSEL::UInt8

A voxel kind classify(p) returns: a grown vessel; a > 0 is an arterial segment, a < 0 the venous segment -a.

XCAT Inputs

Vasculature.XCAT turns XCAT's surfaces, meshes and label volumes into the domains, inlet trees and blockers grow takes.

src/xcat/classify.jl

XCAT.tissue_numbersfunctionsource ↗
tissue_numbers(mask) -> Array{Int32, 3}

Each voxel's number among the true voxels of mask (in findall order), 0 elsewhere: the tissue voxel numbers flow and phantom use.

XCAT.nearest_tissue_voxelfunctionsource ↗
nearest_tissue_voxel(numbers, k) -> Int32

The number of the tissue voxel at grid index k (a tuple), or of the nearest one within 2 voxels (0: none).

XCAT.imaged_segmentsfunctionsource ↗
imaged_segments(tree) -> (indices, starts, ends, radii)

The imaged (base) segments of a grown tree (vtree): their indices in the tree, end points (3 × n, mm) and radii (mm).

XCAT.nearest_segmentfunction
nearest_segment(segments, p) -> (index, distance)

Of segments (imaged_segments), the one whose surface is nearest to p (mm): its index in the tree and the distance from p to its surface (mm; negative inside the lumen).


nearest_segment(index::SegmentIndex, p) -> (index, distance)

As nearest_segment over the indexed segments, for a point within the index's reach of a lumen; elsewhere (0, Inf32).

XCAT.SegmentIndexstructsource ↗
SegmentIndex

A bucket grid over a tree's imaged_segments for fast nearest-lumen queries near the vessels: each cell lists the segments whose lumen, widened by the index's reach, touches it. Build it with segment_index.

XCAT.segment_indexfunctionsource ↗
segment_index(segments; reach, cell = 4.0) -> SegmentIndex

An index over segments (imaged_segments) for nearest_segment queries at points within reach mm of a lumen; cell is the bucket size (mm).

XCAT.closingfunctionsource ↗
closing(mask, r) -> Array{Bool, 3}

Morphological closing of a 3-D mask by a ball of r voxels: dilate, then erode, each from an exact squared Euclidean distance transform (Felzenszwalb & Huttenlocher, one line at a time).

XCAT.great_vessel_transitfunctionsource ↗
great_vessel_transit(labels, origin, h; cell_mm = 1.0, svc_mm = 70) -> NamedTuple

Which great vessel each vessel voxel belongs to, and where it lies along it, on a cell_mm grid: its tree (0 off the vessels) and the blood (mL) between it and the heart. labels is XCAT's label volume with voxel (1, 1, 1) centred at origin (mm), spacing h. The trees grow breadth first through XCAT's vessel labels from where they meet the heart, by connection rather than by mesh:

  1. the aorta (label 28) and the systemic arteries joined to it, from the LV blood;

  2. the pulmonary arteries (label 25 and those in the lungs, 125), from the RV blood, never stepping beside the aorta, so they cannot run into it where the two touch; grown first, so the aortic tree takes the rest;

  3. the superior vena cava, from the upper half of the RA's veins: its first svc_mm (the SVC itself, about 70 mm long [mahlon2007_svc_ct_length]) is tree 3, and the veins beyond it are the right (6) or left (7) brachiocephalic tree, by side (the liver's is the right);

  4. the inferior vena cava, from the lower half of the RA's veins;

  5. the pulmonary veins (label 24 and those in the lungs, 124), from the LA blood.

The venous trees grow at once, each voxel joining the first to reach it. A voxel's volume is its tree's volume nearer the heart: a cross-section of the pulmonary or venous trees cuts every parallel branch, which together carry the whole flow, so all of them count; in the aortic tree only the aorta counts, since its branches run beside it, not in its path. Past the arch's top the descending aorta carries its own, smaller flow, so it is timed from the arch's volume (apex_ml) on; a brachiocephalic voxel is timed from the SVC's top (confluence_ml). Returns (; origin_mm, h, tree, volume, descending, apex\_ml, confluence\_ml, tree\_ml, ostia\_ml); ostia\_ml is the aorta's volume where the coronary arteries (label 26) leave it.

XCAT.scale_treefunctionsource ↗
scale_tree(transit, tree, k) -> NamedTuple

transit (great_vessel_transit) with the blood along tree tree scaled by k: each of its voxels keeps its place along the vessel, and the blood upstream of it is k times XCAT's. For a phantom whose drawn vessel is narrower than the blood the body model that times it holds (XCAT vmale_50's aorta). The marks on that tree (apex_ml, ostia_ml on the aorta, confluence_ml on the superior vena cava) scale with it; tree_ml stays XCAT's.

XCAT.vessel_volume_atfunctionsource ↗
vessel_volume_at(transit, p, t) -> Float32

Where the great-vessel voxel at p (mm) lies along tree t of transit (great_vessel_transit): the blood (mL) between it and the heart, interpolated trilinearly over the tree's cells around it (smooth along the vessel), else its nearest cell on the tree's value.

XCAT.vessel_tree_atfunctionsource ↗
vessel_tree_at(transit, p, candidates) -> UInt8

The great-vessel tree the voxel at p (mm) is on: of candidates, the one most of the cells around it are on (trilinear weights), else its nearest cell's, else the first candidate.

XCAT.great_vessel_poolfunctionsource ↗
great_vessel_pool(transit, p, t) -> (VOX_POOL, pool, ml)

The body pool (XCAT_BODY_POOLS) of a voxel at p (mm) on great-vessel tree t, and the blood (mL) along its vessel that times it: the aorta (5) or, past the arch's top, the descending aorta (27, timed from the arch's end at its own flow); the pulmonary arteries (6); the superior (8) and inferior (11) venae cavae; the pulmonary veins (7); the right (9) and left (10) brachiocephalic veins, timed from the SVC's top.

src/xcat/heart.jl

XCAT.Centerlinestructsource ↗
Centerline

Polyline X (3×K, mm) with radius r (K, mm), sampled from a tube surface, running from the vessel's proximal end to its distal end.

XCAT.centerlinefunctionsource ↗
centerline(s::NurbsSurface; step = 1.0) -> Centerline

Centerline of an XCAT tube surface: ring centroids and mean ring radii along v (u runs around the tube), resampled every step mm. The collapsed cap rings at both ends are dropped.

XCAT.BaseTreestructsource ↗
BaseTree

Binary base tree for one coronary root: node 1 is the ostium; parent[k] (0 for the ostium), positions X (3×n, mm), radii r and the vessel each node came from.

XCAT.base_forestfunctionsource ↗
base_forest(lines; roots, attach_tol = 3.0) -> Vector{BaseTree}

Joins centerlines into one binary tree per root. roots names the vessels that start at an ostium (e.g. ["dias_lad1", "dias_rca1"]). Every other vessel attaches at the node nearest to its proximal end on the closest already-placed vessel (within attach_tol mm of that vessel's surface); its proximal end is the end nearer to that tree. A node may carry at most two children, so a third branch moves to the next node downstream.

XCAT.write_base_gxlfunctionsource ↗
write_base_gxl(path, t::BaseTree)

Writes the base tree in the GXL layout import_gxl reads: node 0 is the ostium ("root node"), leaves are "terminal node", and each edge carries the child node's radius.

XCAT.heart_masksfunctionsource ↗
heart_masks(S, g; phase = "dias", res = (160, 160)) -> NamedTuple of Array{Bool,3}

LV and RV myocardium and cavities of an XCAT heart on grid g: LV myocardium = inside lv_4 minus inside lv_0, plus the papillary muscles; RV myocardium = inside rvm minus rv minus the LV; LV cavity = inside lv_0 minus papillary muscles; RV cavity = inside rv minus the LV.

XCAT.label_masksfunctionsource ↗
label_masks(L, origin, h, g; roi = trues(g.dims), lv = 15, rv = 16, lvpool = 19, rvpool = 20,
            pools = (19, 20, 21, 22, 24, 25)) -> NamedTuple of Array{Bool,3}

Heart masks on the coarse grid g from a fine XCAT label volume L (voxel size h mm, voxel (1,1,1) centred at origin). Each coarse voxel looks at the fine voxels whose centres fall inside it:

  • lvmyo / rvmyo: at least half of them are LV / RV myocardium (the larger count wins);

  • lvcav / rvcav: at least half are LV / RV blood pool;

  • pool: any of them is blood (the four chamber pools, great arteries and veins by default);

  • blood: the fraction of them that is blood;

  • domain: myocardium with no blood at all, so a vessel grown inside it cannot enter a chamber.

XCAT reuses some label values for distant organs (e.g. 16 for the kidneys), so roi limits the muscle and cavity masks to the heart region; pool covers the whole grid. The default label values are those of the XCAT heart phantom.

XCAT.vessel_tubefunctionsource ↗
vessel_tube(t::BaseTree, g; margin = 1.5) -> Array{Bool,3}

Voxels of g within r + margin mm of the base tree's centerline. Epicardial arteries run in the fat outside the myocardium; adding this tube to a tree's growth domain lets new segments connect to them without leaving the domain.

XCAT.vessel_bridgesfunctionsource ↗
vessel_bridges(t::BaseTree, myo, g; every = 2.0, reach = 15.0, margin = 1.5) -> Array{Bool,3}

Epicardial arteries run in the fat a few mm outside the myocardium. Every every mm along the centerline, a capsule (radius r + margin) joins the vessel to the nearest myocardial voxel within reach mm, so new branches can pass from the vessel into the wall, as real penetrating branches do, without the growth domain having to include the whole fat layer.

src/xcat/labels.jl

XCAT.label_maskfunctionsource ↗
label_mask(L, origin, h, g::VoxelGrid, labels; fraction = 0.5) -> Array{Bool, 3}

Cells of the (coarser) grid g in which at least fraction of the label volume's voxels carry one of labels. L is a label volume with voxel (1, 1, 1) centred at origin (mm) and spacing h (mm); a cell takes the voxels whose centres lie inside it.

XCAT.label_blockersfunctionsource ↗
label_blockers(L, origin, h, g::VoxelGrid, labels; factor = 2) -> (mask, origin_mm, cell_mm)

The blocker mask over the box of grid g, in the form grow and penetrating_stubs take: cells of factor × factor × factor label voxels (0.4 mm for XCAT's 0.2 mm), a cell blocked (1) when any of its voxels carries one of labels (chambers, great vessels, airways). L, origin and h are as for label_mask.

src/xcat/nurbs.jl

XCAT.NurbsSurfacestructsource ↗
NurbsSurface

Cubic B-spline surface with control points P[:, j, i]: j along u (fastest in the file, nu points) and i along v (nv points); knot vectors u (length nu + 4) and v (length nv + 4). Coordinates in mm.

XCAT.read_nrbfunctionsource ↗
read_nrb(path) -> Dict{String, NurbsSurface}

Reads every surface in an XCAT .nrb file (for example vmale_50_heart.nrb), keyed by name ("dias_lad1", "sys_lv_0", ...).

XCAT.write_nrbfunctionsource ↗
write_nrb(path, surfaces)

Writes surfaces in XCAT .nrb format, in the given order (a vector of NurbsSurface, or a Dict, written sorted by name).

XCAT.evaluatefunctionsource ↗
evaluate(s, t_u, t_v) -> NTuple{3, Float64}

Point on the surface at parameters (t_u, t_v), each within its knot range.

XCAT.tessellatefunctionsource ↗
tessellate(s; nu = 64, nv = 64) -> (points::Matrix{Float64} (3×K), triangles::Matrix{Int} (3×T))

Samples the surface on a regular parameter grid and triangulates it. Degenerate triangles (collapsed caps) are dropped.

src/xcat/stubs.jl

XCAT.penetrating_stubsfunctionsource ↗
penetrating_stubs(t::BaseTree, g::VoxelGrid, tissues; scale = 1, spacing = 2.8scale, reach = 6scale, depth = 1.5scale,
                  domain = nothing, forbid = nothing, blockers = nothing, radius = 0.1) -> (tree, report)

Adds stubs, short branches from the vessels of t into the tissue they run beside, about every spacing mm along each vessel. A stub runs from a node of t to the nearest voxel of tissues and depth mm on into it; its tip is a new end of the tree (radius radius mm until growth sizes it by flow).

The step decides where stubs are needed, so it can be run on any inlet tree:

  • Sites lie about spacing mm apart along each vessel: the node nearest one spacing past the previous site, a side branch of t (an imaged branch counts as a stub that is already there) or the root. The first node of every branch is a site. Sample the centrelines at spacing or finer (centerline(s; step = spacing)).

  • A site gets no stub when it already has two children (the tree is binary); when no voxel of tissues lies within reach mm; when the tip lies outside the grid, outside domain or in forbid; or when the stub crosses blockers.

So a densely imaged tree gets few stubs, a bare trunk one per spacing, and stretches far from the tissue (e.g. a left main running free in fat) none. To grow from the imaged tree alone, do not call it.

Where the vessel runs through the tissue (its nearest tissue voxel lies within its own radius, as for a femoral artery inside a soft-tissue domain), the stub leaves sideways, perpendicular to the vessel and radius_k + depth mm long, turning by the golden angle from one such stub to the next; of eight directions the first whose tip is inside and clear is taken.

tissues is one Bool mask on g or several (the heart's LV and RV walls); where several are within reach, successive stubs alternate between them. domain (default: the union of tissues) and forbid are Bool masks on g; blockers is (mask, origin_mm, cell_mm) as for grow, a fine mask no stub may cross (chambers, great vessels). report counts the sites and what became of them: (; sites, stubs, sideways, no_tissue, outside, blocked).

Lengths: the heart's are the defaults at scale = 1: spacing 2.8 mm (the pig LAD's mean segment length; RCA 3.2, Kassab et al. 1993), reach 6 mm, depth 1.5 mm. Like every other length set for the heart, they scale with the organ (docs/deviations.md §16): pass scale = size_scale(spec) and sample the centrelines at 2.8 * scale mm. An organ whose feeding arteries' branch spacing is measured passes that as spacing.

src/xcat/supply_gaps.jl

XCAT.fill_supply_gapsfunctionsource ↗
fill_supply_gaps(parent, X, r, domain, lower; r_resolved, h = 1.0, max_gap_mm = nothing, min_gap_ml = nothing, qexp = 7 / 3,
                 step_mm = 2.0, forbidden = nothing, region = nothing, free_mm = 3.0, max_iter = 200)
    -> (; parent, X, r, added, gaps)

parent (0 = root), X (3 × n node positions, mm) and r (radii, mm) describe the tree as nodes; segment k runs from parent[k] to k. domain is the organ mask on a grid whose voxel (1, 1, 1) is centred at lower (mm) with spacing h. A gap is a connected set (6-neighbour) of domain voxels farther than max_gap_mm from every segment, of at least min_gap_ml. Both follow from the imaging's resolution by default: the most tissue a vessel thinner than r_resolved (the smallest radius the imaging resolves, mm; required, since a grown or flow-resized tree's smallest radius says nothing about the imaging) can supply is V_max = V_organ · (r_resolved / r_root)^qexp, so a region larger than V_max that is farther than G = (3 V_max / 4π)^(1/3) from the imaged tree must be fed by a vessel the imaging would have shown, and one is missing (min_gap_ml = V_max, max_gap_mm = G). Its branch ends at the gap voxel nearest the gap's centroid and starts at the nearest node whose radius is at least the new branch's radius and whose straight path to it obeys the organ's blockers: no point of it lies in forbidden (a Bool mask on the domain grid: chambers, airways, ...), and every point farther than free_mm from the start lies in the domain and, with a region label map (left / right lung, lobes, ...), in the target's own region. Nodes are placed every step_mm. A start node that already has two children is moved to the midpoint of its parent segment (no node gets three children). Radius: r_root × (V_gap / V_domain)^(1/qexp). Returns the new arrays, one entry per added branch in added, the gap volumes found in each round in gaps, and in unfilled the gaps no node can reach without crossing a blocker or a region boundary (an organ region with no imaged vessel of its own).

src/xcat/tubes.jl

XCAT.read_mesh_blocksfunctionsource ↗
read_mesh_blocks(path; names = nothing, offset = (0, 0, 0)) -> Dict{String, Vector{Tuple{Matrix{Float64}, Matrix{Int}}}}

The triangle blocks of an XCAT .raw mesh file, by name: each block as (points, triangles), points 3 × n (mm, plus offset) and triangles 3 × m (indices into the points), the form voxelize takes. A name heads every block of its surface, so a vessel system drawn as many tubes gives one entry per tube. names keeps only those surfaces. offset moves the mesh into another frame (XCAT writes its meshes offset mm from its NURBS frame; its log gives it).

XCAT.tube_centerlinefunctionsource ↗
tube_centerline(points; name = "tube", step = 1.0, max_nodes = 400) -> Centerline

The centreline of one tube mesh, given as its vertices (3 × n, mm): slices across its principal axis about step mm apart, each slice's centroid a node. XCAT's mesh tubes have one calibre, so every node takes the tube's median slice radius (the end slices are partial rings). A tube too short to slice is its two ends at its axis' extremes.

XCAT.tube_forestfunctionsource ↗
tube_forest(lines; toward, join_mm = 6.0, min_nodes = 12) -> Vector{BaseTree}

Joins tube centrelines into trees, largest tube first: a tube attaches by its nearer end to the nearest node of an already placed tube at least half its radius, when that end lies within the two radii plus join_mm of it (a node that already has a child is split at the midpoint of its parent segment, so the tree stays binary); otherwise it starts a new tree, whose root is its end nearer toward (mm; the hilum or the heart). Trees of fewer than min_nodes nodes (stray single tubes) are dropped. Each tree's nodes run from its root, parents first.

XCAT.graftfunctionsource ↗
graft(trunk::Centerline, trees; name = trunk.name) -> BaseTree

One tree: the trunk (e.g. the main pulmonary artery from its valve) with trees joined to it, in order of their roots' distance from the trunk's distal end, each at the nearest node already in the tree (the trunk's or an earlier tree's). The trunk runs from the end farther from every tree root; its rings narrower than half its widest (a valve sinus or dome collapsing at an end) are dropped. A node that already has two children is split at the midpoint of its parent segment. The root edge takes the trunk's median ring radius, robust to the sinus and the bifurcation dome at its two ends.

XCAT.drainfunctionsource ↗
drain(trees, surface; root_radius, name = "veins") -> BaseTree

One draining tree: the largest of trees takes the others, each joined by its root at its nearest node, and a straight segment carries the joined tree from its root to the nearest point of surface (3 × n points, mm: the chamber it drains into), which becomes the root, of radius root_radius (mm).

Files and Checks

Tree and voxel files, morphometry and quality checks.

src/io_gxl.jl

import_gxlfunction
import_gxl(T, path, params) -> CCOModel{T}

Builds the model exactly as upstream does for -i base_tree.xml: the constructor state (a two-segment tree whose terminal list holds upstream index 1) followed by readTreeFromXml, which replaces the segments but keeps that terminal entry and the first two parent/children slots, then reconciles flows and runs a full hydraulic update.


import_gxl(T, paths::AbstractVector, params) -> CCOModel{T}

A forest: each file's tree is imported as above and appended, so the model has one root segment per tree (parent = the shared sentinel). Terminal flow, pressures and the length factor are shared, as for a single tree.

export_gxlfunctionsource ↗
export_gxl(m, path)

CoronaryTreeIO::writeTreeToXml: nodes (type from the terminal list; a node is the "root" if its parent is upstream index 0), then one edge per segment with flow, resistance and radius.

src/io_vol.jl

read_volfunctionsource ↗
read_vol(path) -> (img::Array{UInt8,3}, lower::NTuple{3,Int32})

Voxel img[i, j, k] is the DGtal point lower .+ (i, j, k) .- 1. When the header has Center-* fields, the domain is centred as in DGtal: lower = c - (s - 1) ÷ 2.

write_volfunctionsource ↗
write_vol(path, mask::AbstractArray{Bool, 3}, lower::NTuple{3, <:Integer}) -> path

Write a Bool mask as a DGtal .vol (Version 2, 1 mm voxels): voxel mask[i, j, k] is the point lower .+ (i, j, k) .- 1.

src/morphometry.jl

dds_ordersfunctionsource ↗
dds_orders(m; maxiter = 100, tol = 0.01) -> NamedTuple

Diameter-defined Strahler ordering (Jiang, Kassab and Fung 1994), from the tree's own diameters:

  1. Start from topological Strahler orders (leaves 1; a parent takes the largest child order, plus one when two children share it; a single child passes its order through).

  2. Elements are maximal connected runs of segments with the same order; an element's diameter is the mean of its segments' diameters. Per order, take the mean D̄ₙ and SD σₙ of element diameters; the upper boundary of order n is ½[(D̄ₙ + σₙ) + (D̄ₙ₊₁ − σₙ₊₁)] (Eq. 12).

  3. Recompute orders from the leaves up: where children meet, the parent starts from the largest child order k (k is reached by two or more children, or by one child), and is promoted to k + 1 only if its diameter exceeds the upper boundary of order k.

  4. Repeat 2-3 until fewer than tol of the segments change order.

Returns per-element orders, diameters and lengths, plus per-order count, mean and SD of element diameters.

src/postprocess.jl

postprocessfunctionsource ↗
postprocess(xml_path, prefix; domain_vol = nothing, mass = :mask, elements = :series,
            obj = true, verbose = true) -> NamedTuple

Port of upstream coronary_postprocess.run(xml_path, prefix, domain_vol). Reads a tree XML written by export_gxl and writes

  • <prefix>_connectivity_matrix.csv, <prefix>_diameter_stats.csv, <prefix>_diameter_ranges.csv

  • <prefix>_tree.vtp (cell arrays radius and final_order) and <prefix>_tree.obj (24-sided capped tube mesh; skipped with obj = false)

  • <prefix>_hemodynamics.csv when domain_vol is given

Returns the computed data: per-segment diameters and diameter-defined orders, the ordering internals (strahler, seg2elem, elem_order), the connectivity mean/SE matrices (indexed [daughter order, parent order]), per-order diameter_stats, and hemodynamics (nothing without a domain).

Two upstream bugs are corrected by default; each has an option that restores the upstream behaviour, used to verify the port against the Python:

  • mass = :mask decodes the domain and counts voxels > 128; mass = :upstream counts the non-zero bytes of the (possibly compressed) file payload, see domain_mass_g.

  • elements = :series builds elements from same-order segments joined end to end; elements = :upstream also joins siblings, see diameter_defined_orders.

postprocess(...; mass = :upstream, elements = :upstream) reproduces the Python outputs.

src/qc.jl

tree_qcfunctionsource ↗
tree_qc(E; rmin = 0.25, hops = 2) -> NamedTuple

Checks the vessels with radius ≥ rmin (mm) of a grown tree (or forest):

  • overlaps: pairs of segments whose tubes intersect (centerline distance < sum of radii), with the deepest penetration in mm. Pairs that must touch at a shared junction are skipped: segments within hops steps of each other (a step joins segments sharing an end node: parent and child, or siblings), and segments joined through the tree by a centerline path shorter than their two radii plus the largest radius along the path (short, wide imaged segments around a bifurcation, such as a left main and its first branches).

  • turns: joints between a segment and its parent whose direction changes by more than 90° or 135°, split into imaged base-tree vessels and grown vessels.