From 6c541c77c66e8104173afab2cf753387b5aa49bc Mon Sep 17 00:00:00 2001 From: cainja Date: Sun, 6 Jul 2025 21:42:01 -0700 Subject: [PATCH 1/9] refactor remodel and graph utilities --- .../env/component/PatchComponentRemodel.java | 12 +-- .../PatchComponentSitesGraphUtilities.java | 96 +++++++++++++++---- 2 files changed, 79 insertions(+), 29 deletions(-) diff --git a/src/arcade/patch/env/component/PatchComponentRemodel.java b/src/arcade/patch/env/component/PatchComponentRemodel.java index b2e1b4f6d..659b738a8 100644 --- a/src/arcade/patch/env/component/PatchComponentRemodel.java +++ b/src/arcade/patch/env/component/PatchComponentRemodel.java @@ -13,6 +13,8 @@ import static arcade.patch.env.component.PatchComponentSitesGraphUtilities.MAXIMUM_WALL_RADIUS_FRACTION; import static arcade.patch.env.component.PatchComponentSitesGraphUtilities.MINIMUM_CAPILLARY_RADIUS; import static arcade.patch.env.component.PatchComponentSitesGraphUtilities.MINIMUM_WALL_THICKNESS; +import static arcade.patch.env.component.PatchComponentSitesGraphUtilities.calculateCurrentState; +import static arcade.patch.env.component.PatchComponentSitesGraphUtilities.updateGraph; import static arcade.patch.util.PatchEnums.Ordering; /** @@ -175,15 +177,9 @@ public void step(SimState state) { // If any edges are removed, update the graph edges that are ignored. // Otherwise, recalculate pressure, flow, and stresses. if (removed) { - PatchComponentSitesGraphUtilities.updateGraph(graph); + updateGraph(graph); } else { - PatchComponentSitesGraphUtilities.calculatePressures(graph); - boolean reversed = PatchComponentSitesGraphUtilities.reversePressures(graph); - if (reversed) { - PatchComponentSitesGraphUtilities.calculatePressures(graph); - } - PatchComponentSitesGraphUtilities.calculateFlows(graph); - PatchComponentSitesGraphUtilities.calculateStresses(graph); + calculateCurrentState(graph); } } diff --git a/src/arcade/patch/env/component/PatchComponentSitesGraphUtilities.java b/src/arcade/patch/env/component/PatchComponentSitesGraphUtilities.java index 63c6bb050..56354732b 100644 --- a/src/arcade/patch/env/component/PatchComponentSitesGraphUtilities.java +++ b/src/arcade/patch/env/component/PatchComponentSitesGraphUtilities.java @@ -155,8 +155,19 @@ private static double calculateViscosity(double radius) { * @return the flow rate coefficient */ private static double getCoefficient(SiteEdge edge) { - double mu = PLASMA_VISCOSITY * calculateViscosity(edge.radius) / 60; - return (Math.PI * Math.pow(edge.radius, 4)) / (8 * mu * edge.length); + return getCoefficient(edge.radius, edge.length); + } + + /** + * Gets flow rate coefficient in units of um3/(mmHg min). + * + * @param radius the edge radius + * @param length the edge length + * @return the flow rate coefficient + */ + private static double getCoefficient(double radius, double length) { + double mu = PLASMA_VISCOSITY * calculateViscosity(radius) / 60; + return (Math.PI * Math.pow(radius, 4)) / (8 * mu * length); } /** @@ -473,6 +484,10 @@ static void calculatePressures(Graph graph) { if (div != 0) { x0[id] /= div; } + + if (node.pressure > 0) { + x0[id] = node.pressure; + } } double[][] sA = Matrix.scale(mA, 1E-7); @@ -559,11 +574,21 @@ static void calculateFlows(Graph graph) { static void calculateThicknesses(Graph graph) { for (Object obj : graph.getAllEdges()) { SiteEdge edge = (SiteEdge) obj; - double d = 2 * edge.radius; - edge.wall = d * (0.267 - 0.084 * Math.log10(d)); + edge.wall = calculateThickness(edge); } } + /** + * Calculates the thickness of an edge. + * + * @param edge the edge object + * @return the thickness of the edge + */ + static double calculateThickness(SiteEdge edge) { + double d = 2 * edge.radius; + return d * (0.267 - 0.084 * Math.log10(d)); + } + /** * Gets in degree for edge in given calculation direction. * @@ -892,7 +917,7 @@ static void path(Graph graph, SiteNode start, SiteNode end) { settled.add(evalNode); // If end node found, exit from loop. - if (evalNode == end) { + if (evalNode.equals(end)) { break; } @@ -1099,6 +1124,42 @@ static void updateRadii( * @param graph the graph object */ static void updateGraph(Graph graph) { + trimGraph(graph); + + calculateCurrentState(graph); + + // Set oxygen nodes. + for (Object obj : graph.getAllEdges()) { + SiteEdge edge = (SiteEdge) obj; + SiteNode to = edge.getTo(); + SiteNode from = edge.getFrom(); + if (Double.isNaN(to.pressure)) { + to.oxygen = Double.NaN; + } + if (Double.isNaN(from.pressure)) { + from.oxygen = Double.NaN; + } + } + } + + /** + * Updates hemodynamic properties based on the current state of the graph. + * + * @param graph the graph object + */ + static void calculateCurrentState(Graph graph) { + do { + calculatePressures(graph); + boolean reversed = reversePressures(graph); + if (reversed) { + calculatePressures(graph); + } + calculateFlows(graph); + calculateStresses(graph); + } while (checkForNegativeFlow(graph)); + } + + static void trimGraph(Graph graph) { ArrayList list; Graph gCurr = graph; @@ -1133,27 +1194,20 @@ static void updateGraph(Graph graph) { gCurr = gNew; } while (list.size() != 0); + } - calculatePressures(graph); - boolean reversed = reversePressures(graph); - if (reversed) { - calculatePressures(graph); - } - calculateFlows(graph); - calculateStresses(graph); - - // Set oxygen nodes. + // This *MIGHT* be a problem, I think we could revisit adding this check. + // I'm not sure why it would get to the point where there would be a negative flow in the graph? + static boolean checkForNegativeFlow(Graph graph) { + boolean negative = false; for (Object obj : graph.getAllEdges()) { SiteEdge edge = (SiteEdge) obj; - SiteNode to = edge.getTo(); - SiteNode from = edge.getFrom(); - if (Double.isNaN(to.pressure)) { - to.oxygen = Double.NaN; - } - if (Double.isNaN(from.pressure)) { - from.oxygen = Double.NaN; + if (edge.flow < 0) { + negative = true; + break; } } + return negative; } /** From da6a05d5539af1e4f3f5dbe5a408509683e2a74c Mon Sep 17 00:00:00 2001 From: cainja Date: Sun, 6 Jul 2025 21:42:01 -0700 Subject: [PATCH 2/9] refactoring component sites graph --- .../component/PatchComponentSitesGraph.java | 88 +++++++++++++------ 1 file changed, 61 insertions(+), 27 deletions(-) diff --git a/src/arcade/patch/env/component/PatchComponentSitesGraph.java b/src/arcade/patch/env/component/PatchComponentSitesGraph.java index ca5bf1b1c..f1a5cfd2c 100644 --- a/src/arcade/patch/env/component/PatchComponentSitesGraph.java +++ b/src/arcade/patch/env/component/PatchComponentSitesGraph.java @@ -13,9 +13,6 @@ import arcade.core.util.MiniBox; import arcade.core.util.Solver; import arcade.core.util.Solver.Function; -import arcade.patch.env.component.PatchComponentSitesGraphFactory.EdgeLevel; -import arcade.patch.env.component.PatchComponentSitesGraphFactory.EdgeTag; -import arcade.patch.env.component.PatchComponentSitesGraphFactory.EdgeType; import arcade.patch.env.location.CoordinateXYZ; import arcade.patch.sim.PatchSeries; import arcade.patch.util.PatchEnums.ComponentType; @@ -243,29 +240,11 @@ void simpleStep() { * @param random the random number generator */ void complexStep(MersenneTwisterFast random) { - Bag allEdges = new Bag(graph.getAllEdges()); - // Check if graph has become unconnected. - boolean isConnected = false; - for (Object obj : allEdges) { - SiteEdge edge = (SiteEdge) obj; - if (edge.getFrom().isRoot && !edge.isIgnored) { - isConnected = true; - break; - } - } - if (!isConnected) { - for (SiteLayer layer : layers) { - for (int k = 0; k < latticeHeight; k++) { - for (int i = 0; i < latticeLength; i++) { - for (int j = 0; j < latticeWidth; j++) { - layer.delta[k][i][j] = 0; - } - } - } - } + if (checkDisconnected()) { return; } + ; // Iterate through each molecule. for (SiteLayer layer : layers) { @@ -285,10 +264,11 @@ void complexStep(MersenneTwisterFast random) { } } - allEdges.shuffle(random); + Bag currentEdges = new Bag(graph.getAllEdges()); + currentEdges.shuffle(random); // Iterate through each edge in graph. - for (Object obj : allEdges) { + for (Object obj : currentEdges) { SiteEdge edge = (SiteEdge) obj; if (edge.isIgnored) { continue; @@ -381,6 +361,34 @@ void complexStep(MersenneTwisterFast random) { } } + private boolean checkDisconnected() { + Bag allEdges = new Bag(graph.getAllEdges()); + + // Check if graph has become unconnected. + boolean isConnected = false; + for (Object obj : allEdges) { + SiteEdge edge = (SiteEdge) obj; + if (edge.getFrom().isRoot && !edge.isIgnored) { + isConnected = true; + break; + } + } + if (!isConnected) { + for (SiteLayer layer : layers) { + for (int k = 0; k < latticeHeight; k++) { + for (int i = 0; i < latticeLength; i++) { + for (int j = 0; j < latticeWidth; j++) { + layer.delta[k][i][j] = 0; + } + } + } + } + return true; + } + + return false; + } + /** * Extension of {@link arcade.core.util.Graph.Node} for site nodes. * @@ -402,6 +410,12 @@ public static class SiteNode extends Node { /** Distance for Dijkstra's algorithm. */ int distance; + /** Tick for the last update during growth. */ + int lastUpdate; + + /** Tick for when the node was added to the graph. */ + int addTime; + /** Parent node. */ SiteNode prev; @@ -419,7 +433,7 @@ public static class SiteNode extends Node { } @Override - public Node duplicate() { + public SiteNode duplicate() { return new SiteNode(x, y, z); } @@ -517,7 +531,7 @@ public static class SiteEdge extends Edge { * @param type the edge type * @param level the graph resolution level */ - SiteEdge(Node from, Node to, EdgeType type, EdgeLevel level) { + SiteEdge(SiteNode from, SiteNode to, EdgeType type, EdgeLevel level) { super(from, to); this.type = type; this.level = level; @@ -609,6 +623,22 @@ public double getCircum() { public double getFlow() { return flow; } + + public String getFraction() { + StringBuilder sb = new StringBuilder(); + for (String key : fraction.keySet()) { + sb.append(key + ":" + fraction.get(key) + ","); + } + return sb.toString(); + } + + public String getTransport() { + StringBuilder sb = new StringBuilder(); + for (String key : transport.keySet()) { + sb.append(key + ":" + transport.get(key) + ","); + } + return sb.toString(); + } } /** @@ -763,6 +793,10 @@ private ArrayList traverseEdge(SiteNode node, String code) { } } + // If the flow out is zero exit and return no children. + if (flowOut == 0) { + return children; + } // Assign new fractions. for (Object obj : out) { SiteEdge edge = (SiteEdge) obj; From 07a4e8c36ba7ad15d699fcba8cb46340f6ec998a Mon Sep 17 00:00:00 2001 From: cainja Date: Mon, 9 Jun 2025 17:39:07 -0700 Subject: [PATCH 3/9] refactory graph factory --- .../PatchComponentSitesGraphFactory.java | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/arcade/patch/env/component/PatchComponentSitesGraphFactory.java b/src/arcade/patch/env/component/PatchComponentSitesGraphFactory.java index e9ccfb69c..851260272 100644 --- a/src/arcade/patch/env/component/PatchComponentSitesGraphFactory.java +++ b/src/arcade/patch/env/component/PatchComponentSitesGraphFactory.java @@ -188,6 +188,12 @@ enum EdgeMotif { /** Width of the array (y direction). */ final int latticeWidth; + /** List of pointers to artery node and edge objects. */ + ArrayList arteries; + + /** List of pointers to vein node and edge objects. */ + ArrayList veins; + /** * Creates a factory for making {@link Graph} sites. * @@ -604,8 +610,8 @@ public Graph initializeRootGraph(MersenneTwisterFast random, String graphLayout) leaves.addAll(bag); } - ArrayList arteries = new ArrayList<>(); - ArrayList veins = new ArrayList<>(); + arteries = new ArrayList<>(); + veins = new ArrayList<>(); boolean hasArtery = false; boolean hasVein = false; @@ -643,19 +649,19 @@ public Graph initializeRootGraph(MersenneTwisterFast random, String graphLayout) addMotifs(graph, leaves2, EdgeLevel.LEVEL_1, EdgeMotif.SINGLE, random); // Calculate radii, pressure, and shears. - updateRootGraph(graph, arteries, veins, EdgeLevel.LEVEL_1, random); + updateRootGraph(graph, EdgeLevel.LEVEL_1, random); // Iterative remodeling. int iter = 0; double frac = 1.0; while (frac > REMODELING_FRACTION && iter < MAX_ITERATIONS) { frac = remodelRootGraph(graph, EdgeLevel.LEVEL_1, random); - updateRootGraph(graph, arteries, veins, EdgeLevel.LEVEL_1, random); + updateRootGraph(graph, EdgeLevel.LEVEL_1, random); iter++; } // Prune network for perfused segments and recalculate properties. - refineRootGraph(graph, arteries, veins); + refineRootGraph(graph); // Subdivide growth sites and add new motifs. Bag midpoints = subdivideRootGraph(graph, EdgeLevel.LEVEL_1); @@ -664,10 +670,10 @@ public Graph initializeRootGraph(MersenneTwisterFast random, String graphLayout) addMotifs(graph, midpoints2, EdgeLevel.LEVEL_2, EdgeMotif.SINGLE, random); // Calculate radii, pressure, and shears. - updateRootGraph(graph, arteries, veins, EdgeLevel.LEVEL_2, random); + updateRootGraph(graph, EdgeLevel.LEVEL_2, random); // Prune network for perfused segments and recalculate properties. - refineRootGraph(graph, arteries, veins); + refineRootGraph(graph); return graph; } @@ -676,17 +682,10 @@ public Graph initializeRootGraph(MersenneTwisterFast random, String graphLayout) * Updates hemodynamic properties for graph sites with root layouts. * * @param graph the graph instance - * @param arteries the list of artery edges - * @param veins the list of vein edges * @param level the graph resolution level * @param random the random number generator */ - private void updateRootGraph( - Graph graph, - ArrayList arteries, - ArrayList veins, - EdgeLevel level, - MersenneTwisterFast random) { + private void updateRootGraph(Graph graph, EdgeLevel level, MersenneTwisterFast random) { ArrayList list; ArrayList caps = new ArrayList<>(); @@ -811,10 +810,8 @@ private void updateRootGraph( * Refines the graph for graph sites with root layouts. * * @param graph the graph instance - * @param arteries the list of artery edges - * @param veins the list of vein edges */ - private void refineRootGraph(Graph graph, ArrayList arteries, ArrayList veins) { + private void refineRootGraph(Graph graph) { // Reverse edges that are veins and venules. ArrayList reverse = getEdgeByType(graph, new EdgeType[] {EdgeType.VEIN, EdgeType.VENULE}); From 7432b5ba2e454d4b5ccfa8135a29808a3e26804e Mon Sep 17 00:00:00 2001 From: cainja Date: Tue, 17 Jun 2025 14:04:31 -0700 Subject: [PATCH 4/9] update core graph features for angiogenesis --- src/arcade/core/util/Graph.java | 150 ++++++++++++++++++++++++++- test/arcade/core/util/GraphTest.java | 65 +++++++++++- 2 files changed, 210 insertions(+), 5 deletions(-) diff --git a/src/arcade/core/util/Graph.java b/src/arcade/core/util/Graph.java index 77e37bfc8..f062344b2 100644 --- a/src/arcade/core/util/Graph.java +++ b/src/arcade/core/util/Graph.java @@ -31,6 +31,9 @@ public enum Strategy { /** Collection of all {@code Edge} objects in a graph. */ private final Bag allEdges; + /** Map of {@code Node} objects, for lookup. */ + private final Map nodes; + /** Map of {@code Node} OUT to bag of {@code Edge} objects. */ private final Map nodeToOutBag; @@ -42,6 +45,7 @@ public Graph() { allEdges = new Bag(); nodeToOutBag = new HashMap<>(); nodeToInBag = new HashMap<>(); + nodes = new HashMap<>(); } /** @@ -53,6 +57,24 @@ public void update(Graph graph) { allEdges.addAll(graph.allEdges); nodeToOutBag.putAll(graph.nodeToOutBag); nodeToInBag.putAll(graph.nodeToInBag); + updateNodes(); + } + + /** Updates nodes from graph. */ + private void updateNodes() { + nodes.clear(); + for (Object obj : allEdges) { + Edge edge = (Edge) obj; + Node from = edge.getFrom(); + Node to = edge.getTo(); + + if (!nodes.containsKey(from.toString())) { + nodes.put(from.toString(), from); + } + if (!nodes.containsKey(to.toString())) { + nodes.put(to.toString(), to); + } + } } /** Clear edges and nodes from graph. */ @@ -60,6 +82,7 @@ public void clear() { allEdges.clear(); nodeToOutBag.clear(); nodeToInBag.clear(); + nodes.clear(); } /** @@ -91,6 +114,28 @@ public boolean contains(Edge edge) { return checkEdge(edge); } + /** + * Retrieve the node object for the given coordinates. Returns null if no node exists. + * + * @param x the x coordinate + * @param y the y coordinate + * @param z the z coordinate + * @return the node object + */ + public Node lookup(int x, int y, int z) { + return nodes.get("(" + x + "," + y + "," + z + ")"); + } + + /** + * Retrieve the node object for the given coordinates. Returns null if no node exists. + * + * @param node to lookup an original node + * @return the node object + */ + public Node lookup(Node node) { + return nodes.get(node.toString()); + } + /** * Gets all edges in the graph. * @@ -206,6 +251,7 @@ public void getSubgraph(Graph g, GraphFilter f) { for (Object obj : allEdges) { Edge edge = (Edge) obj; if (f.filter(edge)) { + g.addNodes(edge); g.allEdges.add(edge); g.setOutMap(edge.getFrom(), edge); g.setInMap(edge.getTo(), edge); @@ -255,6 +301,8 @@ public void mergeNodes() { e.setTo(join); } } + + nodes.put(join.toString(), join); } } @@ -274,14 +322,44 @@ public void addEdge(Node from, Node to) { * @param edge the edge to add */ public void addEdge(Edge edge) { + addNodes(edge); allEdges.add(edge); setOutMap(edge.getFrom(), edge); setInMap(edge.getTo(), edge); setLinks(edge); } + public void addEdge(Edge edge, boolean duplicate) { + allEdges.add(edge); + setOutMap(edge.getFrom(), edge, duplicate); + setInMap(edge.getTo(), edge, duplicate); + setLinks(edge); + addNodes(edge); + } + + /** + * Helper function to adds the nodes from an edge to a graph. + * + * @param edge the edge to add + */ + private void addNodes(Edge edge) { + Node from = edge.getFrom(); + Node to = edge.getTo(); + if (!nodes.containsKey(from.toString())) { + nodes.put(from.toString(), from); + } else { + edge.setFrom(nodes.get(from.toString())); + } + if (!nodes.containsKey(to.toString())) { + nodes.put(to.toString(), to); + } else { + edge.setTo(nodes.get(to.toString())); + } + } + /** - * Adds the edge to the bag for the mapping of OUT node to edge. + * Adds the edge to the bag for the mapping of OUT node to edge. Default behavior is to + * duplicate nodes. * * @param node the node hash * @param edge the edge @@ -296,7 +374,25 @@ private void setOutMap(Node node, Edge edge) { } /** - * Adds the edge to the bag for the mapping of IN node to edge. + * Adds the edge to the bag for the mapping of OUT node to edge. Used in cases new node object + * is not needed. + * + * @param node the node hash + * @param edge the edge + */ + private void setOutMap(Node node, Edge edge, boolean duplicate) { + Bag objs = nodeToOutBag.get(node); + if (objs == null) { + objs = new Bag(10); + Node bagNode = duplicate ? node.duplicate() : node; + nodeToOutBag.put(bagNode, objs); + } + objs.add(edge); + } + + /** + * Adds the edge to the bag for the mapping of IN node to edge. Default behavior is to duplicate + * nodes. * * @param node the node hash * @param edge the edge @@ -310,6 +406,23 @@ private void setInMap(Node node, Edge edge) { objs.add(edge); } + /** + * Adds the edge to the bag for the mapping of OUT node to edge. Used in cases new node object + * is not needed. + * + * @param node the node hash + * @param edge the edge + */ + private void setInMap(Node node, Edge edge, boolean duplicate) { + Bag objs = nodeToInBag.get(node); + if (objs == null) { + objs = new Bag(10); + Node bagNode = duplicate ? node.duplicate() : node; + nodeToInBag.put(bagNode, objs); + } + objs.add(edge); + } + /** * Adds links between edges in and out of the nodes for a given edge. * @@ -353,6 +466,19 @@ public void removeEdge(Edge edge) { unsetOutMap(edge.getFrom(), edge); unsetInMap(edge.getTo(), edge); unsetLinks(edge); + removeNodeIfDetached(edge.getFrom()); + removeNodeIfDetached(edge.getTo()); + } + + /** + * Remove a node if it is detached from the graph. + * + * @param node the node to check + */ + private void removeNodeIfDetached(Node node) { + if (!nodeToInBag.containsKey(node) && !nodeToOutBag.containsKey(node)) { + nodes.remove(node.toString()); + } } /** @@ -730,7 +856,8 @@ public static class Edge { private final ArrayList edgesOut; /** - * Creates an {@code Edge} between two {@link Node} objects. + * Creates an {@code Edge} between two {@link Node} objects. Default behavior is to + * duplicate nodes. * * @param from the node the edge is from * @param to the node the edge is to @@ -742,6 +869,21 @@ public Edge(Node from, Node to) { edgesOut = new ArrayList<>(); } + /** + * Creates an {@code Edge} object for graph sites. Used in cases where a new node object is + * not needed. + * + * @param from the node the edge is from + * @param to the node the edge is to + * @param duplicate {@code true} if nodes should be duplicated, {@code false} otherwise + */ + public Edge(Node from, Node to, boolean duplicate) { + this.from = duplicate ? from.duplicate() : from; + this.to = duplicate ? to.duplicate() : to; + edgesIn = new ArrayList<>(); + edgesOut = new ArrayList<>(); + } + /** * Gets the node the edge points to based on the calculation strategy (e.g. upstream or * downstream). @@ -822,7 +964,7 @@ public ArrayList getEdgesOut() { * * @return the reversed edge */ - Edge reverse() { + public Edge reverse() { Node tempTo = to; Node tempFrom = from; to = tempFrom; diff --git a/test/arcade/core/util/GraphTest.java b/test/arcade/core/util/GraphTest.java index 683577224..984f2ef45 100644 --- a/test/arcade/core/util/GraphTest.java +++ b/test/arcade/core/util/GraphTest.java @@ -797,7 +797,8 @@ public void update_updatesGraph() { () -> assertTrue(graph.contains(edge3)), () -> assertTrue(graph.contains(edge4)), () -> assertTrue(graph.contains(edge5)), - () -> assertTrue(graph.contains(edge6))); + () -> assertTrue(graph.contains(edge6)), + () -> assertEquals(graph.lookup(node3B), graph.lookup(new Node(-2, -2, 0)))); } @Test @@ -861,4 +862,66 @@ public void graph_toString_returnsString() { assertEquals(expected, graph.toString()); } + + @Test + public void lookup_returnsNode() { + Graph graph = new Graph(); + Node node0 = new Node(-1, 0, 0); + Node node1 = new Node(0, 0, 0); + Node node2 = new Node(3, 0, 0); + + Edge edge0 = new Edge(node0, node1); + Edge edge1 = new Edge(node1, node2); + + graph.addEdge(edge0); + graph.addEdge(edge1); + + Node node0Copy = new Node(-1, 0, 0); + Node node1Copy = new Node(0, 0, 0); + Node node2Copy = new Node(3, 0, 0); + + assertAll( + () -> assertFalse(node0Copy == graph.lookup(-1, 0, 0)), + () -> assertFalse(node1Copy == graph.lookup(0, 0, 0)), + () -> assertFalse(node2Copy == graph.lookup(3, 0, 0)), + () -> assertTrue(graph.lookup(node0Copy) == graph.lookup(-1, 0, 0)), + () -> assertTrue(graph.lookup(node1Copy) == graph.lookup(0, 0, 0)), + () -> assertTrue(graph.lookup(node2Copy) == graph.lookup(3, 0, 0))); + } + + @Test + public void lookup_returnsNull() { + Graph graph = new Graph(); + Node node0 = new Node(-1, 0, 0); + Node node1 = new Node(0, 0, 0); + Node node2 = new Node(3, 0, 0); + + Edge edge0 = new Edge(node0, node1); + Edge edge1 = new Edge(node1, node2); + + graph.addEdge(edge0); + graph.addEdge(edge1); + + assertAll( + () -> assertNull(graph.lookup(-2, 0, 0)), () -> assertNull(graph.lookup(4, 0, 0))); + } + + @Test + public void lookup_returnsNullAfterEdgeIsRemoved() { + Graph graph = new Graph(); + Node node0 = new Node(-1, 0, 0); + Node node1 = new Node(0, 0, 0); + Node node2 = new Node(3, 0, 0); + + Edge edge0 = new Edge(node0, node1); + Edge edge1 = new Edge(node1, node2); + + graph.addEdge(edge0); + graph.addEdge(edge1); + graph.removeEdge(edge1); + + assertAll( + () -> assertNotNull(graph.lookup(0, 0, 0)), + () -> assertNull(graph.lookup(3, 0, 0))); + } } From f0a1f673d5a17b99cc5f962288c21e5b810551a8 Mon Sep 17 00:00:00 2001 From: Krista Phommatha Date: Mon, 28 Jul 2025 17:08:06 -0700 Subject: [PATCH 5/9] fixing object handling in mergeGraph() --- src/arcade/core/util/Graph.java | 12 ++++++++++++ .../component/PatchComponentSitesGraphFactory.java | 2 +- .../component/PatchComponentSitesGraphUtilities.java | 8 +++++--- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/arcade/core/util/Graph.java b/src/arcade/core/util/Graph.java index f062344b2..25c292f96 100644 --- a/src/arcade/core/util/Graph.java +++ b/src/arcade/core/util/Graph.java @@ -77,6 +77,18 @@ private void updateNodes() { } } + public void combine(Graph graph1, Graph graph2) { + clear(); + for (Object obj : graph2.getAllEdges()) { + Edge edge = (Edge) obj; + addEdge(edge); + } + for (Object obj : graph1.getAllEdges()) { + Edge edge = (Edge) obj; + addEdge(edge); + } + } + /** Clear edges and nodes from graph. */ public void clear() { allEdges.clear(); diff --git a/src/arcade/patch/env/component/PatchComponentSitesGraphFactory.java b/src/arcade/patch/env/component/PatchComponentSitesGraphFactory.java index 851260272..27af8d701 100644 --- a/src/arcade/patch/env/component/PatchComponentSitesGraphFactory.java +++ b/src/arcade/patch/env/component/PatchComponentSitesGraphFactory.java @@ -761,7 +761,7 @@ private void updateRootGraph(Graph graph, EdgeLevel level, MersenneTwisterFast r Graph g2 = new Graph(); graph.getSubgraph(g1, e -> ((SiteEdge) e).level == EdgeLevel.LEVEL_1); graph.getSubgraph(g2, e -> ((SiteEdge) e).level == EdgeLevel.LEVEL_2); - mergeGraphs(g1, g2); + mergeGraphs(graph, g1, g2); break; default: break; diff --git a/src/arcade/patch/env/component/PatchComponentSitesGraphUtilities.java b/src/arcade/patch/env/component/PatchComponentSitesGraphUtilities.java index 56354732b..30a62c061 100644 --- a/src/arcade/patch/env/component/PatchComponentSitesGraphUtilities.java +++ b/src/arcade/patch/env/component/PatchComponentSitesGraphUtilities.java @@ -359,10 +359,11 @@ static void checkPerfused(Graph graph, ArrayList arteries, ArrayList /** * Merges the nodes from one graph with another graph. * - * @param graph1 the first graph object - * @param graph2 the second graph object + * @param graph the original graph object + * @param graph1 the first subgraph object + * @param graph2 the second subgraph object */ - static void mergeGraphs(Graph graph1, Graph graph2) { + static void mergeGraphs(Graph graph, Graph graph1, Graph graph2) { // Merge nodes for subgraph. graph2.mergeNodes(); @@ -385,6 +386,7 @@ static void mergeGraphs(Graph graph1, Graph graph2) { } } } + graph.combine(graph1, graph2); } /** From ca46551f59688b49b01edd63198b4668e379a892 Mon Sep 17 00:00:00 2001 From: cainja Date: Sun, 6 Jul 2025 21:42:01 -0700 Subject: [PATCH 6/9] add more graph logging --- src/arcade/patch/sim/output/PatchOutputSerializer.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/arcade/patch/sim/output/PatchOutputSerializer.java b/src/arcade/patch/sim/output/PatchOutputSerializer.java index cdfa02efa..44996416f 100644 --- a/src/arcade/patch/sim/output/PatchOutputSerializer.java +++ b/src/arcade/patch/sim/output/PatchOutputSerializer.java @@ -244,6 +244,8 @@ public JsonElement serialize( json.addProperty("shear", src.getShear()); json.addProperty("stress", src.getCircum()); json.addProperty("flow", src.getFlow()); + json.addProperty("fraction", src.getFraction()); + json.addProperty("transport", src.getTransport()); return json; } From 2748b94685ba207322eaf9483d4cc072b50833f6 Mon Sep 17 00:00:00 2001 From: cainja Date: Fri, 13 Jun 2025 11:46:43 -0700 Subject: [PATCH 7/9] quick fomratting fix --- test/arcade/patch/sim/PatchSeriesTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/arcade/patch/sim/PatchSeriesTest.java b/test/arcade/patch/sim/PatchSeriesTest.java index 357a0e9d8..8d28989f9 100644 --- a/test/arcade/patch/sim/PatchSeriesTest.java +++ b/test/arcade/patch/sim/PatchSeriesTest.java @@ -80,9 +80,9 @@ public static void setupParameters() { PARAMETERS.addTag(PATCH_PARAMETER_NAMES[i], "PATCH"); PARAMETERS.addAtt(PATCH_PARAMETER_NAMES[i], "value", "" + PATCH_PARAMETER_VALUES[i]); } - MiniBox potts = PARAMETERS.getIdValForTag("PATCH"); - for (String key : potts.getKeys()) { - PATCH.put(key, potts.get(key)); + MiniBox patch = PARAMETERS.getIdValForTag("PATCH"); + for (String key : patch.getKeys()) { + PATCH.put(key, patch.get(key)); } // POPULATION From f97d1f5e783d6dedd3cfa0191f71d20f045cd69f Mon Sep 17 00:00:00 2001 From: Krista Phommatha Date: Mon, 15 Jun 2026 15:02:40 -0700 Subject: [PATCH 8/9] I am not sure how this semicolon got there? --- src/arcade/patch/env/component/PatchComponentSitesGraph.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/arcade/patch/env/component/PatchComponentSitesGraph.java b/src/arcade/patch/env/component/PatchComponentSitesGraph.java index f1a5cfd2c..93fb1a51d 100644 --- a/src/arcade/patch/env/component/PatchComponentSitesGraph.java +++ b/src/arcade/patch/env/component/PatchComponentSitesGraph.java @@ -244,7 +244,6 @@ void complexStep(MersenneTwisterFast random) { if (checkDisconnected()) { return; } - ; // Iterate through each molecule. for (SiteLayer layer : layers) { From c3c06cf27ee4ab5efafb964cd38862218f018784 Mon Sep 17 00:00:00 2001 From: Krista Phommatha Date: Wed, 17 Jun 2026 15:11:01 -0700 Subject: [PATCH 9/9] add javadocs --- src/arcade/core/util/Graph.java | 24 ++++++++++++++++- .../component/PatchComponentSitesGraph.java | 23 ++++++++++++++++ .../PatchComponentSitesGraphUtilities.java | 27 +++++++++++++++++-- 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/src/arcade/core/util/Graph.java b/src/arcade/core/util/Graph.java index 25c292f96..c010a59c8 100644 --- a/src/arcade/core/util/Graph.java +++ b/src/arcade/core/util/Graph.java @@ -77,6 +77,17 @@ private void updateNodes() { } } + /** + * Replaces this graph with the combined contents of two graphs. + * + *

All existing edges are removed and the edges from {@code graph1} and {@code graph2} are + * copied into this graph. This method should be used after node references have been merged or + * reconciled between the two graphs, producing a single graph containing the same edge objects + * of both inputs. + * + * @param graph1 the first graph to combine + * @param graph2 the second graph to combine + */ public void combine(Graph graph1, Graph graph2) { clear(); for (Object obj : graph2.getAllEdges()) { @@ -329,7 +340,7 @@ public void addEdge(Node from, Node to) { } /** - * Adds edge to graph. + * Adds edge to graph. Default behavior is to duplicate nodes. * * @param edge the edge to add */ @@ -341,6 +352,13 @@ public void addEdge(Edge edge) { setLinks(edge); } + /** + * Adds edge to graph. + * + * @param edge the edge to add + * @param duplicate {@code true} if desired behavior is to duplicate nodes, {@code false} + * otherwise + */ public void addEdge(Edge edge, boolean duplicate) { allEdges.add(edge); setOutMap(edge.getFrom(), edge, duplicate); @@ -391,6 +409,8 @@ private void setOutMap(Node node, Edge edge) { * * @param node the node hash * @param edge the edge + * @param duplicate {@code true} if desired behavior is to duplicate nodes, {@code false} + * otherwise */ private void setOutMap(Node node, Edge edge, boolean duplicate) { Bag objs = nodeToOutBag.get(node); @@ -424,6 +444,8 @@ private void setInMap(Node node, Edge edge) { * * @param node the node hash * @param edge the edge + * @param duplicate {@code true} if desired behavior is to duplicate nodes, {@code false} + * otherwise */ private void setInMap(Node node, Edge edge, boolean duplicate) { Bag objs = nodeToInBag.get(node); diff --git a/src/arcade/patch/env/component/PatchComponentSitesGraph.java b/src/arcade/patch/env/component/PatchComponentSitesGraph.java index 93fb1a51d..216c28595 100644 --- a/src/arcade/patch/env/component/PatchComponentSitesGraph.java +++ b/src/arcade/patch/env/component/PatchComponentSitesGraph.java @@ -13,6 +13,9 @@ import arcade.core.util.MiniBox; import arcade.core.util.Solver; import arcade.core.util.Solver.Function; +import arcade.patch.env.component.PatchComponentSitesGraphFactory.EdgeLevel; +import arcade.patch.env.component.PatchComponentSitesGraphFactory.EdgeTag; +import arcade.patch.env.component.PatchComponentSitesGraphFactory.EdgeType; import arcade.patch.env.location.CoordinateXYZ; import arcade.patch.sim.PatchSeries; import arcade.patch.util.PatchEnums.ComponentType; @@ -360,6 +363,16 @@ void complexStep(MersenneTwisterFast random) { } } + /** + * Checks whether the graph remains connected to at least one root node. + * + *

A graph is considered connected if it contains a non-ignored edge originating from a root + * node. If no such edge exists, all layer delta values are reset to zero to reflect the + * disconnected state. + * + * @return {@code true} if the graph is disconnected and the layer deltas were reset; {@code + * false} if the graph remains connected + */ private boolean checkDisconnected() { Bag allEdges = new Bag(graph.getAllEdges()); @@ -623,6 +636,11 @@ public double getFlow() { return flow; } + /** + * Get the concentration fraction in edge as a string. + * + * @return the string represenation of the fraction + */ public String getFraction() { StringBuilder sb = new StringBuilder(); for (String key : fraction.keySet()) { @@ -631,6 +649,11 @@ public String getFraction() { return sb.toString(); } + /** + * Get the concentration fraction transported out as a string. + * + * @return the string represenation of the fraction + */ public String getTransport() { StringBuilder sb = new StringBuilder(); for (String key : transport.keySet()) { diff --git a/src/arcade/patch/env/component/PatchComponentSitesGraphUtilities.java b/src/arcade/patch/env/component/PatchComponentSitesGraphUtilities.java index 30a62c061..3a8976123 100644 --- a/src/arcade/patch/env/component/PatchComponentSitesGraphUtilities.java +++ b/src/arcade/patch/env/component/PatchComponentSitesGraphUtilities.java @@ -6,9 +6,16 @@ import sim.util.Bag; import ec.util.MersenneTwisterFast; import arcade.core.util.Graph; +import arcade.core.util.Graph.Edge; import arcade.core.util.Graph.Strategy; import arcade.core.util.Matrix; import arcade.core.util.Solver; +import arcade.patch.env.component.PatchComponentSitesGraph.SiteEdge; +import arcade.patch.env.component.PatchComponentSitesGraph.SiteNode; +import arcade.patch.env.component.PatchComponentSitesGraphFactory.EdgeCategory; +import arcade.patch.env.component.PatchComponentSitesGraphFactory.EdgeLevel; +import arcade.patch.env.component.PatchComponentSitesGraphFactory.EdgeType; +import arcade.patch.env.component.PatchComponentSitesGraphFactory.Root; import static arcade.core.util.Graph.Edge; import static arcade.patch.env.component.PatchComponentSitesGraph.SiteEdge; import static arcade.patch.env.component.PatchComponentSitesGraph.SiteNode; @@ -1161,6 +1168,15 @@ static void calculateCurrentState(Graph graph) { } while (checkForNegativeFlow(graph)); } + /** + * Iteratively removes leaf branches from a graph. + * + *

A leaf branch is an edge connected to a non-root node with either no outgoing edges or no + * incoming edges. Such edges are marked as ignored and their endpoint pressures are set to + * {@link Double#NaN}. The process is repeated until no additional leaf branches remain. + * + * @param graph the graph to trim + */ static void trimGraph(Graph graph) { ArrayList list; Graph gCurr = graph; @@ -1198,9 +1214,16 @@ static void trimGraph(Graph graph) { } while (list.size() != 0); } - // This *MIGHT* be a problem, I think we could revisit adding this check. - // I'm not sure why it would get to the point where there would be a negative flow in the graph? + /** + * Checks whether any edge in the graph has a negative flow value. + * + * @param graph the graph to inspect + * @return {@code true} if at least one edge has a flow less than zero; {@code false} otherwise + */ static boolean checkForNegativeFlow(Graph graph) { + // This *MIGHT* be a problem, I think we could revisit adding this check. + // I'm not sure why it would get to the point where there would be a negative flow in the + // graph? boolean negative = false; for (Object obj : graph.getAllEdges()) { SiteEdge edge = (SiteEdge) obj;